mgplot
mgplot
Package to provide a frontend to matplotlib for working with timeseries data that is indexed with a PeriodIndex.
1""" 2mgplot 3------ 4 5Package to provide a frontend to matplotlib for working 6with timeseries data that is indexed with a PeriodIndex. 7""" 8 9# --- version and author 10import importlib.metadata 11 12# --- local imports 13# Do not import the utilities, test nor type-checking modules here. 14from mgplot.finalise_plot import finalise_plot, FINALISE_KW_TYPES 15from mgplot.bar_plot import bar_plot, BAR_KW_TYPES 16from mgplot.line_plot import line_plot, LINE_KW_TYPES 17from mgplot.seastrend_plot import seastrend_plot, SEASTREND_KW_TYPES 18from mgplot.postcovid_plot import postcovid_plot, POSTCOVID_KW_TYPES 19from mgplot.revision_plot import revision_plot, REVISION_KW_TYPES 20from mgplot.run_plot import run_plot, RUN_KW_TYPES 21from mgplot.summary_plot import summary_plot, SUMMARY_KW_TYPES 22from mgplot.growth_plot import ( 23 calc_growth, 24 raw_growth_plot, 25 series_growth_plot, 26 SERIES_GROWTH_KW_TYPES, 27 RAW_GROWTH_KW_TYPES, 28) 29from mgplot.multi_plot import ( 30 multi_start, 31 multi_column, 32 plot_then_finalise, 33) 34from mgplot.colors import ( 35 get_color, 36 get_party_palette, 37 colorise_list, 38 contrast, 39 abbreviate_state, 40 state_names, 41 state_abbrs, 42) 43from mgplot.settings import ( 44 get_setting, 45 set_setting, 46 set_chart_dir, 47 clear_chart_dir, 48) 49from mgplot.finalisers import ( 50 line_plot_finalise, 51 bar_plot_finalise, 52 seastrend_plot_finalise, 53 postcovid_plot_finalise, 54 revision_plot_finalise, 55 summary_plot_finalise, 56 raw_growth_plot_finalise, 57 series_growth_plot_finalise, 58 run_plot_finalise, 59) 60 61 62# --- version and author 63try: 64 __version__ = importlib.metadata.version(__name__) 65except importlib.metadata.PackageNotFoundError: 66 __version__ = "0.0.0" # Fallback for development mode 67__author__ = "Bryan Palmer" 68 69 70# --- public API 71__all__ = ( 72 "__version__", 73 "__author__", 74 # --- settings 75 "get_setting", 76 "set_setting", 77 "set_chart_dir", 78 "clear_chart_dir", 79 # --- colors 80 "get_color", 81 "get_party_palette", 82 "colorise_list", 83 "contrast", 84 "abbreviate_state", 85 "state_names", 86 "state_abbrs", 87 # --- finalise_plot 88 "finalise_plot", 89 # --- line_plot 90 "line_plot", 91 # --- bar plot 92 "bar_plot", 93 # --- seastrend_plot 94 "seastrend_plot", 95 # --- postcovid_plot 96 "postcovid_plot", 97 # --- revision_plot 98 "revision_plot", 99 # --- run_plot 100 "run_plot", 101 # --- summary_plot 102 "summary_plot", 103 # --- growth_plot 104 "calc_growth", 105 "raw_growth_plot", 106 "series_growth_plot", 107 # --- multi_plot 108 "multi_start", 109 "multi_column", 110 "plot_then_finalise", 111 # --- finaliser functions 112 "line_plot_finalise", 113 "bar_plot_finalise", 114 "seastrend_plot_finalise", 115 "postcovid_plot_finalise", 116 "revision_plot_finalise", 117 "summary_plot_finalise", 118 "raw_growth_plot_finalise", 119 "series_growth_plot_finalise", 120 "run_plot_finalise", 121 # --- typing information 122 "FINALISE_KW_TYPES", 123 "BAR_KW_TYPES", 124 "LINE_KW_TYPES", 125 "SEASTREND_KW_TYPES", 126 "POSTCOVID_KW_TYPES", 127 "REVISION_KW_TYPES", 128 "RUN_KW_TYPES", 129 "SUMMARY_KW_TYPES", 130 "SERIES_GROWTH_KW_TYPES", 131 "RAW_GROWTH_KW_TYPES", 132 # --- The rest are internal use only 133) 134# __pdoc__: dict[str, Any] = {"test": False} # hide submodules from documentation
86def get_setting(setting: str) -> Any: 87 """ 88 Get a setting from the global settings. 89 90 Arguments: 91 - setting: str - name of the setting to get. The possible settings are: 92 - file_type: str - the file type to use for saving plots 93 - figsize: tuple[float, float] - the figure size to use for plots 94 - file_dpi: int - the DPI to use for saving plots 95 - line_narrow: float - the line width for narrow lines 96 - line_normal: float - the line width for normal lines 97 - line_wide: float - the line width for wide lines 98 - bar_width: float - the width of bars in bar plots 99 - legend_font_size: float | str - the font size for legends 100 - legend: dict[str, Any] - the legend settings 101 - colors: dict[int, list[str]] - a dictionary of colors for 102 different numbers of lines 103 - chart_dir: str - the directory to save charts in 104 105 Raises: 106 - KeyError: if the setting is not found 107 108 Returns: 109 - value: Any - the value of the setting 110 """ 111 if setting not in _mgplot_defaults: 112 raise KeyError(f"Setting '{setting}' not found in _mgplot_defaults.") 113 return _mgplot_defaults[setting] # type: ignore[literal-required]
Get a setting from the global settings.
Arguments:
- setting: str - name of the setting to get. The possible settings are:
- file_type: str - the file type to use for saving plots
- figsize: tuple[float, float] - the figure size to use for plots
- file_dpi: int - the DPI to use for saving plots
- line_narrow: float - the line width for narrow lines
- line_normal: float - the line width for normal lines
- line_wide: float - the line width for wide lines
- bar_width: float - the width of bars in bar plots
- legend_font_size: float | str - the font size for legends
- legend: dict[str, Any] - the legend settings
- colors: dict[int, list[str]] - a dictionary of colors for different numbers of lines
- chart_dir: str - the directory to save charts in
Raises: - KeyError: if the setting is not found
Returns: - value: Any - the value of the setting
116def set_setting(setting: str, value: Any) -> None: 117 """ 118 Set a setting in the global settings. 119 Raises KeyError if the setting is not found. 120 121 Arguments: 122 - setting: str - name of the setting to set (see get_setting()) 123 - value: Any - the value to set the setting to 124 """ 125 126 if setting not in _mgplot_defaults: 127 raise KeyError(f"Setting '{setting}' not found in _mgplot_defaults.") 128 _mgplot_defaults[setting] = value # type: ignore[literal-required]
Set a setting in the global settings. Raises KeyError if the setting is not found.
Arguments: - setting: str - name of the setting to set (see get_setting()) - value: Any - the value to set the setting to
147def set_chart_dir(chart_dir: str) -> None: 148 """ 149 A function to set a global chart directory for finalise_plot(), 150 so that it does not need to be included as an argument in each 151 call to finalise_plot(). Create the directory if it does not exist. 152 153 Note: Path.mkdir() may raise an exception if a directory cannot be created. 154 155 Note: This is a wrapper for set_setting() to set the chart_dir setting, and 156 create the directory if it does not exist. 157 158 Arguments: 159 - chart_dir: str - the directory to set as the chart directory 160 """ 161 162 if not chart_dir: 163 chart_dir = "." # avoid the empty string 164 Path(chart_dir).mkdir(parents=True, exist_ok=True) 165 set_setting("chart_dir", chart_dir)
A function to set a global chart directory for finalise_plot(), so that it does not need to be included as an argument in each call to finalise_plot(). Create the directory if it does not exist.
Note: Path.mkdir() may raise an exception if a directory cannot be created.
Note: This is a wrapper for set_setting() to set the chart_dir setting, and create the directory if it does not exist.
Arguments: - chart_dir: str - the directory to set as the chart directory
131def clear_chart_dir() -> None: 132 """ 133 Remove all graph-image files from the global chart_dir. 134 This is a convenience function to remove all files from the 135 chart_dir directory. It does not remove the directory itself. 136 Note: the function creates the directory if it does not exist. 137 """ 138 139 chart_dir = get_setting("chart_dir") 140 Path(chart_dir).mkdir(parents=True, exist_ok=True) 141 for ext in ("png", "svg", "jpg", "jpeg"): 142 for fs_object in Path(chart_dir).glob(f"*.{ext}"): 143 if fs_object.is_file(): 144 fs_object.unlink()
Remove all graph-image files from the global chart_dir. This is a convenience function to remove all files from the chart_dir directory. It does not remove the directory itself. Note: the function creates the directory if it does not exist.
35def get_color(s: str) -> str: 36 """ 37 Return a matplotlib color for a party label 38 or an Australian state/territory. 39 """ 40 41 color_map = { 42 # --- Australian states and territories 43 ("wa", "western australia"): "gold", 44 ("sa", "south australia"): "red", 45 ("nt", "northern territory"): "#CC7722", # ochre 46 ("nsw", "new south wales"): "deepskyblue", 47 ("act", "australian capital territory"): "blue", 48 ("vic", "victoria"): "navy", 49 ("tas", "tasmania"): "seagreen", # bottle green #006A4E? 50 ("qld", "queensland"): "#c32148", # a lighter maroon 51 ("australia", "aus"): "grey", 52 # --- political parties 53 ("dissatisfied",): "darkorange", # must be before satisfied 54 ("satisfied",): "mediumblue", 55 ( 56 "lnp", 57 "l/np", 58 "liberal", 59 "liberals", 60 "coalition", 61 "dutton", 62 "ley", 63 "liberal and/or nationals", 64 ): "royalblue", 65 ( 66 "nat", 67 "nats", 68 "national", 69 "nationals", 70 ): "forestgreen", 71 ( 72 "alp", 73 "labor", 74 "albanese", 75 ): "#dd0000", 76 ( 77 "grn", 78 "green", 79 "greens", 80 ): "limegreen", 81 ( 82 "other", 83 "oth", 84 ): "darkorange", 85 } 86 87 for find_me, return_me in color_map.items(): 88 if any(x == s.lower() for x in find_me): 89 return return_me 90 91 return "darkgrey"
Return a matplotlib color for a party label or an Australian state/territory.
14def get_party_palette(party_text: str) -> str: 15 """ 16 Return a matplotlib color-map name based on party_text. 17 Works for Australian major political parties. 18 """ 19 20 # Note: light to dark maps work best 21 match party_text.lower(): 22 case "alp" | "labor": 23 return "Reds" 24 case "l/np" | "coalition": 25 return "Blues" 26 case "grn" | "green" | "greens": 27 return "Greens" 28 case "oth" | "other": 29 return "YlOrBr" 30 case "onp" | "one nation": 31 return "YlGnBu" 32 return "Purples"
Return a matplotlib color-map name based on party_text. Works for Australian major political parties.
94def colorise_list(party_list: Iterable) -> list[str]: 95 """ 96 Return a list of party/state colors for a party_list. 97 """ 98 99 return [get_color(x) for x in party_list]
Return a list of party/state colors for a party_list.
102def contrast(orig_color: str) -> str: 103 """ 104 Provide a constrasting color to any party color 105 generated by get_color() above. 106 """ 107 108 new_color = "black" 109 match orig_color: 110 case "royalblue": 111 new_color = "indianred" 112 case "indianred": 113 new_color = "mediumblue" 114 115 case "darkorange": 116 new_color = "mediumblue" 117 case "mediumblue": 118 new_color = "darkorange" 119 120 case "mediumseagreen": 121 new_color = "darkblue" 122 123 case "darkgrey": 124 new_color = "hotpink" 125 126 return new_color
Provide a constrasting color to any party color generated by get_color() above.
157def abbreviate_state(state: str) -> str: 158 """ 159 A function to abbreviate long-form state 160 names. 161 162 Arguments 163 - state: the long-form state name. 164 165 Return the abbreviation for a state name. 166 """ 167 168 return _state_names_multi.get(state.lower(), state)
A function to abbreviate long-form state names.
Arguments
- state: the long-form state name.
Return the abbreviation for a state name.
279def finalise_plot(axes: Axes, **kwargs) -> None: 280 """ 281 A function to finalise and save plots to the file system. The filename 282 for the saved plot is constructed from the global chart_dir, the plot's title, 283 any specified tag text, and the file_type for the plot. 284 285 Arguments: 286 - axes - matplotlib axes object - required 287 - kwargs 288 - title: str - plot title, also used to create the save file name 289 - xlabel: str | None - text label for the x-axis 290 - ylabel: str | None - label for the y-axis 291 - pre_tag: str - text before the title in file name 292 - tag: str - text after the title in the file name 293 (useful for ensuring that same titled charts do not over-write) 294 - chart_dir: str - location of the chart directory 295 - file_type: str - specify a file type - eg. 'png' or 'svg' 296 - lfooter: str - text to display on bottom left of plot 297 - rfooter: str - text to display of bottom right of plot 298 - lheader: str - text to display on top left of plot 299 - rheader: str - text to display of top right of plot 300 - figsize: tuple[float, float] - figure size in inches - eg. (8, 4) 301 - show: bool - whether to show the plot or not 302 - zero_y: bool - ensure y=0 is included in the plot. 303 - y0: bool - highlight the y=0 line on the plot (if in scope) 304 - x0: bool - highlights the x=0 line on the plot 305 - dont_save: bool - dont save the plot to the file system 306 - dont_close: bool - dont close the plot 307 - dpi: int - dots per inch for the saved chart 308 - legend: bool | dict - if dict, use as the arguments to pass to axes.legend(), 309 if True pass the global default arguments to axes.legend() 310 - axhspan: dict - arguments to pass to axes.axhspan() 311 - axvspan: dict - arguments to pass to axes.axvspan() 312 - axhline: dict - arguments to pass to axes.axhline() 313 - axvline: dict - arguments to pass to axes.axvline() 314 - ylim: tuple[float, float] - set lower and upper y-axis limits 315 - xlim: tuple[float, float] - set lower and upper x-axis limits 316 - preserve_lims: bool - if True, preserve the original axes limits, 317 lims saved at the start, and restored after the tight layout 318 - remove_legend: bool | None - if True, remove the legend from the plot 319 - report_kwargs: bool - if True, report the kwargs used in this function 320 321 Returns: 322 - None 323 """ 324 325 # --- check the kwargs 326 me = "finalise_plot" 327 report_kwargs(called_from=me, **kwargs) 328 validate_kwargs(FINALISE_KW_TYPES, me, **kwargs) 329 330 # --- sanity checks 331 if len(axes.get_children()) < 1: 332 print("Warning: finalise_plot() called with empty axes, which was ignored.") 333 return 334 335 # --- remember should we need to restore the axes limits 336 xlim, ylim = axes.get_xlim(), axes.get_ylim() 337 338 # margins 339 # axes.use_sticky_margins = False ### CHECK THIS 340 axes.margins(0.02) 341 axes.autoscale(tight=False) # This is problematic ... 342 343 _apply_kwargs(axes, **kwargs) 344 345 # tight layout and save the figure 346 fig = axes.figure 347 if not isinstance(fig, mpl.figure.SubFigure): # should never be a SubFigure 348 fig.tight_layout(pad=1.1) 349 if "preserve_lims" in kwargs and kwargs["preserve_lims"]: 350 # restore the original limits of the axes 351 axes.set_xlim(xlim) 352 axes.set_ylim(ylim) 353 _apply_late_kwargs(axes, **kwargs) 354 legend = axes.get_legend() 355 if legend and kwargs.get("remove_legend", False): 356 legend.remove() 357 _save_to_file(fig, **kwargs) 358 359 # show the plot in Jupyter Lab 360 if "show" in kwargs and kwargs["show"]: 361 plt.show() 362 363 # And close 364 closing = True if "dont_close" not in kwargs else not kwargs["dont_close"] 365 if closing: 366 plt.close()
A function to finalise and save plots to the file system. The filename for the saved plot is constructed from the global chart_dir, the plot's title, any specified tag text, and the file_type for the plot.
Arguments:
- axes - matplotlib axes object - required
- kwargs
- title: str - plot title, also used to create the save file name
- xlabel: str | None - text label for the x-axis
- ylabel: str | None - label for the y-axis
- pre_tag: str - text before the title in file name
- tag: str - text after the title in the file name (useful for ensuring that same titled charts do not over-write)
- chart_dir: str - location of the chart directory
- file_type: str - specify a file type - eg. 'png' or 'svg'
- lfooter: str - text to display on bottom left of plot
- rfooter: str - text to display of bottom right of plot
- lheader: str - text to display on top left of plot
- rheader: str - text to display of top right of plot
- figsize: tuple[float, float] - figure size in inches - eg. (8, 4)
- show: bool - whether to show the plot or not
- zero_y: bool - ensure y=0 is included in the plot.
- y0: bool - highlight the y=0 line on the plot (if in scope)
- x0: bool - highlights the x=0 line on the plot
- dont_save: bool - dont save the plot to the file system
- dont_close: bool - dont close the plot
- dpi: int - dots per inch for the saved chart
- legend: bool | dict - if dict, use as the arguments to pass to axes.legend(), if True pass the global default arguments to axes.legend()
- axhspan: dict - arguments to pass to axes.axhspan()
- axvspan: dict - arguments to pass to axes.axvspan()
- axhline: dict - arguments to pass to axes.axhline()
- axvline: dict - arguments to pass to axes.axvline()
- ylim: tuple[float, float] - set lower and upper y-axis limits
- xlim: tuple[float, float] - set lower and upper x-axis limits
- preserve_lims: bool - if True, preserve the original axes limits, lims saved at the start, and restored after the tight layout
- remove_legend: bool | None - if True, remove the legend from the plot
- report_kwargs: bool - if True, report the kwargs used in this function
Returns: - None
94def line_plot(data: DataT, **kwargs) -> plt.Axes: 95 """ 96 Build a single plot from the data passed in. 97 This can be a single- or multiple-line plot. 98 Return the axes object for the build. 99 100 Agruments: 101 - data: DataFrame | Series - data to plot 102 - kwargs: 103 - ax: plt.Axes | None - axes to plot on (optional) 104 - dropna: bool | list[bool] - whether to delete NAs frm the 105 data before plotting [optional] 106 - color: str | list[str] - line colors. 107 - width: float | list[float] - line widths [optional]. 108 - style: str | list[str] - line styles [optional]. 109 - alpha: float | list[float] - line transparencies [optional]. 110 - marker: str | list[str] - line markers [optional]. 111 - marker_size: float | list[float] - line marker sizes [optional]. 112 - annotate: bool | list[bool] - whether to annotate a series. 113 - rounding: int | bool | list[int | bool] - number of decimal places 114 to round an annotation. If True, a default between 0 and 2 is 115 used. 116 - fontsize: int | str | list[int | str] - font size for the 117 annotation. 118 - drawstyle: str | list[str] - matplotlib line draw styles. 119 120 Returns: 121 - axes: plt.Axes - the axes object for the plot 122 """ 123 124 # --- check the kwargs 125 me = "line_plot" 126 report_kwargs(called_from=me, **kwargs) 127 validate_kwargs(LINE_KW_TYPES, me, **kwargs) 128 129 # --- check the data 130 data = check_clean_timeseries(data, me) 131 df = DataFrame(data) # really we are only plotting DataFrames 132 df, kwargs = constrain_data(df, **kwargs) 133 134 # --- some special defaults 135 if len(df.columns) > 1: 136 # default to displaying a legend 137 kwargs["legend"] = kwargs.get("legend", True) 138 if len(df.columns) > 4: 139 # default to using a style for the lines 140 kwargs["style"] = kwargs.get("style", ["solid", "dashed", "dashdot", "dotted"]) 141 142 # --- Let's plot 143 axes, kwargs = get_axes(**kwargs) # get the axes to plot on 144 if df.empty or df.isna().all().all(): 145 # Note: finalise plot will ignore an empty axes object 146 print("Warning: No data to plot.") 147 return axes 148 149 # --- get the arguments for each line we will plot ... 150 item_count = len(df.columns) 151 num_data_points = len(df) 152 swce, kwargs = _get_style_width_color_etc(item_count, num_data_points, **kwargs) 153 154 for i, column in enumerate(df.columns): 155 series = df[column] 156 series = series.dropna() if DROPNA in swce and swce[DROPNA][i] else series 157 if series.empty or series.isna().all(): 158 continue 159 160 axes = series.plot( 161 ls=swce[STYLE][i], 162 lw=swce[WIDTH][i], 163 color=swce[COLOR][i], 164 alpha=swce[ALPHA][i], 165 marker=swce[MARKER][i], 166 ms=swce[MARKERSIZE][i], 167 drawstyle=swce[DRAWSTYLE][i], 168 ax=axes, 169 ) 170 171 if swce[ANNOTATE][i] is None or not swce[ANNOTATE][i]: 172 continue 173 174 annotate_series( 175 series, 176 axes, 177 rounding=swce[ROUNDING][i], 178 color=swce[COLOR][i], 179 fontsize=swce[FONTSIZE][i], 180 ) 181 182 # add a legend if requested 183 if len(df.columns) > 1: 184 kwargs[LEGEND] = kwargs.get(LEGEND, get_setting("legend")) 185 186 if LEGEND in kwargs: 187 make_legend(axes, kwargs[LEGEND]) 188 189 return axes
Build a single plot from the data passed in. This can be a single- or multiple-line plot. Return the axes object for the build.
Agruments:
- data: DataFrame | Series - data to plot
- kwargs:
- ax: plt.Axes | None - axes to plot on (optional)
- dropna: bool | list[bool] - whether to delete NAs frm the data before plotting [optional]
- color: str | list[str] - line colors.
- width: float | list[float] - line widths [optional].
- style: str | list[str] - line styles [optional].
- alpha: float | list[float] - line transparencies [optional].
- marker: str | list[str] - line markers [optional].
- marker_size: float | list[float] - line marker sizes [optional].
- annotate: bool | list[bool] - whether to annotate a series.
- rounding: int | bool | list[int | bool] - number of decimal places to round an annotation. If True, a default between 0 and 2 is used.
- fontsize: int | str | list[int | str] - font size for the annotation.
- drawstyle: str | list[str] - matplotlib line draw styles.
Returns:
- axes: plt.Axes - the axes object for the plot
43def bar_plot( 44 data: DataT, 45 **kwargs, 46) -> Axes: 47 """ 48 Create a bar plot from the given data. Each column in the DataFrame 49 will be stacked on top of each other, with positive values above 50 zero and negative values below zero. 51 52 Parameters 53 - data: Series - The data to plot. Can be a DataFrame or a Series. 54 - **kwargs: dict Additional keyword arguments for customization. 55 - color: list - A list of colors for the each series (column) in the DataFrame. 56 - width: float - The width of the bars. 57 - stacked: bool - If True, the bars will be stacked. 58 - rotation: int - The rotation angle in degrees for the x-axis labels. 59 - bar_legend: bool - If True, show the legend. Defaults to True 60 if more than one bar being plotted for each category. 61 - "max_ticks": int - The maximum number of ticks on the x-axis, 62 (this option only applies to PeriodIndex data.). 63 64 Note: This function does not assume all data is timeseries with a PeriodIndex, 65 66 Returns 67 - axes: Axes - The axes for the plot. 68 """ 69 70 # --- check the kwargs 71 me = "bar_plot" 72 report_kwargs(called_from=me, **kwargs) 73 validate_kwargs(BAR_KW_TYPES, me, **kwargs) 74 75 # --- get the data 76 # no call to check_clean_timeseries here, as bar plots are not 77 # necessarily timeseries data. If the data is a Series, it will be 78 # converted to a DataFrame with a single column. 79 df = DataFrame(data) # really we are only plotting DataFrames 80 df, kwargs = constrain_data(df, **kwargs) 81 item_count = len(df.columns) 82 83 defaults: dict[str, Any] = { 84 "color": get_color_list(item_count), 85 "width": get_setting("bar_width"), 86 "stacked": False, 87 "rotation": 90, 88 "bar_legend": (item_count > 1), 89 "max_ticks": 10, 90 } 91 bar_args, remaining_kwargs = apply_defaults(item_count, defaults, kwargs) 92 93 # --- plot the data 94 axes, _rkwargs = get_axes(**remaining_kwargs) 95 96 df.plot.bar( 97 ax=axes, 98 color=bar_args["color"], 99 stacked=bar_args["stacked"][0], 100 width=bar_args["width"][0], 101 legend=bar_args["bar_legend"][0], 102 ) 103 104 rotate_labels = True 105 if isinstance(df.index, PeriodIndex): 106 complete = period_range( 107 start=df.index.min(), end=df.index.max(), freq=df.index.freqstr 108 ) 109 if complete.equals(df.index): 110 # if the index is complete, we can set the labels 111 set_labels(axes, df.index, bar_args["max_ticks"][0]) 112 rotate_labels = False 113 114 if rotate_labels: 115 plt.xticks(rotation=bar_args["rotation"][0]) 116 117 return axes
Create a bar plot from the given data. Each column in the DataFrame will be stacked on top of each other, with positive values above zero and negative values below zero.
Parameters
- data: Series - The data to plot. Can be a DataFrame or a Series.
- **kwargs: dict Additional keyword arguments for customization.
- color: list - A list of colors for the each series (column) in the DataFrame.
- width: float - The width of the bars.
- stacked: bool - If True, the bars will be stacked.
- rotation: int - The rotation angle in degrees for the x-axis labels.
- bar_legend: bool - If True, show the legend. Defaults to True if more than one bar being plotted for each category.
- "max_ticks": int - The maximum number of ticks on the x-axis, (this option only applies to PeriodIndex data.).
Note: This function does not assume all data is timeseries with a PeriodIndex,
Returns
- axes: Axes - The axes for the plot.
29def seastrend_plot(data: DataT, **kwargs) -> Axes: 30 """ 31 Publish a DataFrame, where the first column is seasonally 32 adjusted data, and the second column is trend data. 33 34 Aguments: 35 - data: DataFrame - the data to plot with the first column 36 being the seasonally adjusted data, and the second column 37 being the trend data. 38 The remaining arguments are the same as those passed to 39 line_plot(). 40 41 Returns: 42 - a matplotlib Axes object 43 """ 44 45 # Note: we will rely on the line_plot() function to do most of the work. 46 # including constraining the data to the plot_from keyword argument. 47 48 # --- check the kwargs 49 me = "seastrend_plot" 50 report_kwargs(called_from=me, **kwargs) 51 validate_kwargs(SEASTREND_KW_TYPES, me, **kwargs) 52 53 # --- check the data 54 data = check_clean_timeseries(data, me) 55 if len(data.columns) < 2: 56 raise ValueError( 57 "seas_trend_plot() expects a DataFrame data item with at least 2 columns." 58 ) 59 60 # --- defaults if not in kwargs 61 colors = kwargs.pop(COLOR, get_color_list(2)) 62 widths = kwargs.pop(WIDTH, [get_setting("line_normal"), get_setting("line_wide")]) 63 styles = kwargs.pop(STYLE, ["-", "-"]) 64 annotations = kwargs.pop(ANNOTATE, [True, False]) 65 rounding = kwargs.pop(ROUNDING, True) 66 legend = kwargs.pop(LEGEND, True) 67 68 # series breaks are common in seas-trend data 69 kwargs[DROPNA] = kwargs.pop(DROPNA, False) 70 71 return line_plot( 72 data, 73 color=colors, 74 width=widths, 75 style=styles, 76 annotate=annotations, 77 rounding=rounding, 78 legend=legend, 79 **kwargs, 80 )
Publish a DataFrame, where the first column is seasonally adjusted data, and the second column is trend data.
Aguments:
- data: DataFrame - the data to plot with the first column being the seasonally adjusted data, and the second column being the trend data. The remaining arguments are the same as those passed to line_plot().
Returns:
- a matplotlib Axes object
54def postcovid_plot(data: DataT, **kwargs) -> Axes: 55 """ 56 Plots a series with a PeriodIndex. 57 58 Arguments 59 - data - the series to be plotted (note that this function 60 is designed to work with a single series, not a DataFrame). 61 - **kwargs - same as for line_plot() and finalise_plot(). 62 63 Raises: 64 - TypeError if series is not a pandas Series 65 - TypeError if series does not have a PeriodIndex 66 - ValueError if series does not have a D, M or Q frequency 67 - ValueError if regression start is after regression end 68 """ 69 70 # --- check the kwargs 71 me = "postcovid_plot" 72 report_kwargs(called_from=me, **kwargs) 73 validate_kwargs(POSTCOVID_KW_TYPES, me, **kwargs) 74 75 # --- check the data 76 data = check_clean_timeseries(data, me) 77 if not isinstance(data, Series): 78 raise TypeError("The series argument must be a pandas Series") 79 series: Series = data 80 series_index = PeriodIndex(series.index) # syntactic sugar for type hinting 81 if series_index.freqstr[:1] not in ("Q", "M", "D"): 82 raise ValueError("The series index must have a D, M or Q freq") 83 # rely on line_plot() to validate kwargs 84 if "plot_from" in kwargs: 85 print("Warning: the 'plot_from' argument is ignored in postcovid_plot().") 86 del kwargs["plot_from"] 87 88 # --- plot COVID counterfactural 89 freq = PeriodIndex(series.index).freqstr # syntactic sugar for type hinting 90 match freq[0]: 91 case "Q": 92 start_regression = Period("2014Q4", freq=freq) 93 end_regression = Period("2019Q4", freq=freq) 94 case "M": 95 start_regression = Period("2015-01", freq=freq) 96 end_regression = Period("2020-01", freq=freq) 97 case "D": 98 start_regression = Period("2015-01-01", freq=freq) 99 end_regression = Period("2020-01-01", freq=freq) 100 101 start_regression = Period(kwargs.pop("start_r", start_regression), freq=freq) 102 end_regression = Period(kwargs.pop("end_r", end_regression), freq=freq) 103 if start_regression >= end_regression: 104 raise ValueError("Start period must be before end period") 105 106 # --- combine data and projection 107 recent = series[series.index >= start_regression].copy() 108 recent.name = "Series" 109 projection = get_projection(recent, end_regression) 110 projection.name = "Pre-COVID projection" 111 data_set = DataFrame([projection, recent]).T 112 113 # --- activate plot settings 114 kwargs[WIDTH] = kwargs.pop( 115 WIDTH, (get_setting("line_normal"), get_setting("line_wide")) 116 ) # series line is thicker than projection 117 kwargs[STYLE] = kwargs.pop(STYLE, ("--", "-")) # dashed regression line 118 kwargs["legend"] = kwargs.pop("legend", True) # show legend by default 119 kwargs["annotate"] = kwargs.pop("annotate", (False, True)) # annotate series only 120 kwargs["color"] = kwargs.pop("color", ("darkblue", "#dd0000")) 121 122 return line_plot( 123 data_set, 124 **kwargs, 125 )
Plots a series with a PeriodIndex.
Arguments
- data - the series to be plotted (note that this function is designed to work with a single series, not a DataFrame).
- **kwargs - same as for line_plot() and finalise_plot().
Raises:
- TypeError if series is not a pandas Series
- TypeError if series does not have a PeriodIndex
- ValueError if series does not have a D, M or Q frequency
- ValueError if regression start is after regression end
29def revision_plot(data: DataT, **kwargs) -> Axes: 30 """ 31 Plot the revisions to ABS data. 32 33 Arguments 34 data: pd.DataFrame - the data to plot, the DataFrame has a 35 column for each data revision 36 recent: int - the number of recent data points to plot 37 kwargs : dict : 38 - units: str - the units for the data (Note: you may need to 39 recalibrate the units for the y-axis) 40 - rounding: int | bool - if True apply default rounding, otherwise 41 apply int rounding. 42 """ 43 44 # --- check the kwargs and data 45 me = "revision_plot" 46 report_kwargs(called_from=me, **kwargs) 47 validate_kwargs(REVISION_KW_TYPES, me, **kwargs) 48 49 data = check_clean_timeseries(data, me) 50 51 # --- critical defaults 52 kwargs["plot_from"] = kwargs.get("plot_from", -19) 53 54 # --- plot 55 axes = line_plot(data, **kwargs) 56 57 # --- Annotate the last value in each series ... 58 rounding: int | bool = kwargs.pop(ROUNDING, True) 59 for c in data.columns: 60 col: Series = data.loc[:, c].dropna() 61 annotate_series(col, axes, color="#222222", rounding=rounding, fontsize="small") 62 63 return axes
Plot the revisions to ABS data.
Arguments data: pd.DataFrame - the data to plot, the DataFrame has a column for each data revision recent: int - the number of recent data points to plot kwargs : dict : - units: str - the units for the data (Note: you may need to recalibrate the units for the y-axis) - rounding: int | bool - if True apply default rounding, otherwise apply int rounding.
115def run_plot(data: DataT, **kwargs) -> Axes: 116 """Plot a series of percentage rates, highlighting the increasing runs. 117 118 Arguments 119 - data - ordered pandas Series of percentages, with PeriodIndex 120 - **kwargs 121 - threshold - float - used to ignore micro noise near zero 122 (for example, threshhold=0.01) 123 - round - int - rounding for highlight text 124 - highlight - str or Sequence[str] - color(s) for highlighting the 125 runs, two colors can be specified in a list if direction is "both" 126 - direction - str - whether the highlight is for an upward 127 or downward or both runs. Options are "up", "down" or "both". 128 - in addition the **kwargs for line_plot are accepted. 129 130 Return 131 - matplotlib Axes object""" 132 133 # --- check the kwargs 134 me = "run_plot" 135 report_kwargs(called_from=me, **kwargs) 136 validate_kwargs(RUN_KW_TYPES, me, **kwargs) 137 138 # --- check the data 139 series = check_clean_timeseries(data, me) 140 if not isinstance(series, Series): 141 raise TypeError("series must be a pandas Series for run_plot()") 142 series, kwargs = constrain_data(series, **kwargs) 143 144 # --- default arguments - in **kwargs 145 kwargs[THRESHOLD] = kwargs.get(THRESHOLD, 0.1) 146 kwargs[ROUND] = kwargs.get(ROUND, 2) 147 direct = kwargs[DIRECTION] = kwargs.get(DIRECTION, "up") 148 kwargs[HIGHLIGHT], kwargs["color"] = ( 149 (kwargs.get(HIGHLIGHT, "gold"), kwargs.get("color", "#dd0000")) 150 if direct == "up" 151 else ( 152 (kwargs.get(HIGHLIGHT, "skyblue"), kwargs.get("color", "navy")) 153 if direct == "down" 154 else ( 155 kwargs.get(HIGHLIGHT, ("gold", "skyblue")), 156 kwargs.get("color", "navy"), 157 ) 158 ) 159 ) 160 161 # defauls for line_plot 162 kwargs["width"] = kwargs.get("width", 2) 163 164 # plot the line 165 kwargs["drawstyle"] = kwargs.get("drawstyle", "steps-post") 166 lp_kwargs = limit_kwargs(LINE_KW_TYPES, **kwargs) 167 axes = line_plot(series, **lp_kwargs) 168 169 # plot the runs 170 match kwargs[DIRECTION]: 171 case "up": 172 _plot_runs(axes, series, up=True, **kwargs) 173 case "down": 174 _plot_runs(axes, series, up=False, **kwargs) 175 case "both": 176 _plot_runs(axes, series, up=True, **kwargs) 177 _plot_runs(axes, series, up=False, **kwargs) 178 case _: 179 raise ValueError( 180 f"Invalid value for direction: {kwargs[DIRECTION]}. " 181 "Expected 'up', 'down', or 'both'." 182 ) 183 return axes
Plot a series of percentage rates, highlighting the increasing runs.
Arguments
- data - ordered pandas Series of percentages, with PeriodIndex
- *kwargs
- threshold - float - used to ignore micro noise near zero (for example, threshhold=0.01)
- round - int - rounding for highlight text
- highlight - str or Sequence[str] - color(s) for highlighting the runs, two colors can be specified in a list if direction is "both"
- direction - str - whether the highlight is for an upward or downward or both runs. Options are "up", "down" or "both".
- in addition the *
Return
- matplotlib Axes object
200def summary_plot( 201 data: DataT, # summary data 202 **kwargs, 203) -> Axes: 204 """Plot a summary of historical data for a given DataFrame. 205 206 Args: 207 - summary: DataFrame containing the summary data. The column names are 208 used as labels for the plot. 209 - kwargs: additional arguments for the plot, including: 210 - plot_from: int | Period | None 211 - verbose: if True, print the summary data. 212 - middle: proportion of data to highlight (default is 0.8). 213 - plot_types: list of plot types to generate. 214 215 216 Returns Axes. 217 """ 218 219 # --- check the kwargs 220 me = "summary_plot" 221 report_kwargs(called_from=me, **kwargs) 222 validate_kwargs(SUMMARY_KW_TYPES, me, **kwargs) 223 224 # --- check the data 225 data = check_clean_timeseries(data, me) 226 if not isinstance(data, DataFrame): 227 raise TypeError("data must be a pandas DataFrame for summary_plot()") 228 df = DataFrame(data) # syntactic sugar for type hinting 229 230 # --- optional arguments 231 verbose = kwargs.pop("verbose", False) 232 middle = float(kwargs.pop("middle", 0.8)) 233 plot_type = kwargs.pop("plot_type", ZSCORES) 234 kwargs["legend"] = kwargs.get( 235 "legend", 236 { 237 # put the legend below the x-axis label 238 "loc": "upper center", 239 "fontsize": "xx-small", 240 "bbox_to_anchor": (0.5, -0.125), 241 "ncol": 4, 242 }, 243 ) 244 245 # get the data, calculate z-scores and scaled scores based on the start period 246 subset, kwargs = constrain_data(df, **kwargs) 247 z_scores, z_scaled = _calculate_z(subset, middle, verbose=verbose) 248 249 # plot as required by the plot_types argument 250 adjusted = z_scores if plot_type == ZSCORES else z_scaled 251 ax = _horizontal_bar_plot(subset, adjusted, middle, plot_type, kwargs) 252 ax.tick_params(axis="y", labelsize="small") 253 make_legend(ax, kwargs["legend"]) 254 ax.set_xlim(kwargs.get("xlim", None)) # provide space for the labels 255 256 return ax
Plot a summary of historical data for a given DataFrame.
Args:
- summary: DataFrame containing the summary data. The column names are used as labels for the plot.
- kwargs: additional arguments for the plot, including:
- plot_from: int | Period | None
- verbose: if True, print the summary data.
- middle: proportion of data to highlight (default is 0.8).
- plot_types: list of plot types to generate.
Returns Axes.
63def calc_growth(series: Series) -> DataFrame: 64 """ 65 Calculate annual and periodic growth for a pandas Series, 66 where the index is a PeriodIndex. 67 68 Args: 69 - series: A pandas Series with an appropriate PeriodIndex. 70 71 Returns a two column DataFrame: 72 73 Raises 74 - TypeError if the series is not a pandas Series. 75 - TypeError if the series index is not a PeriodIndex. 76 - ValueError if the series is empty. 77 - ValueError if the series index does not have a frequency of Q, M, or D. 78 - ValueError if the series index has duplicates. 79 """ 80 81 # --- sanity checks 82 if not isinstance(series, Series): 83 raise TypeError("The series argument must be a pandas Series") 84 if not isinstance(series.index, PeriodIndex): 85 raise TypeError("The series index must be a pandas PeriodIndex") 86 if series.empty: 87 raise ValueError("The series argument must not be empty") 88 if series.index.freqstr[0] not in ("Q", "M", "D"): 89 raise ValueError("The series index must have a frequency of Q, M, or D") 90 if series.index.has_duplicates: 91 raise ValueError("The series index must not have duplicate values") 92 93 # --- ensure the index is complete and the date is sorted 94 complete = period_range(start=series.index.min(), end=series.index.max()) 95 series = series.reindex(complete, fill_value=nan) 96 series = series.sort_index(ascending=True) 97 98 # --- calculate annual and periodic growth 99 ppy = {"Q": 4, "M": 12, "D": 365}[PeriodIndex(series.index).freqstr[:1]] 100 annual = series.pct_change(periods=ppy) * 100 101 periodic = series.pct_change(periods=1) * 100 102 periodic_name = {4: "Quarterly", 12: "Monthly", 365: "Daily"}[ppy] + " Growth" 103 return DataFrame( 104 { 105 "Annual Growth": annual, 106 periodic_name: periodic, 107 } 108 )
Calculate annual and periodic growth for a pandas Series, where the index is a PeriodIndex.
Args:
- series: A pandas Series with an appropriate PeriodIndex.
Returns a two column DataFrame:
Raises
- TypeError if the series is not a pandas Series.
- TypeError if the series index is not a PeriodIndex.
- ValueError if the series is empty.
- ValueError if the series index does not have a frequency of Q, M, or D.
- ValueError if the series index has duplicates.
160def raw_growth_plot( 161 data: DataT, 162 **kwargs, 163) -> Axes: 164 """ 165 Plot annual (as a line) and periodic (as bars) growth on the 166 same axes. 167 168 Args: 169 - data: A pandas DataFrame with two columns: 170 - kwargs: 171 - line_width: The width of the line (default is 2). 172 - line_color: The color of the line (default is "darkblue"). 173 - line_style: The style of the line (default is "-"). 174 - annotate_line: None | bool | int | str - fontsize to annotate 175 the line (default is "small", which means the line is annotated with 176 small text). 177 - rounding: None | bool | int - the number of decimal places to round 178 the line (default is 0). 179 - bar_width: The width of the bars (default is 0.8). 180 - bar_color: The color of the bars (default is "indianred"). 181 - annotate_bar: None | int | str - fontsize to annotate the bars 182 (default is "small", which means the bars are annotated with 183 small text). 184 - bar_rounding: The number of decimal places to round the 185 annotations to (default is 1). 186 - plot_from: None | Period | int -- if: 187 - None: the entire series is plotted 188 - Period: the plot starts from this period 189 - int: the plot starts from this +/- index position 190 - max_ticks: The maximum number of ticks to show on the x-axis 191 (default is 10). 192 193 Returns: 194 - axes: The matplotlib Axes object. 195 196 Raises: 197 - TypeError if the annual and periodic arguments are not pandas Series. 198 - TypeError if the annual index is not a PeriodIndex. 199 - ValueError if the annual and periodic series do not have the same index. 200 """ 201 202 # --- check the kwargs 203 me = "raw_growth_plot" 204 report_kwargs(called_from=me, **kwargs) 205 validate_kwargs(RAW_GROWTH_KW_TYPES, me, **kwargs) 206 207 # --- data checks 208 data = check_clean_timeseries(data, me) 209 if len(data.columns) != 2: 210 raise TypeError("The data argument must be a pandas DataFrame with two columns") 211 212 # --- get the series of interest ... 213 annual = data[data.columns[0]] 214 periodic = data[data.columns[1]] 215 216 # --- plot 217 plot_from: None | Period | int = kwargs.get("plot_from", None) 218 if plot_from is not None: 219 if isinstance(plot_from, int): 220 plot_from = annual.index[plot_from] 221 annual = annual[annual.index >= plot_from] 222 periodic = periodic[periodic.index >= plot_from] 223 224 save_index = PeriodIndex(annual.index).copy() 225 annual.index = Index(range(len(annual))) 226 annual.name = "Annual Growth" 227 periodic.index = annual.index 228 periodic.name = {"M": "Monthly", "Q": "Quarterly", "D": "Daily"}[ 229 PeriodIndex(save_index).freqstr[:1] 230 ] + " Growth" 231 color = kwargs.get("bar_color", "#dd0000") 232 kwargs["bar_color"] = color # for annotations 233 axes = periodic.plot.bar( 234 color=color, 235 width=kwargs.get("bar_width}", 0.8), 236 ) 237 thin_threshold = 180 238 annual.plot( 239 ax=axes, 240 color=kwargs.get("line_color", "darkblue"), 241 lw=kwargs.get( 242 "line_width", 243 ( 244 get_setting("line_normal") 245 if len(annual) >= thin_threshold 246 else get_setting("line_wide") 247 ), 248 ), 249 linestyle=kwargs.get("line_style", "-"), 250 ) 251 _annotations(annual, periodic, axes, **kwargs) 252 253 # --- expose the legend by default 254 legend = kwargs.get("legend", True) 255 make_legend(axes, legend) 256 257 # --- fix the x-axis labels 258 set_labels(axes, save_index, kwargs.get("max_ticks", 10)) 259 260 # --- and done ... 261 return axes
Plot annual (as a line) and periodic (as bars) growth on the same axes.
Args:
- data: A pandas DataFrame with two columns:
- kwargs:
- line_width: The width of the line (default is 2).
- line_color: The color of the line (default is "darkblue").
- line_style: The style of the line (default is "-").
- annotate_line: None | bool | int | str - fontsize to annotate the line (default is "small", which means the line is annotated with small text).
- rounding: None | bool | int - the number of decimal places to round the line (default is 0).
- bar_width: The width of the bars (default is 0.8).
- bar_color: The color of the bars (default is "indianred").
- annotate_bar: None | int | str - fontsize to annotate the bars (default is "small", which means the bars are annotated with small text).
- bar_rounding: The number of decimal places to round the annotations to (default is 1).
- plot_from: None | Period | int -- if:
- None: the entire series is plotted
- Period: the plot starts from this period
- int: the plot starts from this +/- index position
- max_ticks: The maximum number of ticks to show on the x-axis (default is 10).
Returns:
- axes: The matplotlib Axes object.
Raises:
- TypeError if the annual and periodic arguments are not pandas Series.
- TypeError if the annual index is not a PeriodIndex.
- ValueError if the annual and periodic series do not have the same index.
264def series_growth_plot( 265 data: DataT, 266 **kwargs, 267) -> Axes: 268 """ 269 Plot annual and periodic growth from a pandas Series, 270 and finalise the plot. 271 272 Args: 273 - data: A pandas Series with an appropriate PeriodIndex. 274 - kwargs: 275 - takes the same kwargs as for growth_plot() 276 """ 277 278 # --- check the kwargs 279 me = "series_growth_plot" 280 report_kwargs(called_from=me, **kwargs) 281 validate_kwargs(SERIES_GROWTH_KW_TYPES, me, **kwargs) 282 283 # --- sanity checks 284 if not isinstance(data, Series): 285 raise TypeError( 286 "The data argument to series_growth_plot() must be a pandas Series" 287 ) 288 289 # --- calculate growth and plot - add ylabel 290 ylabel: str | None = kwargs.pop("ylabel", None) 291 if ylabel is not None: 292 print(f"Did you intend to specify a value for the 'ylabel' in {me}()?") 293 ylabel = "Growth (%)" if ylabel is None else ylabel 294 growth = calc_growth(data) 295 ax = raw_growth_plot(growth, **kwargs) 296 ax.set_ylabel(ylabel) 297 return ax
Plot annual and periodic growth from a pandas Series, and finalise the plot.
Args:
- data: A pandas Series with an appropriate PeriodIndex.
- kwargs:
- takes the same kwargs as for growth_plot()
188def multi_start( 189 data: DataT, 190 function: Callable | list[Callable], 191 starts: Iterable[None | Period | int], 192 **kwargs, 193) -> None: 194 """ 195 Create multiple plots with different starting points. 196 Each plot will start from the specified starting point. 197 198 Parameters 199 - data: Series | DataFrame - The data to be plotted. 200 - function: Callable | list[Callable] - The plotting function 201 to be used. 202 - starts: Iterable[Period | int | None] - The starting points 203 for each plot (None means use the entire data). 204 - **kwargs: Additional keyword arguments to be passed to 205 the plotting function. 206 207 Returns None. 208 209 Raises 210 - ValueError if the starts is not an iterable of None, Period or int. 211 212 Note: kwargs['tag'] is used to create a unique tag for each plot. 213 """ 214 215 # --- sanity checks 216 me = "multi_start" 217 report_kwargs(called_from=me, **kwargs) 218 if not isinstance(starts, Iterable): 219 raise ValueError("starts must be an iterable of None, Period or int") 220 # data not checked here, assume it is checked by the called 221 # plot function. 222 223 # --- check the function argument 224 original_tag: Final[str] = kwargs.get("tag", "") 225 first, kwargs["function"] = first_unchain(function) 226 if not kwargs["function"]: 227 del kwargs["function"] # remove the function key if it is empty 228 229 # --- iterate over the starts 230 for i, start in enumerate(starts): 231 kw = kwargs.copy() # copy to avoid modifying the original kwargs 232 this_tag = f"{original_tag}_{i}" 233 kw["tag"] = this_tag 234 kw["plot_from"] = start # rely on plotting function to constrain the data 235 first(data, **kw)
Create multiple plots with different starting points. Each plot will start from the specified starting point.
Parameters
- data: Series | DataFrame - The data to be plotted.
- function: Callable | list[Callable] - The plotting function to be used.
- starts: Iterable[Period | int | None] - The starting points for each plot (None means use the entire data).
- **kwargs: Additional keyword arguments to be passed to the plotting function.
Returns None.
Raises
- ValueError if the starts is not an iterable of None, Period or int.
Note: kwargs['tag'] is used to create a unique tag for each plot.
238def multi_column( 239 data: DataFrame, 240 function: Callable | list[Callable], 241 **kwargs, 242) -> None: 243 """ 244 Create multiple plots, one for each column in a DataFrame. 245 The plot title will be the column name. 246 247 Parameters 248 - data: DataFrame - The data to be plotted 249 - function: Callable - The plotting function to be used. 250 - **kwargs: Additional keyword arguments to be passed to 251 the plotting function. 252 253 Returns None. 254 """ 255 256 # --- sanity checks 257 me = "multi_column" 258 report_kwargs(called_from=me, **kwargs) 259 if not isinstance(data, DataFrame): 260 raise TypeError("data must be a pandas DataFrame for multi_column()") 261 # Otherwise, the data is assumed to be checked by the called 262 # plot function, so we do not check it here. 263 264 # --- check the function argument 265 title_stem = kwargs.get("title", "") 266 tag: Final[str] = kwargs.get("tag", "") 267 first, kwargs["function"] = first_unchain(function) 268 if not kwargs["function"]: 269 del kwargs["function"] # remove the function key if it is empty 270 271 # --- iterate over the columns 272 for i, col in enumerate(data.columns): 273 274 series = data[[col]] 275 kwargs["title"] = f"{title_stem}{col}" if title_stem else col 276 277 this_tag = f"_{tag}_{i}".replace("__", "_") 278 kwargs["tag"] = this_tag 279 280 first(series, **kwargs)
Create multiple plots, one for each column in a DataFrame. The plot title will be the column name.
Parameters
- data: DataFrame - The data to be plotted
- function: Callable - The plotting function to be used.
- **kwargs: Additional keyword arguments to be passed to the plotting function.
Returns None.
123def plot_then_finalise( 124 data: DataT, 125 function: Callable | list[Callable], 126 **kwargs, 127) -> None: 128 """ 129 Chain a plotting function with the finalise_plot() function. 130 This is designed to be the last function in a chain. 131 132 Parameters 133 - data: Series | DataFrame - The data to be plotted. 134 - function: Callable | list[Callable] - The plotting function 135 to be used. 136 - **kwargs: Additional keyword arguments to be passed to 137 the plotting function, and then the finalise_plot() function. 138 139 Returns None. 140 """ 141 142 # --- checks 143 me = "plot_then_finalise" 144 report_kwargs(called_from=me, **kwargs) 145 # validate once we have established the first function 146 147 # data is not checked here, assume it is checked by the called 148 # plot function. 149 150 first, kwargs["function"] = first_unchain(function) 151 if not kwargs["function"]: 152 del kwargs["function"] # remove the function key if it is empty 153 154 bad_next = (multi_start, multi_column) 155 if first in bad_next: 156 # these functions should not be called by plot_then_finalise() 157 raise ValueError( 158 f"[{', '.join(k.__name__ for k in bad_next)}] should not be called by {me}. " 159 "Call them before calling {me}. " 160 ) 161 162 if first in EXPECTED_CALLABLES: 163 expected = EXPECTED_CALLABLES[first] 164 plot_kwargs = limit_kwargs(expected, **kwargs) 165 else: 166 # this is an unexpected Callable, so we will give it a try 167 print(f"Unknown proposed function: {first}; nonetheless, will give it a try.") 168 expected = {} 169 plot_kwargs = kwargs.copy() 170 171 # --- validate the original kwargs (could not do before now) 172 validate_kwargs(FINALISE_KW_TYPES | expected, me, **kwargs) 173 174 # --- call the first function with the data and selected plot kwargs 175 axes = first(data, **plot_kwargs) 176 177 # --- remove potentially overlapping kwargs 178 fp_kwargs = limit_kwargs(FINALISE_KW_TYPES, **kwargs) 179 overlapping = expected.keys() & FINALISE_KW_TYPES.keys() 180 if overlapping: 181 for key in overlapping: 182 fp_kwargs.pop(key, None) # remove overlapping keys from kwargs 183 184 # --- finalise the plot 185 finalise_plot(axes, **fp_kwargs)
Chain a plotting function with the finalise_plot() function. This is designed to be the last function in a chain.
Parameters
- data: Series | DataFrame - The data to be plotted.
- function: Callable | list[Callable] - The plotting function to be used.
- **kwargs: Additional keyword arguments to be passed to the plotting function, and then the finalise_plot() function.
Returns None.
41def line_plot_finalise( 42 data: DataT, 43 **kwargs, 44) -> None: 45 """ 46 A convenience function to call plot_then_finalise(), which 47 wraps calls to line_plot() and finalise_plot(). 48 """ 49 50 plot_then_finalise( 51 data, 52 function=line_plot, 53 **kwargs, 54 )
A convenience function to call plot_then_finalise(), which wraps calls to line_plot() and finalise_plot().
57def bar_plot_finalise( 58 data: DataT, 59 **kwargs, 60) -> None: 61 """ 62 A convenience function to call plot_then_finalise(), which 63 wraps calls to bar_plot() and finalise_plot(). 64 """ 65 66 plot_then_finalise( 67 data, 68 function=bar_plot, 69 **kwargs, 70 )
A convenience function to call plot_then_finalise(), which wraps calls to bar_plot() and finalise_plot().
73def seastrend_plot_finalise( 74 data: DataT, 75 **kwargs, 76) -> None: 77 """ 78 A convenience function to call seas_trend_plot() and finalise_plot(). 79 """ 80 81 plot_then_finalise( 82 data, 83 function=seastrend_plot, 84 **kwargs, 85 )
A convenience function to call seas_trend_plot() and finalise_plot().
88def postcovid_plot_finalise( 89 data: DataT, 90 **kwargs, 91) -> None: 92 """ 93 A convenience function to call postcovid_plot() and finalise_plot(). 94 """ 95 96 plot_then_finalise( 97 data, 98 function=postcovid_plot, 99 **kwargs, 100 )
A convenience function to call postcovid_plot() and finalise_plot().
103def revision_plot_finalise( 104 data: DataT, 105 **kwargs, 106) -> None: 107 """ 108 A convenience function to call revision_plot() and finalise_plot(). 109 """ 110 111 kwargs["legend"] = kwargs.get( 112 "legend", {"loc": "best", "fontsize": "x-small", "ncol": 2} 113 ) 114 kwargs["style"] = kwargs.get("style", ["solid", "dashed", "dashdot", "dotted"]) 115 plot_then_finalise( 116 data, 117 function=revision_plot, 118 **kwargs, 119 )
A convenience function to call revision_plot() and finalise_plot().
165def summary_plot_finalise( 166 data: DataT, 167 **kwargs, 168) -> None: 169 """ 170 A convenience function to call summary_plot() and finalise_plot(). 171 This is more complex than most convienience methods. 172 173 Arguments 174 - data: DataFrame containing the summary data. The index must be a PeriodIndex. 175 - kwargs: additional arguments for the plot, including: 176 - plot_from: int | Period | None (None means plot from 1995-01-01) 177 - verbose: if True, print the summary data. 178 - middle: proportion of data to highlight (default is 0.8). 179 - plot_type: list of plot types to generate (either "zscores" or "zscaled") 180 defaults to "zscores". 181 """ 182 183 # --- standard arguments 184 kwargs["title"] = kwargs.get("title", f"Summary at {data.index[-1]}") 185 kwargs["preserve_lims"] = kwargs.get( 186 "preserve_lims", True 187 ) # preserve the x-axis limits 188 189 start: None | int | Period = kwargs.get("plot_from", None) 190 if start is None: 191 start = data.index[0] 192 if isinstance(start, int): 193 start = data.index[start] 194 kwargs["plot_from"] = start 195 196 for plot_type in (ZSCORES, ZSCALED): 197 # some sorting of kwargs for plot production 198 kwargs["plot_type"] = plot_type 199 kwargs["pre_tag"] = plot_type # necessary because the title is the same 200 201 if plot_type == "zscores": 202 kwargs["xlabel"] = f"Z-scores for prints since {start}" 203 kwargs["x0"] = True 204 else: 205 kwargs["xlabel"] = f"-1 to 1 scaled z-scores since {start}" 206 kwargs.pop("x0", None) 207 208 plot_then_finalise( 209 data, 210 function=summary_plot, 211 **kwargs, 212 )
A convenience function to call summary_plot() and finalise_plot(). This is more complex than most convienience methods.
Arguments
- data: DataFrame containing the summary data. The index must be a PeriodIndex.
- kwargs: additional arguments for the plot, including:
- plot_from: int | Period | None (None means plot from 1995-01-01)
- verbose: if True, print the summary data.
- middle: proportion of data to highlight (default is 0.8).
- plot_type: list of plot types to generate (either "zscores" or "zscaled") defaults to "zscores".
151def raw_growth_plot_finalise(data: DataT, **kwargs) -> None: 152 """ 153 A convenience function to call series_growth_plot() and finalise_plot(). 154 Use this when you are providing the raw growth data. Don't forget to 155 set the ylabel in kwargs. 156 """ 157 158 plot_then_finalise( 159 data=data, 160 function=raw_growth_plot, 161 **kwargs, 162 )
A convenience function to call series_growth_plot() and finalise_plot(). Use this when you are providing the raw growth data. Don't forget to set the ylabel in kwargs.
137def series_growth_plot_finalise(data: DataT, **kwargs) -> None: 138 """ 139 A convenience function to call series_growth_plot() and finalise_plot(). 140 Use this when you are providing the series data, for mgplot to calculate 141 the growth series. 142 """ 143 144 plot_then_finalise( 145 data=data, 146 function=series_growth_plot, 147 **kwargs, 148 )
A convenience function to call series_growth_plot() and finalise_plot(). Use this when you are providing the series data, for mgplot to calculate the growth series.
122def run_plot_finalise( 123 data: DataT, 124 **kwargs, 125) -> None: 126 """ 127 A convenience function to call run_plot() and finalise_plot(). 128 """ 129 130 plot_then_finalise( 131 data=data, 132 function=run_plot, 133 **kwargs, 134 )
A convenience function to call run_plot() and finalise_plot().