mgplot
Provide a frontend to matplotlib for working with timeseries data, indexed with a PeriodIndex.
This package simplifiers the creation of common plots used in economic and financial analysis, such as bar plots, line plots, growth plots, and seasonal trend plots. It also includes utilities for color management and finalising plots with consistent styling.
1"""Provide a frontend to matplotlib for working with timeseries data, indexed with a PeriodIndex. 2 3This package simplifiers the creation of common plots used in economic and financial analysis, 4such as bar plots, line plots, growth plots, and seasonal trend plots. It also includes utilities 5for color management and finalising plots with consistent styling. 6""" 7 8# --- version and author 9import importlib.metadata 10 11# --- local imports 12# Do not import the utilities, axis_utils nor keyword_checking modules here. 13from mgplot.bar_plot import BarKwargs, bar_plot 14from mgplot.colors import ( 15 abbreviate_state, 16 colorise_list, 17 contrast, 18 get_color, 19 get_party_palette, 20 state_abbrs, 21 state_names, 22) 23from mgplot.fill_between_plot import FillBetweenKwargs, fill_between_plot 24from mgplot.finalise_plot import FinaliseKwargs, finalise_plot 25from mgplot.finalisers import ( 26 bar_plot_finalise, 27 fill_between_plot_finalise, 28 growth_plot_finalise, 29 line_plot_finalise, 30 postcovid_plot_finalise, 31 revision_plot_finalise, 32 run_plot_finalise, 33 seastrend_plot_finalise, 34 series_growth_plot_finalise, 35 summary_plot_finalise, 36) 37from mgplot.growth_plot import ( 38 GrowthKwargs, 39 SeriesGrowthKwargs, 40 calc_growth, 41 growth_plot, 42 series_growth_plot, 43) 44from mgplot.line_plot import LineKwargs, line_plot 45from mgplot.multi_plot import multi_column, multi_start, plot_then_finalise 46from mgplot.postcovid_plot import PostcovidKwargs, postcovid_plot 47from mgplot.revision_plot import revision_plot 48from mgplot.run_plot import RunKwargs, run_plot 49from mgplot.seastrend_plot import seastrend_plot 50from mgplot.settings import ( 51 clear_chart_dir, 52 get_setting, 53 set_chart_dir, 54 set_setting, 55) 56from mgplot.summary_plot import SummaryKwargs, summary_plot 57 58# --- version and author 59try: 60 __version__ = importlib.metadata.version(__name__) 61except importlib.metadata.PackageNotFoundError: 62 __version__ = "0.0.0" # Fallback for development mode 63__author__ = "Bryan Palmer" 64 65 66# --- public API 67__all__ = ( 68 "BarKwargs", 69 "FillBetweenKwargs", 70 "FinaliseKwargs", 71 "GrowthKwargs", 72 "LineKwargs", 73 "PostcovidKwargs", 74 "RunKwargs", 75 "SeriesGrowthKwargs", 76 "SummaryKwargs", 77 "__author__", 78 "__version__", 79 "abbreviate_state", 80 "bar_plot", 81 "bar_plot_finalise", 82 "fill_between_plot", 83 "fill_between_plot_finalise", 84 "calc_growth", 85 "clear_chart_dir", 86 "colorise_list", 87 "contrast", 88 "finalise_plot", 89 "get_color", 90 "get_party_palette", 91 "get_setting", 92 "growth_plot", 93 "growth_plot_finalise", 94 "line_plot", 95 "line_plot_finalise", 96 "multi_column", 97 "multi_start", 98 "plot_then_finalise", 99 "postcovid_plot", 100 "postcovid_plot_finalise", 101 "revision_plot", 102 "revision_plot_finalise", 103 "run_plot", 104 "run_plot", 105 "run_plot_finalise", 106 "seastrend_plot", 107 "seastrend_plot_finalise", 108 "series_growth_plot", 109 "series_growth_plot_finalise", 110 "set_chart_dir", 111 "set_setting", 112 "state_abbrs", 113 "state_names", 114 "summary_plot", 115 "summary_plot_finalise", 116)
41class BarKwargs(BaseKwargs): 42 """Keyword arguments for the bar_plot function.""" 43 44 # --- options for the entire bar plot 45 ax: NotRequired[Axes | None] 46 stacked: NotRequired[bool] 47 max_ticks: NotRequired[int] 48 plot_from: NotRequired[int | Period] 49 label_rotation: NotRequired[int | float] 50 # --- options for each bar ... 51 color: NotRequired[str | Sequence[str]] 52 label_series: NotRequired[bool | Sequence[bool]] 53 width: NotRequired[float | int | Sequence[float | int]] 54 zorder: NotRequired[int | float | Sequence[int | float]] 55 # --- options for bar annotations 56 annotate: NotRequired[bool] 57 fontsize: NotRequired[int | float | str] 58 fontname: NotRequired[str] 59 rounding: NotRequired[int] 60 rotation: NotRequired[int | float] 61 annotate_color: NotRequired[str] 62 above: NotRequired[bool]
Keyword arguments for the bar_plot function.
21class FillBetweenKwargs(BaseKwargs): 22 """Keyword arguments for the fill_between_plot function.""" 23 24 ax: NotRequired[Axes | None] 25 color: NotRequired[str] 26 alpha: NotRequired[float] 27 label: NotRequired[str | None] 28 linewidth: NotRequired[float] 29 edgecolor: NotRequired[str | None] 30 zorder: NotRequired[int | float] 31 plot_from: NotRequired[int | None] 32 max_ticks: NotRequired[int]
Keyword arguments for the fill_between_plot function.
31class FinaliseKwargs(BaseKwargs): 32 """Keyword arguments for the finalise_plot function.""" 33 34 # --- value options 35 suptitle: NotRequired[str | None] 36 title: NotRequired[str | None] 37 xlabel: NotRequired[str | None] 38 ylabel: NotRequired[str | None] 39 xlim: NotRequired[tuple[float, float] | None] 40 ylim: NotRequired[tuple[float, float] | None] 41 xticks: NotRequired[list[float] | None] 42 yticks: NotRequired[list[float] | None] 43 xscale: NotRequired[str | None] 44 yscale: NotRequired[str | None] 45 # --- splat options 46 legend: NotRequired[bool | dict[str, Any] | None] 47 axhspan: NotRequired[dict[str, Any]] 48 axvspan: NotRequired[dict[str, Any]] 49 axhline: NotRequired[dict[str, Any]] 50 axvline: NotRequired[dict[str, Any]] 51 # --- options for annotations 52 lfooter: NotRequired[str] 53 rfooter: NotRequired[str] 54 lheader: NotRequired[str] 55 rheader: NotRequired[str] 56 # --- file/save options 57 pre_tag: NotRequired[str] 58 tag: NotRequired[str] 59 chart_dir: NotRequired[str] 60 file_type: NotRequired[str] 61 dpi: NotRequired[int] 62 figsize: NotRequired[tuple[float, float]] 63 show: NotRequired[bool] 64 # --- other options 65 preserve_lims: NotRequired[bool] 66 remove_legend: NotRequired[bool] 67 zero_y: NotRequired[bool] 68 y0: NotRequired[bool] 69 x0: NotRequired[bool] 70 dont_save: NotRequired[bool] 71 dont_close: NotRequired[bool]
Keyword arguments for the finalise_plot function.
38class GrowthKwargs(BaseKwargs): 39 """Keyword arguments for the growth_plot function.""" 40 41 # --- common options 42 ax: NotRequired[Axes | None] 43 plot_from: NotRequired[int | Period] 44 label_series: NotRequired[bool] 45 max_ticks: NotRequired[int] 46 # --- options passed to the line plot 47 line_width: NotRequired[float | int] 48 line_color: NotRequired[str] 49 line_style: NotRequired[str] 50 annotate_line: NotRequired[bool] 51 line_rounding: NotRequired[bool | int] 52 line_fontsize: NotRequired[str | int | float] 53 line_fontname: NotRequired[str] 54 line_anno_color: NotRequired[str] 55 # --- options passed to the bar plot 56 annotate_bars: NotRequired[bool] 57 bar_fontsize: NotRequired[str | int | float] 58 bar_fontname: NotRequired[str] 59 bar_rounding: NotRequired[int] 60 bar_width: NotRequired[float] 61 bar_color: NotRequired[str] 62 bar_anno_color: NotRequired[str] 63 bar_rotation: NotRequired[int | float]
Keyword arguments for the growth_plot function.
28class LineKwargs(BaseKwargs): 29 """Keyword arguments for the line_plot function.""" 30 31 # --- options for the entire line plot 32 ax: NotRequired[Axes | None] 33 style: NotRequired[str | Sequence[str]] 34 width: NotRequired[float | int | Sequence[float | int]] 35 color: NotRequired[str | Sequence[str]] 36 alpha: NotRequired[float | Sequence[float]] 37 drawstyle: NotRequired[str | Sequence[str] | None] 38 marker: NotRequired[str | Sequence[str] | None] 39 markersize: NotRequired[float | Sequence[float] | int | None] 40 zorder: NotRequired[int | float | Sequence[int | float]] 41 dropna: NotRequired[bool | Sequence[bool]] 42 annotate: NotRequired[bool | Sequence[bool]] 43 rounding: NotRequired[Sequence[int | bool] | int | bool | None] 44 fontsize: NotRequired[Sequence[str | int | float] | str | int | float] 45 fontname: NotRequired[str | Sequence[str]] 46 rotation: NotRequired[Sequence[int | float] | int | float] 47 annotate_color: NotRequired[str | Sequence[str] | bool | Sequence[bool] | None] 48 plot_from: NotRequired[int | Period | None] 49 label_series: NotRequired[bool | Sequence[bool] | None] 50 max_ticks: NotRequired[int]
Keyword arguments for the line_plot function.
30class PostcovidKwargs(LineKwargs): 31 """Keyword arguments for the post-COVID plot.""" 32 33 start_r: NotRequired[Period] # start of regression period 34 end_r: NotRequired[Period] # end of regression period
Keyword arguments for the post-COVID plot.
32class RunKwargs(LineKwargs): 33 """Keyword arguments for the run_plot function.""" 34 35 threshold: NotRequired[float] 36 direction: NotRequired[str] 37 highlight_color: NotRequired[str | Sequence[str]] 38 highlight_label: NotRequired[str | Sequence[str]]
Keyword arguments for the run_plot function.
66class SeriesGrowthKwargs(GrowthKwargs): 67 """Keyword arguments for the series_growth_plot function.""" 68 69 ylabel: NotRequired[str | None]
Keyword arguments for the series_growth_plot function.
41class SummaryKwargs(BaseKwargs): 42 """Keyword arguments for the summary_plot function.""" 43 44 ax: NotRequired[Axes | None] 45 verbose: NotRequired[bool] 46 middle: NotRequired[float] 47 plot_type: NotRequired[str] 48 plot_from: NotRequired[int | Period] 49 legend: NotRequired[bool | dict[str, Any] | None] 50 xlabel: NotRequired[str | None]
Keyword arguments for the summary_plot function.
158def abbreviate_state(state: str) -> str: 159 """Abbreviate long-form state names. 160 161 Args: 162 state: str - the long-form state name. 163 164 Return the abbreviation for a state name. 165 166 """ 167 return _state_names_multi.get(state.lower(), state)
Abbreviate long-form state names.
Args: state: str - the long-form state name.
Return the abbreviation for a state name.
215def bar_plot(data: DataT, **kwargs: Unpack[BarKwargs]) -> Axes: 216 """Create a bar plot from the given data. 217 218 Each column in the DataFrame will be stacked on top of each other, 219 with positive values above zero and negative values below zero. 220 221 Args: 222 data: Series | DataFrame - The data to plot. Can be a DataFrame or a Series. 223 **kwargs: BarKwargs - Additional keyword arguments for customization. 224 (see BarKwargs for details) 225 226 Note: This function does not assume all data is timeseries with a PeriodIndex. 227 228 Returns: 229 axes: Axes - The axes for the plot. 230 231 """ 232 # --- check the kwargs 233 report_kwargs(caller=ME, **kwargs) 234 validate_kwargs(schema=BarKwargs, caller=ME, **kwargs) 235 236 # --- get the data 237 # no call to check_clean_timeseries here, as bar plots are not 238 # necessarily timeseries data. If the data is a Series, it will be 239 # converted to a DataFrame with a single column. 240 df = DataFrame(data) # really we are only plotting DataFrames 241 df, kwargs_d = constrain_data(df, **kwargs) 242 item_count = len(df.columns) 243 244 # --- deal with complete PeriodIndex indices 245 saved_pi = map_periodindex(df) 246 if saved_pi is not None: 247 df = saved_pi[0] # extract the reindexed DataFrame from the PeriodIndex 248 249 # --- set up the default arguments 250 chart_defaults: dict[str, bool | int] = { 251 "stacked": False, 252 "max_ticks": DEFAULT_MAX_TICKS, 253 "label_series": item_count > 1, 254 "xlabel_rotation": 0, 255 } 256 chart_args = {k: kwargs_d.get(k, v) for k, v in chart_defaults.items()} 257 258 bar_defaults = { 259 "color": get_color_list(item_count), 260 "width": get_setting("bar_width"), 261 "label_series": item_count > 1, 262 "zorder": None, 263 } 264 above = kwargs_d.get("above", False) 265 anno_args: AnnoKwargs = { 266 "annotate": kwargs_d.get("annotate", False), 267 "fontsize": kwargs_d.get("fontsize", "small"), 268 "fontname": kwargs_d.get("fontname", "Helvetica"), 269 "rotation": kwargs_d.get("rotation", 0), 270 "rounding": kwargs_d.get("rounding", True), 271 "color": kwargs_d.get("annotate_color", "black" if above else "white"), 272 "above": above, 273 } 274 bar_args, remaining_kwargs = apply_defaults(item_count, bar_defaults, kwargs_d) 275 276 # --- plot the data 277 axes, remaining_kwargs = get_axes(**dict(remaining_kwargs)) 278 if chart_args["stacked"]: 279 stacked(axes, df, anno_args, **bar_args) 280 else: 281 grouped(axes, df, anno_args, **bar_args) 282 283 # --- handle complete periodIndex data and label rotation 284 if saved_pi is not None: 285 set_labels(axes, saved_pi[1], chart_args["max_ticks"]) 286 plt.xticks(rotation=chart_args["xlabel_rotation"]) 287 288 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.
Args: data: Series | DataFrame - The data to plot. Can be a DataFrame or a Series. **kwargs: BarKwargs - Additional keyword arguments for customization. (see BarKwargs for details)
Note: This function does not assume all data is timeseries with a PeriodIndex.
Returns: axes: Axes - The axes for the plot.
138def bar_plot_finalise( 139 data: DataT, 140 **kwargs: Unpack[BPFKwargs], 141) -> None: 142 """Call bar_plot() and finalise_plot(). 143 144 Args: 145 data: The data to be plotted. 146 kwargs: Combined bar plot and finalise plot keyword arguments. 147 148 """ 149 validate_kwargs(schema=BPFKwargs, caller="bar_plot_finalise", **kwargs) 150 kwargs = impose_legend(kwargs=kwargs, data=data) 151 plot_then_finalise( 152 data, 153 function=bar_plot, 154 **kwargs, 155 )
Call bar_plot() and finalise_plot().
Args: data: The data to be plotted. kwargs: Combined bar plot and finalise plot keyword arguments.
35def fill_between_plot(data: DataFrame, **kwargs: Unpack[FillBetweenKwargs]) -> Axes: 36 """Plot a filled region between lower and upper bounds. 37 38 Args: 39 data: DataFrame - A two-column DataFrame with PeriodIndex. 40 The first column is the lower bound, the second is the upper bound. 41 kwargs: FillBetweenKwargs - keyword arguments for the plot. 42 43 Returns: 44 Axes - matplotlib Axes object. 45 46 Raises: 47 TypeError: If data is not a DataFrame. 48 ValueError: If data does not have exactly two columns. 49 50 """ 51 # --- validate inputs 52 report_kwargs(caller=ME, **kwargs) 53 validate_kwargs(schema=FillBetweenKwargs, caller=ME, **kwargs) 54 55 if not isinstance(data, DataFrame): 56 raise TypeError(f"data must be a DataFrame for {ME}()") 57 58 if len(data.columns) != REQUIRED_COLUMNS: 59 raise ValueError(f"data must have exactly two columns for {ME}(), got {len(data.columns)}") 60 61 # --- check and constrain data 62 data = check_clean_timeseries(data, ME) 63 data, kwargs_d = constrain_data(data, **kwargs) 64 65 # --- handle PeriodIndex conversion 66 saved_pi = map_periodindex(data) 67 if saved_pi is not None: 68 data = saved_pi[0] 69 70 # --- get axes 71 axes, kwargs_d = get_axes(**kwargs_d) 72 73 if data.empty or data.isna().all().all(): 74 print(f"Warning: No data to plot in {ME}().") 75 return axes 76 77 # --- extract bounds 78 lower = data.iloc[:, 0] 79 upper = data.iloc[:, 1] 80 81 # --- extract plot arguments 82 color = kwargs_d.get("color", DEFAULT_COLOR) 83 alpha = kwargs_d.get("alpha", DEFAULT_ALPHA) 84 label = kwargs_d.get("label", None) 85 linewidth = kwargs_d.get("linewidth", 0) 86 edgecolor = kwargs_d.get("edgecolor", None) 87 zorder = kwargs_d.get("zorder", None) 88 89 # --- plot 90 axes.fill_between( 91 data.index, 92 lower, 93 upper, 94 color=color, 95 alpha=alpha, 96 label=label, 97 linewidth=linewidth, 98 edgecolor=edgecolor, 99 zorder=zorder, 100 ) 101 102 # --- set axis labels 103 if saved_pi is not None: 104 set_labels(axes, saved_pi[1], kwargs_d.get("max_ticks", get_setting("max_ticks"))) 105 106 return axes
Plot a filled region between lower and upper bounds.
Args: data: DataFrame - A two-column DataFrame with PeriodIndex. The first column is the lower bound, the second is the upper bound. kwargs: FillBetweenKwargs - keyword arguments for the plot.
Returns: Axes - matplotlib Axes object.
Raises: TypeError: If data is not a DataFrame. ValueError: If data does not have exactly two columns.
158def fill_between_plot_finalise( 159 data: DataFrame, 160 **kwargs: Unpack[FBPFKwargs], 161) -> None: 162 """Call fill_between_plot() and finalise_plot(). 163 164 Args: 165 data: DataFrame with two columns (lower bound, upper bound). 166 kwargs: Combined fill_between plot and finalise plot keyword arguments. 167 168 """ 169 validate_kwargs(schema=FBPFKwargs, caller="fill_between_plot_finalise", **kwargs) 170 kwargs = impose_legend(kwargs=kwargs, data=data) 171 plot_then_finalise( 172 data, 173 function=fill_between_plot, 174 **kwargs, 175 )
Call fill_between_plot() and finalise_plot().
Args: data: DataFrame with two columns (lower bound, upper bound). kwargs: Combined fill_between plot and finalise plot keyword arguments.
111def calc_growth(series: Series) -> DataFrame: 112 """Calculate annual and periodic growth for a pandas Series. 113 114 Args: 115 series: Series - a pandas series with a date-like PeriodIndex. 116 117 Returns: 118 DataFrame: A two column DataFrame with annual and periodic growth rates. 119 120 Raises: 121 TypeError if the series is not a pandas Series. 122 TypeError if the series index is not a PeriodIndex. 123 ValueError if the series is empty. 124 ValueError if the series index does not have a frequency of Q, M, or D. 125 ValueError if the series index has duplicates. 126 127 """ 128 # --- sanity checks 129 if not isinstance(series, Series): 130 raise TypeError("The series argument must be a pandas Series") 131 if not isinstance(series.index, PeriodIndex): 132 raise TypeError("The series index must be a pandas PeriodIndex") 133 if series.empty: 134 raise ValueError("The series argument must not be empty") 135 freq = series.index.freqstr 136 if not freq or freq[0] not in FREQUENCY_TO_PERIODS: 137 raise ValueError("The series index must have a frequency of Q, M, or D") 138 if series.index.has_duplicates: 139 raise ValueError("The series index must not have duplicate values") 140 141 # --- ensure the index is complete and the date is sorted 142 complete = period_range(start=series.index.min(), end=series.index.max()) 143 series = series.reindex(complete, fill_value=nan) 144 series = series.sort_index(ascending=True) 145 146 # --- calculate annual and periodic growth 147 freq = PeriodIndex(series.index).freqstr 148 if not freq or freq[0] not in FREQUENCY_TO_PERIODS: 149 raise ValueError("The series index must have a frequency of Q, M, or D") 150 151 freq_key = freq[0] 152 ppy = FREQUENCY_TO_PERIODS[freq_key] 153 annual = series.pct_change(periods=ppy) * 100 154 periodic = series.pct_change(periods=1) * 100 155 periodic_name = FREQUENCY_TO_NAME[freq_key] + " Growth" 156 return DataFrame( 157 { 158 "Annual Growth": annual, 159 periodic_name: periodic, 160 }, 161 )
Calculate annual and periodic growth for a pandas Series.
Args: series: Series - a pandas series with a date-like PeriodIndex.
Returns: DataFrame: A two column DataFrame with annual and periodic growth rates.
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.
146def clear_chart_dir() -> None: 147 """Remove all graph-image files from the global chart_dir.""" 148 chart_dir = get_setting("chart_dir") 149 Path(chart_dir).mkdir(parents=True, exist_ok=True) 150 for ext in IMAGE_EXTENSIONS: 151 for fs_object in Path(chart_dir).glob(f"*.{ext}"): 152 if fs_object.is_file(): 153 fs_object.unlink()
Remove all graph-image files from the global chart_dir.
103def colorise_list(party_list: Iterable[str]) -> list[str]: 104 """Return a list of party/state colors for a party_list.""" 105 return [get_color(x) for x in party_list]
Return a list of party/state colors for a party_list.
108def contrast(orig_color: str) -> str: 109 """Provide a contrasting color to any party color.""" 110 new_color = DEFAULT_CONTRAST_COLOR 111 match orig_color: 112 case "royalblue": 113 new_color = "indianred" 114 case "indianred": 115 new_color = "royalblue" 116 117 case "darkorange": 118 new_color = "mediumblue" 119 case "mediumblue": 120 new_color = "darkorange" 121 122 case "seagreen": 123 new_color = "darkblue" 124 125 case color if color == DEFAULT_UNKNOWN_COLOR: 126 new_color = "hotpink" 127 128 return new_color
Provide a contrasting color to any party color.
341def finalise_plot(axes: Axes, **kwargs: Unpack[FinaliseKwargs]) -> None: 342 """Finalise and save plots to the file system. 343 344 The filename for the saved plot is constructed from the global 345 chart_dir, the plot's title, any specified tag text, and the 346 file_type for the plot. 347 348 Args: 349 axes: Axes - matplotlib axes object - required 350 kwargs: FinaliseKwargs 351 352 """ 353 # --- check the kwargs 354 report_kwargs(caller=ME, **kwargs) 355 validate_kwargs(schema=FinaliseKwargs, caller=ME, **kwargs) 356 357 # --- sanity checks 358 if len(axes.get_children()) < 1: 359 print(f"Warning: {ME}() called with an empty axes, which was ignored.") 360 return 361 362 # --- remember axis-limits should we need to restore thems 363 xlim, ylim = axes.get_xlim(), axes.get_ylim() 364 365 # margins 366 axes.margins(DEFAULT_MARGIN) 367 axes.autoscale(tight=False) # This is problematic ... 368 369 apply_kwargs(axes, **kwargs) 370 371 # tight layout and save the figure 372 fig = axes.figure 373 if suptitle := kwargs.get("suptitle"): 374 fig.suptitle(suptitle) 375 if kwargs.get("preserve_lims"): 376 # restore the original limits of the axes 377 axes.set_xlim(xlim) 378 axes.set_ylim(ylim) 379 if not isinstance(fig, SubFigure): 380 fig.tight_layout(pad=TIGHT_LAYOUT_PAD) 381 apply_late_kwargs(axes, **kwargs) 382 legend = axes.get_legend() 383 if legend and kwargs.get("remove_legend", False): 384 legend.remove() 385 if not isinstance(fig, SubFigure): 386 save_to_file(fig, **kwargs) 387 388 # show the plot in Jupyter Lab 389 if kwargs.get("show"): 390 plt.show() 391 392 # And close 393 if not kwargs.get("dont_close", False): 394 plt.close()
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.
Args: axes: Axes - matplotlib axes object - required kwargs: FinaliseKwargs
45def get_color(s: str) -> str: 46 """Return a matplotlib color for a party label or an Australian state/territory. 47 48 Args: 49 s: str - the party label or Australian state/territory name. 50 51 Returns a color string that can be used in matplotlib plots. 52 53 """ 54 # Flattened color map for better readability 55 color_map: dict[str, str] = { 56 # --- Australian states and territories 57 "wa": "gold", 58 "western australia": "gold", 59 "sa": "red", 60 "south australia": "red", 61 "nt": "#CC7722", # ochre 62 "northern territory": "#CC7722", 63 "nsw": "deepskyblue", 64 "new south wales": "deepskyblue", 65 "act": "blue", 66 "australian capital territory": "blue", 67 "vic": "navy", 68 "victoria": "navy", 69 "tas": "seagreen", # bottle green #006A4E? 70 "tasmania": "seagreen", 71 "qld": "#c32148", # a lighter maroon 72 "queensland": "#c32148", 73 "australia": "grey", 74 "aus": "grey", 75 # --- political parties 76 "dissatisfied": "darkorange", # must be before satisfied 77 "satisfied": "mediumblue", 78 "lnp": "royalblue", 79 "l/np": "royalblue", 80 "liberal": "royalblue", 81 "liberals": "royalblue", 82 "coalition": "royalblue", 83 "dutton": "royalblue", 84 "ley": "royalblue", 85 "liberal and/or nationals": "royalblue", 86 "nat": "forestgreen", 87 "nats": "forestgreen", 88 "national": "forestgreen", 89 "nationals": "forestgreen", 90 "alp": "#dd0000", 91 "labor": "#dd0000", 92 "albanese": "#dd0000", 93 "grn": "limegreen", 94 "green": "limegreen", 95 "greens": "limegreen", 96 "other": "darkorange", 97 "oth": "darkorange", 98 } 99 100 return color_map.get(s.lower(), DEFAULT_UNKNOWN_COLOR)
Return a matplotlib color for a party label or an Australian state/territory.
Args: s: str - the party label or Australian state/territory name.
Returns a color string that can be used in matplotlib plots.
21def get_party_palette(party_text: str) -> str: 22 """Return a matplotlib color-map name based on party_text. 23 24 Works for Australian major political parties. 25 26 Args: 27 party_text: str - the party label or name. 28 29 """ 30 # Note: light to dark colormaps work best for sequential data visualization 31 match party_text.lower(): 32 case "alp" | "labor": 33 return "Reds" 34 case "l/np" | "coalition": 35 return "Blues" 36 case "grn" | "green" | "greens": 37 return "Greens" 38 case "oth" | "other": 39 return "YlOrBr" 40 case "onp" | "one nation": 41 return "YlGnBu" 42 return DEFAULT_PARTY_PALETTE
Return a matplotlib color-map name based on party_text.
Works for Australian major political parties.
Args: party_text: str - the party label or name.
102def get_setting(setting: str) -> Any: 103 """Get a setting from the global settings. 104 105 Args: 106 setting: str - name of the setting to get. 107 108 Raises: 109 KeyError: if the setting is not found 110 111 Returns: 112 value: Any - the value of the setting 113 114 """ 115 if setting not in get_fields(): 116 raise KeyError(f"Setting '{setting}' not found in mgplot_defaults.") 117 return getattr(mgplot_defaults, setting)
Get a setting from the global settings.
Args: setting: str - name of the setting to get.
Raises: KeyError: if the setting is not found
Returns: value: Any - the value of the setting
164def growth_plot( 165 data: DataT, 166 **kwargs: Unpack[GrowthKwargs], 167) -> Axes: 168 """Plot annual growth and periodic growth on the same axes. 169 170 Args: 171 data: A pandas DataFrame with two columns: 172 kwargs: GrowthKwargs 173 174 Returns: 175 axes: The matplotlib Axes object. 176 177 Raises: 178 TypeError if the data is not a 2-column DataFrame. 179 TypeError if the annual index is not a PeriodIndex. 180 ValueError if the annual and periodic series do not have the same index. 181 182 """ 183 # --- check the kwargs 184 me = "growth_plot" 185 report_kwargs(caller=me, **kwargs) 186 validate_kwargs(GrowthKwargs, caller=me, **kwargs) 187 188 # --- data checks 189 data = check_clean_timeseries(data, me) 190 if len(data.columns) != TWO_COLUMNS: 191 raise TypeError("The data argument must be a pandas DataFrame with two columns") 192 data, kwargsd = constrain_data(data, **kwargs) 193 194 # --- get the series of interest ... 195 annual = data[data.columns[0]] 196 periodic = data[data.columns[1]] 197 198 # --- series names 199 annual.name = "Annual Growth" 200 freq = PeriodIndex(periodic.index).freqstr 201 if freq and freq[0] in FREQUENCY_TO_NAME: 202 periodic.name = FREQUENCY_TO_NAME[freq[0]] + " Growth" 203 else: 204 periodic.name = "Periodic Growth" 205 206 # --- convert PeriodIndex periodic growth data to integer indexed data. 207 saved_pi = map_periodindex(periodic) 208 if saved_pi is not None: 209 periodic = saved_pi[0] # extract the reindexed DataFrame 210 211 # --- simple bar chart for the periodic growth 212 if "bar_anno_color" not in kwargsd or kwargsd["bar_anno_color"] is None: 213 kwargsd["bar_anno_color"] = "black" if kwargsd.get("above", False) else "white" 214 selected = package_kwargs(to_bar_plot, **kwargsd) 215 axes = bar_plot(periodic, **selected) 216 217 # --- and now the annual growth as a line 218 selected = package_kwargs(to_line_plot, **kwargsd) 219 line_plot(annual, ax=axes, **selected) 220 221 # --- fix the x-axis labels 222 if saved_pi is not None: 223 set_labels(axes, saved_pi[1], kwargsd.get("max_ticks", 10)) 224 225 # --- and done ... 226 return axes
Plot annual growth and periodic growth on the same axes.
Args: data: A pandas DataFrame with two columns: kwargs: GrowthKwargs
Returns: axes: The matplotlib Axes object.
Raises: TypeError if the data is not a 2-column DataFrame. TypeError if the annual index is not a PeriodIndex. ValueError if the annual and periodic series do not have the same index.
178def growth_plot_finalise(data: DataT, **kwargs: Unpack[GrowthPFKwargs]) -> None: 179 """Call growth_plot() and finalise_plot(). 180 181 Args: 182 data: The growth data to be plotted. 183 kwargs: Combined growth plot and finalise plot keyword arguments. 184 185 Note: 186 Use this when you are providing the raw growth data. Don't forget to 187 set the ylabel in kwargs. 188 189 """ 190 validate_kwargs(schema=GrowthPFKwargs, caller="growth_plot_finalise", **kwargs) 191 kwargs = impose_legend(kwargs=kwargs, force=True) 192 plot_then_finalise(data=data, function=growth_plot, **kwargs)
Call growth_plot() and finalise_plot().
Args: data: The growth data to be plotted. kwargs: Combined growth plot and finalise plot keyword arguments.
Note: Use this when you are providing the raw growth data. Don't forget to set the ylabel in kwargs.
149def line_plot(data: DataT, **kwargs: Unpack[LineKwargs]) -> Axes: 150 """Build a single or multi-line plot. 151 152 Args: 153 data: DataFrame | Series - data to plot 154 kwargs: LineKwargs - keyword arguments for the line plot 155 156 Returns: 157 - axes: Axes - the axes object for the plot 158 159 """ 160 # --- check the kwargs 161 report_kwargs(caller=ME, **kwargs) 162 validate_kwargs(schema=LineKwargs, caller=ME, **kwargs) 163 164 # --- check the data 165 data = check_clean_timeseries(data, ME) 166 df = DataFrame(data) # we are only plotting DataFrames 167 df, kwargs_d = constrain_data(df, **kwargs) 168 169 # --- convert PeriodIndex to Integer Index 170 saved_pi = map_periodindex(df) 171 if saved_pi is not None: 172 df = saved_pi[0] 173 174 if isinstance(df.index, PeriodIndex): 175 print("Internal error: data is still a PeriodIndex - come back here and fix it") 176 177 # --- Let's plot 178 axes, kwargs_d = get_axes(**kwargs_d) # get the axes to plot on 179 if df.empty or df.isna().all().all(): 180 # Note: finalise plot should ignore an empty axes object 181 print(f"Warning: No data to plot in {ME}().") 182 return axes 183 184 # --- get the arguments for each line we will plot ... 185 item_count = len(df.columns) 186 num_data_points = len(df) 187 swce, kwargs_d = get_style_width_color_etc(item_count, num_data_points, **kwargs_d) 188 189 for i, column in enumerate(df.columns): 190 series = df[column] 191 series = series.dropna() if "dropna" in swce and swce["dropna"][i] else series 192 if series.empty or series.isna().all(): 193 print(f"Warning: No data to plot for {column} in line_plot().") 194 continue 195 196 axes.plot( 197 # using matplotlib, as pandas can set xlabel/ylabel 198 series.index, # x 199 series, # y 200 ls=swce["style"][i], 201 lw=swce["width"][i], 202 color=swce["color"][i], 203 alpha=swce["alpha"][i], 204 marker=swce["marker"][i], 205 ms=swce["markersize"][i], 206 drawstyle=swce["drawstyle"][i], 207 zorder=swce["zorder"][i], 208 label=(column if "label_series" in swce and swce["label_series"][i] else f"_{column}_"), 209 ) 210 211 if swce["annotate"][i] is None or not swce["annotate"][i]: 212 continue 213 214 color = swce["color"][i] if swce["annotate_color"][i] is True else swce["annotate_color"][i] 215 annotate_series( 216 series, 217 axes, 218 color=color, 219 rounding=swce["rounding"][i], 220 fontsize=swce["fontsize"][i], 221 fontname=swce["fontname"][i], 222 rotation=swce["rotation"][i], 223 ) 224 225 # --- set the labels 226 if saved_pi is not None: 227 set_labels(axes, saved_pi[1], kwargs_d.get("max_ticks", get_setting("max_ticks"))) 228 229 return axes
Build a single or multi-line plot.
Args: data: DataFrame | Series - data to plot kwargs: LineKwargs - keyword arguments for the line plot
Returns:
- axes: Axes - the axes object for the plot
195def line_plot_finalise( 196 data: DataT, 197 **kwargs: Unpack[LPFKwargs], 198) -> None: 199 """Call line_plot() then finalise_plot(). 200 201 Args: 202 data: The data to be plotted. 203 kwargs: Combined line plot and finalise plot keyword arguments. 204 205 """ 206 validate_kwargs(schema=LPFKwargs, caller="line_plot_finalise", **kwargs) 207 kwargs = impose_legend(kwargs=kwargs, data=data) 208 plot_then_finalise(data, function=line_plot, **kwargs)
Call line_plot() then finalise_plot().
Args: data: The data to be plotted. kwargs: Combined line plot and finalise plot keyword arguments.
273def multi_column( 274 data: DataFrame, 275 function: Callable | list[Callable], 276 **kwargs: Any, 277) -> None: 278 """Create multiple plots, one for each column in a DataFrame. 279 280 Args: 281 data: DataFrame - The data to be plotted. 282 function: Callable | list[Callable] - The plotting function(s) to be used. 283 kwargs: Any - Additional keyword arguments passed to plotting functions. 284 285 Returns: 286 None 287 288 Raises: 289 TypeError: If data is not a DataFrame. 290 ValueError: If DataFrame is empty or has no columns. 291 292 Note: 293 The plot title will be kwargs["title"] plus the column name. 294 295 """ 296 # --- sanity checks 297 me = "multi_column" 298 report_kwargs(caller=me, **kwargs) 299 if not isinstance(data, DataFrame): 300 raise TypeError("data must be a pandas DataFrame for multi_column()") 301 if data.empty: 302 raise ValueError("DataFrame cannot be empty") 303 if len(data.columns) == 0: 304 raise ValueError("DataFrame must have at least one column") 305 306 # --- check the function argument 307 title_stem = kwargs.get("title", "") 308 tag: Final[str] = kwargs.get("tag", "") 309 first, kwargs["function"] = first_unchain(function) 310 if not kwargs["function"]: 311 del kwargs["function"] # remove the function key if it is empty 312 313 # --- iterate over the columns 314 for i, col in enumerate(data.columns): 315 series = data[col] # Extract as Series, not single-column DataFrame 316 kwargs["title"] = f"{title_stem}{col}" if title_stem else str(col) 317 kwargs["tag"] = _generate_tag(tag, i) 318 first(series, **kwargs)
Create multiple plots, one for each column in a DataFrame.
Args: data: DataFrame - The data to be plotted. function: Callable | list[Callable] - The plotting function(s) to be used. kwargs: Any - Additional keyword arguments passed to plotting functions.
Returns: None
Raises: TypeError: If data is not a DataFrame. ValueError: If DataFrame is empty or has no columns.
Note: The plot title will be kwargs["title"] plus the column name.
215def multi_start( 216 data: DataT, 217 function: Callable | list[Callable], 218 starts: Iterable[None | Period | int], 219 **kwargs: Any, 220) -> None: 221 """Create multiple plots with different starting points. 222 223 Args: 224 data: Series | DataFrame - The data to be plotted. 225 function: Callable | list[Callable] - desired plotting function(s). 226 starts: Iterable[Period | int | None] - The starting points for each plot. 227 kwargs: Any - Additional keyword arguments passed to plotting functions. 228 229 Returns: 230 None 231 232 Raises: 233 TypeError: If starts is not an iterable of None, Period or int. 234 ValueError: If starts contains invalid values or is empty. 235 236 Note: 237 kwargs['tag'] is used to create a unique tag for each plot. 238 239 """ 240 # --- sanity checks 241 me = "multi_start" 242 report_kwargs(caller=me, **kwargs) 243 if not isinstance(starts, Iterable): 244 raise TypeError("starts must be an iterable of None, Period or int") 245 246 # Convert to list to validate contents and check if empty 247 starts_list = list(starts) 248 if not starts_list: 249 raise ValueError("starts cannot be empty") 250 251 # Validate each start value 252 for i, start in enumerate(starts_list): 253 if start is not None and not isinstance(start, (Period, int)): 254 raise TypeError( 255 f"Start value at index {i} must be None, Period, or int, got {type(start).__name__}" 256 ) 257 258 # --- check the function argument 259 original_tag: Final[str] = kwargs.get("tag", "") 260 first, kwargs["function"] = first_unchain(function) 261 if not kwargs["function"]: 262 del kwargs["function"] # remove the function key if it is empty 263 264 # --- iterate over the starts 265 for i, start in enumerate(starts_list): 266 kw = kwargs.copy() # copy to avoid modifying the original kwargs 267 this_tag = _generate_tag(original_tag, i) 268 kw["tag"] = this_tag 269 kw["plot_from"] = start # rely on plotting function to constrain the data 270 first(data, **kw)
Create multiple plots with different starting points.
Args: data: Series | DataFrame - The data to be plotted. function: Callable | list[Callable] - desired plotting function(s). starts: Iterable[Period | int | None] - The starting points for each plot. kwargs: Any - Additional keyword arguments passed to plotting functions.
Returns: None
Raises: TypeError: If starts is not an iterable of None, Period or int. ValueError: If starts contains invalid values or is empty.
Note: kwargs['tag'] is used to create a unique tag for each plot.
152def plot_then_finalise( 153 data: DataT, 154 function: Callable | list[Callable], 155 **kwargs: Any, 156) -> None: 157 """Chain a plotting function with the finalise_plot() function. 158 159 Args: 160 data: Series | DataFrame - The data to be plotted. 161 function: Callable | list[Callable] - the desired plotting function(s). 162 kwargs: Any - Additional keyword arguments. 163 164 Returns None. 165 166 """ 167 # --- checks 168 me = "plot_then_finalise" 169 report_kwargs(caller=me, **kwargs) 170 # validate once we have established the first function 171 172 # data is not checked here, assume it is checked by the called 173 # plot function. 174 175 first, kwargs["function"] = first_unchain(function) 176 if not kwargs["function"]: 177 del kwargs["function"] # remove the function key if it is empty 178 179 # Check that forbidden functions are not called first 180 if hasattr(first, "__name__") and first.__name__ in FORBIDDEN_FIRST_FUNCTIONS: 181 raise ValueError( 182 f"Function '{first.__name__}' should not be called by {me}. Call it before calling {me}." 183 ) 184 185 if first in EXPECTED_CALLABLES: 186 expected = EXPECTED_CALLABLES[first] 187 plot_kwargs = limit_kwargs(expected, **kwargs) 188 else: 189 # this is an unexpected Callable, so we will give it a try 190 print(f"Unknown proposed function: {first}; nonetheless, will give it a try.") 191 expected = BaseKwargs 192 plot_kwargs = kwargs.copy() 193 194 # --- validate the original kwargs (could not do before now) 195 kw_types = ( 196 # combine the expected kwargs types with the finalise kwargs types 197 dict(cast("dict[str, Any]", expected.__annotations__)) 198 | dict(cast("dict[str, Any]", FinaliseKwargs.__annotations__)) 199 ) 200 validate_kwargs(schema=kw_types, caller=me, **kwargs) 201 202 # --- call the first function with the data and selected plot kwargs 203 axes = first(data, **plot_kwargs) 204 205 # --- prepare finalise kwargs (remove overlapping arguments) 206 fp_kwargs = limit_kwargs(FinaliseKwargs, **kwargs) 207 # Remove any arguments that were already used in the plot function 208 used_plot_args = set(plot_kwargs.keys()) 209 fp_kwargs = {k: v for k, v in fp_kwargs.items() if k not in used_plot_args} 210 211 # --- finalise the plot 212 finalise_plot(axes, **fp_kwargs)
Chain a plotting function with the finalise_plot() function.
Args: data: Series | DataFrame - The data to be plotted. function: Callable | list[Callable] - the desired plotting function(s). kwargs: Any - Additional keyword arguments.
Returns None.
132def postcovid_plot(data: DataT, **kwargs: Unpack[PostcovidKwargs]) -> Axes: 133 """Plot a series with a PeriodIndex, including a post-COVID projection. 134 135 Args: 136 data: Series - the series to be plotted. 137 kwargs: PostcovidKwargs - plotting arguments. 138 139 Raises: 140 TypeError if series is not a pandas Series 141 TypeError if series does not have a PeriodIndex 142 ValueError if series does not have a D, M or Q frequency 143 ValueError if regression start is after regression end 144 145 """ 146 147 # --- failure 148 def failure() -> Axes: 149 print("postcovid_plot(): plotting the raw data only.") 150 remove: list[Literal["plot_from", "start_r", "end_r"]] = ["plot_from", "start_r", "end_r"] 151 for key in remove: 152 kwargs.pop(key, None) 153 return line_plot( 154 data, 155 **cast("LineKwargs", kwargs), 156 ) 157 158 # --- check the kwargs 159 report_kwargs(caller=ME, **kwargs) 160 validate_kwargs(schema=PostcovidKwargs, caller=ME, **kwargs) 161 162 # --- check the data 163 data = check_clean_timeseries(data, ME) 164 if not isinstance(data, Series): 165 raise TypeError("The series argument must be a pandas Series") 166 167 # --- rely on line_plot() to validate kwargs, but remove any that are not relevant 168 if "plot_from" in kwargs: 169 print("Warning: the 'plot_from' argument is ignored in postcovid_plot().") 170 kwargs.pop("plot_from", None) 171 172 # --- set the regression period 173 start_r, end_r, robust = regression_period(data, **kwargs) 174 kwargs.pop("start_r", None) # remove from kwargs to avoid confusion 175 kwargs.pop("end_r", None) # remove from kwargs to avoid confusion 176 if not robust: 177 return failure() 178 179 # --- combine data and projection 180 if start_r < data.dropna().index.min(): 181 print(f"Caution: Regression start period pre-dates the series index: {start_r=}") 182 recent_data = data[data.index >= start_r].copy() 183 recent_data.name = "Series" 184 projection_data = get_projection(recent_data, end_r) 185 if projection_data.empty: 186 return failure() 187 projection_data.name = "Pre-COVID projection" 188 189 # --- Create DataFrame with proper column alignment 190 combined_data = DataFrame( 191 { 192 projection_data.name: projection_data, 193 recent_data.name: recent_data, 194 } 195 ) 196 197 # --- activate plot settings 198 kwargs["width"] = kwargs.pop( 199 "width", 200 (get_setting("line_normal"), get_setting("line_wide")), 201 ) # series line is thicker than projection 202 kwargs["style"] = kwargs.pop("style", ("--", "-")) # dashed regression line 203 kwargs["label_series"] = kwargs.pop("label_series", True) 204 kwargs["annotate"] = kwargs.pop("annotate", (False, True)) # annotate series only 205 kwargs["color"] = kwargs.pop("color", ("darkblue", "#dd0000")) 206 kwargs["dropna"] = kwargs.pop("dropna", False) # drop NaN values 207 208 return line_plot( 209 combined_data, 210 **cast("LineKwargs", kwargs), 211 )
Plot a series with a PeriodIndex, including a post-COVID projection.
Args: data: Series - the series to be plotted. kwargs: PostcovidKwargs - plotting arguments.
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
211def postcovid_plot_finalise( 212 data: DataT, 213 **kwargs: Unpack[PCFKwargs], 214) -> None: 215 """Call postcovid_plot() and finalise_plot(). 216 217 Args: 218 data: The data to be plotted. 219 kwargs: Combined postcovid plot and finalise plot keyword arguments. 220 221 """ 222 validate_kwargs(schema=PCFKwargs, caller="postcovid_plot_finalise", **kwargs) 223 kwargs = impose_legend(kwargs=kwargs, force=True) 224 plot_then_finalise(data, function=postcovid_plot, **kwargs)
Call postcovid_plot() and finalise_plot().
Args: data: The data to be plotted. kwargs: Combined postcovid plot and finalise plot keyword arguments.
21def revision_plot(data: DataT, **kwargs: Unpack[LineKwargs]) -> Axes: 22 """Plot the revisions to ABS data. 23 24 Args: 25 data: DataFrame - the data to plot, with a column for each data revision. 26 Must have at least 2 columns to show meaningful revision comparisons. 27 kwargs: LineKwargs - additional keyword arguments for the line_plot function. 28 29 Returns: 30 Axes: A matplotlib Axes object containing the revision plot. 31 32 Raises: 33 TypeError: If data is not a DataFrame. 34 ValueError: If DataFrame has fewer than 2 columns for revision comparison. 35 36 """ 37 # --- check the kwargs and data 38 report_kwargs(caller=ME, **kwargs) 39 validate_kwargs(schema=LineKwargs, caller=ME, **kwargs) 40 data = check_clean_timeseries(data, ME) 41 42 # --- additional checks 43 if not isinstance(data, DataFrame): 44 print(f"{ME}() requires a DataFrame with columns for each revision, not a Series or any other type.") 45 raise TypeError(f"{ME}() requires a DataFrame, got {type(data).__name__}") 46 47 if data.shape[1] < MIN_REVISION_COLUMNS: 48 raise ValueError( 49 f"{ME}() requires at least {MIN_REVISION_COLUMNS} columns for revision comparison, " 50 f"but got {data.shape[1]} columns" 51 ) 52 53 # --- set defaults for revision visualization 54 kwargs["plot_from"] = kwargs.get("plot_from", DEFAULT_PLOT_FROM) 55 kwargs["annotate"] = kwargs.get("annotate", True) 56 kwargs["annotate_color"] = kwargs.get("annotate_color", "black") 57 kwargs["rounding"] = kwargs.get("rounding", 3) 58 59 # --- plot 60 return line_plot(data, **kwargs)
Plot the revisions to ABS data.
Args: data: DataFrame - the data to plot, with a column for each data revision. Must have at least 2 columns to show meaningful revision comparisons. kwargs: LineKwargs - additional keyword arguments for the line_plot function.
Returns: Axes: A matplotlib Axes object containing the revision plot.
Raises: TypeError: If data is not a DataFrame. ValueError: If DataFrame has fewer than 2 columns for revision comparison.
227def revision_plot_finalise( 228 data: DataT, 229 **kwargs: Unpack[RevPFKwargs], 230) -> None: 231 """Call revision_plot() and finalise_plot(). 232 233 Args: 234 data: The revision data to be plotted. 235 kwargs: Combined revision plot and finalise plot keyword arguments. 236 237 """ 238 validate_kwargs(schema=RevPFKwargs, caller="revision_plot_finalise", **kwargs) 239 kwargs = impose_legend(kwargs=kwargs, force=True) 240 plot_then_finalise(data=data, function=revision_plot, **kwargs)
Call revision_plot() and finalise_plot().
Args: data: The revision data to be plotted. kwargs: Combined revision plot and finalise plot keyword arguments.
161def run_plot(data: DataT, **kwargs: Unpack[RunKwargs]) -> Axes: 162 """Plot a series of percentage rates, highlighting the increasing runs. 163 164 Arguments: 165 data: Series - ordered pandas Series of percentages, with PeriodIndex. 166 kwargs: RunKwargs - keyword arguments for the run_plot function. 167 168 Return: 169 - matplotlib Axes object 170 171 """ 172 # --- validate inputs 173 report_kwargs(caller=ME, **kwargs) 174 validate_kwargs(schema=RunKwargs, caller=ME, **kwargs) 175 176 series = check_clean_timeseries(data, ME) 177 if not isinstance(series, Series): 178 raise TypeError("series must be a pandas Series for run_plot()") 179 series, kwargs_d = constrain_data(series, **kwargs) 180 181 # --- configure defaults and validate 182 direction = kwargs_d.get("direction", "both") 183 _configure_defaults(kwargs_d, direction) 184 185 threshold = kwargs_d["threshold"] 186 if threshold <= 0: 187 raise ValueError("Threshold must be positive") 188 189 # --- handle PeriodIndex conversion 190 saved_pi = map_periodindex(series) 191 if saved_pi is not None: 192 series = saved_pi[0] 193 194 # --- plot the line 195 lp_kwargs = limit_kwargs(LineKwargs, **kwargs_d) 196 axes = line_plot(series, **lp_kwargs) 197 198 # --- plot runs based on direction 199 run_label = kwargs_d.pop("highlight_label", None) 200 up_label, down_label = _resolve_labels(run_label, direction) 201 202 if direction in ("up", "both"): 203 _plot_runs(axes, series, run_label=up_label, up=True, **kwargs_d) 204 if direction in ("down", "both"): 205 _plot_runs(axes, series, run_label=down_label, up=False, **kwargs_d) 206 207 if direction not in ("up", "down", "both"): 208 raise ValueError(f"Invalid direction: {direction}. Expected 'up', 'down', or 'both'.") 209 210 # --- set axis labels 211 if saved_pi is not None: 212 set_labels(axes, saved_pi[1], kwargs.get("max_ticks", get_setting("max_ticks"))) 213 214 return axes
Plot a series of percentage rates, highlighting the increasing runs.
Arguments: data: Series - ordered pandas Series of percentages, with PeriodIndex. kwargs: RunKwargs - keyword arguments for the run_plot function.
Return:
- matplotlib Axes object
243def run_plot_finalise( 244 data: DataT, 245 **kwargs: Unpack[RunPFKwargs], 246) -> None: 247 """Call run_plot() and finalise_plot(). 248 249 Args: 250 data: The data to be plotted. 251 kwargs: Combined run plot and finalise plot keyword arguments. 252 253 """ 254 validate_kwargs(schema=RunPFKwargs, caller="run_plot_finalise", **kwargs) 255 kwargs = impose_legend(kwargs=kwargs, force="highlight_label" in kwargs) 256 plot_then_finalise(data=data, function=run_plot, **kwargs)
Call run_plot() and finalise_plot().
Args: data: The data to be plotted. kwargs: Combined run plot and finalise plot keyword arguments.
19def seastrend_plot(data: DataT, **kwargs: Unpack[LineKwargs]) -> Axes: 20 """Produce a seasonal+trend plot. 21 22 Arguments: 23 data: DataFrame - the data to plot. Must have exactly 2 columns: 24 Seasonal data in column 0, Trend data in column 1 25 kwargs: LineKwargs - additional keyword arguments to pass to line_plot() 26 27 Returns: 28 Axes: A matplotlib Axes object containing the seasonal+trend plot 29 30 Raises: 31 ValueError: If the DataFrame does not have exactly 2 columns 32 33 """ 34 # --- check the kwargs 35 report_kwargs(caller=ME, **kwargs) 36 validate_kwargs(schema=LineKwargs, caller=ME, **kwargs) 37 38 # --- check the data 39 data = check_clean_timeseries(data, ME) 40 if data.shape[1] != REQUIRED_COLUMNS: 41 raise ValueError( 42 f"{ME}() expects a DataFrame with exactly {REQUIRED_COLUMNS} columns " 43 f"(seasonal and trend), but got {data.shape[1]} columns." 44 ) 45 46 # --- set defaults for seasonal+trend visualization 47 kwargs["color"] = kwargs.get("color", get_color_list(REQUIRED_COLUMNS)) 48 kwargs["width"] = kwargs.get("width", [get_setting("line_normal"), get_setting("line_wide")]) 49 kwargs["style"] = kwargs.get("style", ["-", "-"]) 50 kwargs["annotate"] = kwargs.get("annotate", [True, False]) # annotate seasonal, not trend 51 kwargs["rounding"] = kwargs.get("rounding", True) 52 kwargs["dropna"] = kwargs.get("dropna", False) # series breaks are common in seas-trend data 53 54 return line_plot( 55 data, 56 **kwargs, 57 )
Produce a seasonal+trend plot.
Arguments: data: DataFrame - the data to plot. Must have exactly 2 columns: Seasonal data in column 0, Trend data in column 1 kwargs: LineKwargs - additional keyword arguments to pass to line_plot()
Returns: Axes: A matplotlib Axes object containing the seasonal+trend plot
Raises: ValueError: If the DataFrame does not have exactly 2 columns
259def seastrend_plot_finalise( 260 data: DataT, 261 **kwargs: Unpack[SFKwargs], 262) -> None: 263 """Call seastrend_plot() and finalise_plot(). 264 265 Args: 266 data: The seasonal and trend data to be plotted. 267 kwargs: Combined seastrend plot and finalise plot keyword arguments. 268 269 """ 270 validate_kwargs(schema=SFKwargs, caller="seastrend_plot_finalise", **kwargs) 271 kwargs = impose_legend(kwargs=kwargs, force=True) 272 plot_then_finalise(data, function=seastrend_plot, **kwargs)
Call seastrend_plot() and finalise_plot().
Args: data: The seasonal and trend data to be plotted. kwargs: Combined seastrend plot and finalise plot keyword arguments.
229def series_growth_plot( 230 data: DataT, 231 **kwargs: Unpack[SeriesGrowthKwargs], 232) -> Axes: 233 """Plot annual and periodic growth in percentage terms from a pandas Series. 234 235 Args: 236 data: A pandas Series with an appropriate PeriodIndex. 237 kwargs: SeriesGrowthKwargs 238 239 """ 240 # --- check the kwargs 241 me = "series_growth_plot" 242 report_kwargs(caller=me, **kwargs) 243 validate_kwargs(SeriesGrowthKwargs, caller=me, **kwargs) 244 245 # --- sanity checks 246 if not isinstance(data, Series): 247 raise TypeError("The data argument to series_growth_plot() must be a pandas Series") 248 249 # --- calculate growth and plot - add ylabel 250 ylabel: str | None = kwargs.pop("ylabel", None) 251 if ylabel is not None: 252 print(f"Did you intend to specify a value for the 'ylabel' in {me}()?") 253 ylabel = "Growth (%)" if ylabel is None else ylabel 254 growth = calc_growth(data) 255 ax = growth_plot(growth, **cast("GrowthKwargs", kwargs)) 256 ax.set_ylabel(ylabel) 257 return ax
Plot annual and periodic growth in percentage terms from a pandas Series.
Args: data: A pandas Series with an appropriate PeriodIndex. kwargs: SeriesGrowthKwargs
275def series_growth_plot_finalise(data: DataT, **kwargs: Unpack[SGFPKwargs]) -> None: 276 """Call series_growth_plot() and finalise_plot(). 277 278 Args: 279 data: The series data to calculate and plot growth for. 280 kwargs: Combined series growth plot and finalise plot keyword arguments. 281 282 """ 283 validate_kwargs(schema=SGFPKwargs, caller="series_growth_plot_finalise", **kwargs) 284 kwargs = impose_legend(kwargs=kwargs, force=True) 285 plot_then_finalise(data=data, function=series_growth_plot, **kwargs)
Call series_growth_plot() and finalise_plot().
Args: data: The series data to calculate and plot growth for. kwargs: Combined series growth plot and finalise plot keyword arguments.
156def set_chart_dir(chart_dir: str) -> None: 157 """Set a global chart directory for finalise_plot(). 158 159 Args: 160 chart_dir: str - the directory to set as the chart directory 161 162 Note: Path.mkdir() may raise an exception if a directory cannot be created. 163 164 Note: This is a wrapper for set_setting() to set the chart_dir setting, and 165 create the directory if it does not exist. 166 167 """ 168 if not chart_dir or chart_dir.isspace(): 169 chart_dir = DEFAULT_CHART_DIR # avoid empty/whitespace strings 170 Path(chart_dir).mkdir(parents=True, exist_ok=True) 171 set_setting("chart_dir", chart_dir)
Set a global chart directory for finalise_plot().
Args: chart_dir: str - the directory to set as the chart directory
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.
120def set_setting(setting: str, value: Any) -> None: 121 """Set a setting in the global settings. 122 123 Args: 124 setting: str - name of the setting to set (see get_setting()) 125 value: Any - the value to set the setting to 126 127 Raises: 128 KeyError: if the setting is not found 129 ValueError: if the value is invalid for the setting 130 131 """ 132 if setting not in get_fields(): 133 raise KeyError(f"Setting '{setting}' not found in mgplot_defaults.") 134 135 # Basic validation for some settings 136 if setting == "chart_dir" and not isinstance(value, str): 137 raise ValueError(f"chart_dir must be a string, got {type(value)}") 138 if setting == "dpi" and (not isinstance(value, int) or value <= 0): 139 raise ValueError(f"dpi must be a positive integer, got {value}") 140 if setting == "max_ticks" and (not isinstance(value, int) or value <= 0): 141 raise ValueError(f"max_ticks must be a positive integer, got {value}") 142 143 setattr(mgplot_defaults, setting, value)
Set a setting in the global settings.
Args: setting: str - name of the setting to set (see get_setting()) value: Any - the value to set the setting to
Raises: KeyError: if the setting is not found ValueError: if the value is invalid for the setting
294def summary_plot(data: DataT, **kwargs: Unpack[SummaryKwargs]) -> Axes: 295 """Plot a summary of historical data for a given DataFrame. 296 297 Args: 298 data: DataFrame containing the summary data. The column names are 299 used as labels for the plot. 300 kwargs: Additional arguments for the plot, including middle (float), 301 plot_type (str), verbose (bool), and standard plotting options. 302 303 Returns: 304 Axes: A matplotlib Axes object containing the summary plot. 305 306 Raises: 307 TypeError: If data is not a DataFrame. 308 309 """ 310 # --- check the kwargs 311 report_kwargs(caller=ME, **kwargs) 312 validate_kwargs(schema=SummaryKwargs, caller=ME, **kwargs) 313 314 # --- check the data 315 data = check_clean_timeseries(data, ME) 316 if not isinstance(data, DataFrame): 317 raise TypeError("data must be a pandas DataFrame for summary_plot()") 318 319 # --- legend 320 kwargs["legend"] = kwargs.get( 321 "legend", 322 { 323 # put the legend below the x-axis label 324 "loc": "upper center", 325 "fontsize": "xx-small", 326 "bbox_to_anchor": (0.5, -0.125), 327 "ncol": 4, 328 }, 329 ) 330 331 # --- and plot it ... 332 ax, plot_type = plot_the_data(data, **kwargs) 333 label_x_axis( 334 kwargs.get("plot_from", DEFAULT_PLOT_FROM), 335 label=kwargs.get("xlabel", ""), 336 plot_type=plot_type, 337 ax=ax, 338 df=data, 339 ) 340 mark_reference_lines(plot_type, ax) 341 342 return ax
Plot a summary of historical data for a given DataFrame.
Args: data: DataFrame containing the summary data. The column names are used as labels for the plot. kwargs: Additional arguments for the plot, including middle (float), plot_type (str), verbose (bool), and standard plotting options.
Returns: Axes: A matplotlib Axes object containing the summary plot.
Raises: TypeError: If data is not a DataFrame.
288def summary_plot_finalise( 289 data: DataT, 290 **kwargs: Unpack[SumPFKwargs], 291) -> None: 292 """Call summary_plot() and finalise_plot(). 293 294 This is more complex than most of the above convenience methods as it 295 creates multiple plots (one for each plot type). 296 297 Args: 298 data: DataFrame containing the summary data. The index must be a PeriodIndex. 299 kwargs: Combined summary plot and finalise plot keyword arguments. 300 301 Raises: 302 TypeError: If data is not a DataFrame with a PeriodIndex. 303 IndexError: If DataFrame is empty. 304 305 """ 306 # --- validate data type and structure 307 if not isinstance(data, DataFrame) or not isinstance(data.index, PeriodIndex): 308 raise TypeError("Data must be a DataFrame with a PeriodIndex.") 309 310 if data.empty or len(data.index) == 0: 311 raise ValueError("DataFrame cannot be empty") 312 313 validate_kwargs(schema=SumPFKwargs, caller="summary_plot_finalise", **kwargs) 314 315 # --- set default title with bounds checking 316 kwargs["title"] = kwargs.get("title", f"Summary at {label_period(data.index[-1])}") 317 kwargs["preserve_lims"] = kwargs.get("preserve_lims", True) 318 319 # --- handle plot_from parameter with bounds checking 320 start: int | Period | None = kwargs.get("plot_from", 0) 321 if start is None: 322 start = data.index[0] 323 elif isinstance(start, int): 324 if abs(start) >= len(data.index): 325 raise IndexError( 326 f"plot_from index {start} out of range for DataFrame with {len(data.index)} rows" 327 ) 328 start = data.index[start] 329 330 kwargs["plot_from"] = start 331 if not isinstance(start, Period): 332 raise TypeError("plot_from must be a Period or convertible to one") 333 334 # --- create plots for each plot type 335 pre_tag: str = kwargs.get("pre_tag", "") 336 for plot_type in SUMMARY_PLOT_TYPES: 337 plot_kwargs = kwargs.copy() # Avoid modifying original kwargs 338 plot_kwargs["plot_type"] = plot_type 339 plot_kwargs["pre_tag"] = pre_tag + plot_type 340 341 plot_then_finalise( 342 data, 343 function=summary_plot, 344 **plot_kwargs, 345 )
Call summary_plot() and finalise_plot().
This is more complex than most of the above convenience methods as it creates multiple plots (one for each plot type).
Args: data: DataFrame containing the summary data. The index must be a PeriodIndex. kwargs: Combined summary plot and finalise plot keyword arguments.
Raises: TypeError: If data is not a DataFrame with a PeriodIndex. IndexError: If DataFrame is empty.