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.finalise_plot import FinaliseKwargs, finalise_plot 24from mgplot.finalisers import ( 25 bar_plot_finalise, 26 growth_plot_finalise, 27 line_plot_finalise, 28 postcovid_plot_finalise, 29 revision_plot_finalise, 30 run_plot_finalise, 31 seastrend_plot_finalise, 32 series_growth_plot_finalise, 33 summary_plot_finalise, 34) 35from mgplot.growth_plot import ( 36 GrowthKwargs, 37 SeriesGrowthKwargs, 38 calc_growth, 39 growth_plot, 40 series_growth_plot, 41) 42from mgplot.line_plot import LineKwargs, line_plot 43from mgplot.multi_plot import multi_column, multi_start, plot_then_finalise 44from mgplot.postcovid_plot import PostcovidKwargs, postcovid_plot 45from mgplot.revision_plot import revision_plot 46from mgplot.run_plot import RunKwargs, run_plot 47from mgplot.seastrend_plot import seastrend_plot 48from mgplot.settings import ( 49 clear_chart_dir, 50 get_setting, 51 set_chart_dir, 52 set_setting, 53) 54from mgplot.summary_plot import SummaryKwargs, summary_plot 55 56# --- version and author 57try: 58 __version__ = importlib.metadata.version(__name__) 59except importlib.metadata.PackageNotFoundError: 60 __version__ = "0.0.0" # Fallback for development mode 61__author__ = "Bryan Palmer" 62 63 64# --- public API 65__all__ = ( 66 "BarKwargs", 67 "FinaliseKwargs", 68 "GrowthKwargs", 69 "LineKwargs", 70 "PostcovidKwargs", 71 "RunKwargs", 72 "SeriesGrowthKwargs", 73 "SummaryKwargs", 74 "__author__", 75 "__version__", 76 "abbreviate_state", 77 "bar_plot", 78 "bar_plot_finalise", 79 "calc_growth", 80 "clear_chart_dir", 81 "colorise_list", 82 "contrast", 83 "finalise_plot", 84 "get_color", 85 "get_party_palette", 86 "get_setting", 87 "growth_plot", 88 "growth_plot_finalise", 89 "line_plot", 90 "line_plot_finalise", 91 "multi_column", 92 "multi_start", 93 "plot_then_finalise", 94 "postcovid_plot", 95 "postcovid_plot_finalise", 96 "revision_plot", 97 "revision_plot_finalise", 98 "run_plot", 99 "run_plot", 100 "run_plot_finalise", 101 "seastrend_plot", 102 "seastrend_plot_finalise", 103 "series_growth_plot", 104 "series_growth_plot_finalise", 105 "set_chart_dir", 106 "set_setting", 107 "state_abbrs", 108 "state_names", 109 "summary_plot", 110 "summary_plot_finalise", 111)
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 # --- options for each bar ... 50 color: NotRequired[str | Sequence[str]] 51 label_series: NotRequired[bool | Sequence[bool]] 52 width: NotRequired[float | int | Sequence[float | int]] 53 # --- options for bar annotations 54 annotate: NotRequired[bool] 55 fontsize: NotRequired[int | float | str] 56 fontname: NotRequired[str] 57 rounding: NotRequired[int] 58 rotation: NotRequired[int | float] 59 annotate_color: NotRequired[str] 60 above: NotRequired[bool]
Keyword arguments for the bar_plot function.
31class FinaliseKwargs(BaseKwargs): 32 """Keyword arguments for the finalise_plot function.""" 33 34 # --- value options 35 title: NotRequired[str | None] 36 xlabel: NotRequired[str | None] 37 ylabel: NotRequired[str | None] 38 xlim: NotRequired[tuple[float, float] | None] 39 ylim: NotRequired[tuple[float, float] | None] 40 xticks: NotRequired[list[float] | None] 41 yticks: NotRequired[list[float] | None] 42 xscale: NotRequired[str | None] 43 yscale: NotRequired[str | None] 44 # --- splat options 45 legend: NotRequired[bool | dict[str, Any] | None] 46 axhspan: NotRequired[dict[str, Any]] 47 axvspan: NotRequired[dict[str, Any]] 48 axhline: NotRequired[dict[str, Any]] 49 axvline: NotRequired[dict[str, Any]] 50 # --- options for annotations 51 lfooter: NotRequired[str] 52 rfooter: NotRequired[str] 53 lheader: NotRequired[str] 54 rheader: NotRequired[str] 55 # --- file/save options 56 pre_tag: NotRequired[str] 57 tag: NotRequired[str] 58 chart_dir: NotRequired[str] 59 file_type: NotRequired[str] 60 dpi: NotRequired[int] 61 figsize: NotRequired[tuple[float, float]] 62 show: NotRequired[bool] 63 # --- other options 64 preserve_lims: NotRequired[bool] 65 remove_legend: NotRequired[bool] 66 zero_y: NotRequired[bool] 67 y0: NotRequired[bool] 68 x0: NotRequired[bool] 69 dont_save: NotRequired[bool] 70 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.
27class LineKwargs(BaseKwargs): 28 """Keyword arguments for the line_plot function.""" 29 30 # --- options for the entire line plot 31 ax: NotRequired[Axes | None] 32 style: NotRequired[str | Sequence[str]] 33 width: NotRequired[float | int | Sequence[float | int]] 34 color: NotRequired[str | Sequence[str]] 35 alpha: NotRequired[float | Sequence[float]] 36 drawstyle: NotRequired[str | Sequence[str] | None] 37 marker: NotRequired[str | Sequence[str] | None] 38 markersize: NotRequired[float | Sequence[float] | int | None] 39 dropna: NotRequired[bool | Sequence[bool]] 40 annotate: NotRequired[bool | Sequence[bool]] 41 rounding: NotRequired[Sequence[int | bool] | int | bool | None] 42 fontsize: NotRequired[Sequence[str | int | float] | str | int | float] 43 fontname: NotRequired[str | Sequence[str]] 44 rotation: NotRequired[Sequence[int | float] | int | float] 45 annotate_color: NotRequired[str | Sequence[str] | bool | Sequence[bool] | None] 46 plot_from: NotRequired[int | Period | None] 47 label_series: NotRequired[bool | Sequence[bool] | None] 48 max_ticks: NotRequired[int]
Keyword arguments for the line_plot function.
29class PostcovidKwargs(LineKwargs): 30 """Keyword arguments for the post-COVID plot.""" 31 32 start_r: NotRequired[Period] # start of regression period 33 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[dict[str, Any]] 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.
209def bar_plot(data: DataT, **kwargs: Unpack[BarKwargs]) -> Axes: 210 """Create a bar plot from the given data. 211 212 Each column in the DataFrame will be stacked on top of each other, 213 with positive values above zero and negative values below zero. 214 215 Args: 216 data: Series | DataFrame - The data to plot. Can be a DataFrame or a Series. 217 **kwargs: BarKwargs - Additional keyword arguments for customization. 218 (see BarKwargs for details) 219 220 Note: This function does not assume all data is timeseries with a PeriodIndex. 221 222 Returns: 223 axes: Axes - The axes for the plot. 224 225 """ 226 # --- check the kwargs 227 report_kwargs(caller=ME, **kwargs) 228 validate_kwargs(schema=BarKwargs, caller=ME, **kwargs) 229 230 # --- get the data 231 # no call to check_clean_timeseries here, as bar plots are not 232 # necessarily timeseries data. If the data is a Series, it will be 233 # converted to a DataFrame with a single column. 234 df = DataFrame(data) # really we are only plotting DataFrames 235 df, kwargs_d = constrain_data(df, **kwargs) 236 item_count = len(df.columns) 237 238 # --- deal with complete PeriodIndex indices 239 saved_pi = map_periodindex(df) 240 if saved_pi is not None: 241 df = saved_pi[0] # extract the reindexed DataFrame from the PeriodIndex 242 243 # --- set up the default arguments 244 chart_defaults: dict[str, bool | int] = { 245 "stacked": False, 246 "max_ticks": DEFAULT_MAX_TICKS, 247 "label_series": item_count > 1, 248 } 249 chart_args = {k: kwargs_d.get(k, v) for k, v in chart_defaults.items()} 250 251 bar_defaults = { 252 "color": get_color_list(item_count), 253 "width": get_setting("bar_width"), 254 "label_series": item_count > 1, 255 } 256 above = kwargs_d.get("above", False) 257 anno_args: AnnoKwargs = { 258 "annotate": kwargs_d.get("annotate", False), 259 "fontsize": kwargs_d.get("fontsize", "small"), 260 "fontname": kwargs_d.get("fontname", "Helvetica"), 261 "rotation": kwargs_d.get("rotation", 0), 262 "rounding": kwargs_d.get("rounding", True), 263 "color": kwargs_d.get("annotate_color", "black" if above else "white"), 264 "above": above, 265 } 266 bar_args, remaining_kwargs = apply_defaults(item_count, bar_defaults, kwargs_d) 267 268 # --- plot the data 269 axes, remaining_kwargs = get_axes(**dict(remaining_kwargs)) 270 if chart_args["stacked"]: 271 stacked(axes, df, anno_args, **bar_args) 272 else: 273 grouped(axes, df, anno_args, **bar_args) 274 275 # --- handle complete periodIndex data and label rotation 276 if saved_pi is not None: 277 set_labels(axes, saved_pi[1], chart_args["max_ticks"]) 278 else: 279 plt.xticks(rotation=90) 280 281 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.
133def bar_plot_finalise( 134 data: DataT, 135 **kwargs: Unpack[BPFKwargs], 136) -> None: 137 """Call bar_plot() and finalise_plot(). 138 139 Args: 140 data: The data to be plotted. 141 kwargs: Combined bar plot and finalise plot keyword arguments. 142 143 """ 144 validate_kwargs(schema=BPFKwargs, caller="bar_plot_finalise", **kwargs) 145 kwargs = impose_legend(kwargs=kwargs, data=data) 146 plot_then_finalise( 147 data, 148 function=bar_plot, 149 **kwargs, 150 )
Call bar_plot() and finalise_plot().
Args: data: The data to be plotted. kwargs: Combined bar 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.
338def finalise_plot(axes: Axes, **kwargs: Unpack[FinaliseKwargs]) -> None: 339 """Finalise and save plots to the file system. 340 341 The filename for the saved plot is constructed from the global 342 chart_dir, the plot's title, any specified tag text, and the 343 file_type for the plot. 344 345 Args: 346 axes: Axes - matplotlib axes object - required 347 kwargs: FinaliseKwargs 348 349 """ 350 # --- check the kwargs 351 report_kwargs(caller=ME, **kwargs) 352 validate_kwargs(schema=FinaliseKwargs, caller=ME, **kwargs) 353 354 # --- sanity checks 355 if len(axes.get_children()) < 1: 356 print(f"Warning: {ME}() called with an empty axes, which was ignored.") 357 return 358 359 # --- remember axis-limits should we need to restore thems 360 xlim, ylim = axes.get_xlim(), axes.get_ylim() 361 362 # margins 363 axes.margins(DEFAULT_MARGIN) 364 axes.autoscale(tight=False) # This is problematic ... 365 366 apply_kwargs(axes, **kwargs) 367 368 # tight layout and save the figure 369 fig = axes.figure 370 if kwargs.get("preserve_lims"): 371 # restore the original limits of the axes 372 axes.set_xlim(xlim) 373 axes.set_ylim(ylim) 374 if not isinstance(fig, SubFigure): 375 fig.tight_layout(pad=TIGHT_LAYOUT_PAD) 376 apply_late_kwargs(axes, **kwargs) 377 legend = axes.get_legend() 378 if legend and kwargs.get("remove_legend", False): 379 legend.remove() 380 if not isinstance(fig, SubFigure): 381 save_to_file(fig, **kwargs) 382 383 # show the plot in Jupyter Lab 384 if kwargs.get("show"): 385 plt.show() 386 387 # And close 388 if not kwargs.get("dont_close", False): 389 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.
153def growth_plot_finalise(data: DataT, **kwargs: Unpack[GrowthPFKwargs]) -> None: 154 """Call growth_plot() and finalise_plot(). 155 156 Args: 157 data: The growth data to be plotted. 158 kwargs: Combined growth plot and finalise plot keyword arguments. 159 160 Note: 161 Use this when you are providing the raw growth data. Don't forget to 162 set the ylabel in kwargs. 163 164 """ 165 validate_kwargs(schema=GrowthPFKwargs, caller="growth_plot_finalise", **kwargs) 166 kwargs = impose_legend(kwargs=kwargs, force=True) 167 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.
146def line_plot(data: DataT, **kwargs: Unpack[LineKwargs]) -> Axes: 147 """Build a single or multi-line plot. 148 149 Args: 150 data: DataFrame | Series - data to plot 151 kwargs: LineKwargs - keyword arguments for the line plot 152 153 Returns: 154 - axes: Axes - the axes object for the plot 155 156 """ 157 # --- check the kwargs 158 report_kwargs(caller=ME, **kwargs) 159 validate_kwargs(schema=LineKwargs, caller=ME, **kwargs) 160 161 # --- check the data 162 data = check_clean_timeseries(data, ME) 163 df = DataFrame(data) # we are only plotting DataFrames 164 df, kwargs_d = constrain_data(df, **kwargs) 165 166 # --- convert PeriodIndex to Integer Index 167 saved_pi = map_periodindex(df) 168 if saved_pi is not None: 169 df = saved_pi[0] 170 171 if isinstance(df.index, PeriodIndex): 172 print("Internal error: data is still a PeriodIndex - come back here and fix it") 173 174 # --- Let's plot 175 axes, kwargs_d = get_axes(**kwargs_d) # get the axes to plot on 176 if df.empty or df.isna().all().all(): 177 # Note: finalise plot should ignore an empty axes object 178 print(f"Warning: No data to plot in {ME}().") 179 return axes 180 181 # --- get the arguments for each line we will plot ... 182 item_count = len(df.columns) 183 num_data_points = len(df) 184 swce, kwargs_d = get_style_width_color_etc(item_count, num_data_points, **kwargs_d) 185 186 for i, column in enumerate(df.columns): 187 series = df[column] 188 series = series.dropna() if "dropna" in swce and swce["dropna"][i] else series 189 if series.empty or series.isna().all(): 190 print(f"Warning: No data to plot for {column} in line_plot().") 191 continue 192 193 axes.plot( 194 # using matplotlib, as pandas can set xlabel/ylabel 195 series.index, # x 196 series, # y 197 ls=swce["style"][i], 198 lw=swce["width"][i], 199 color=swce["color"][i], 200 alpha=swce["alpha"][i], 201 marker=swce["marker"][i], 202 ms=swce["markersize"][i], 203 drawstyle=swce["drawstyle"][i], 204 label=(column if "label_series" in swce and swce["label_series"][i] else f"_{column}_"), 205 ) 206 207 if swce["annotate"][i] is None or not swce["annotate"][i]: 208 continue 209 210 color = swce["color"][i] if swce["annotate_color"][i] is True else swce["annotate_color"][i] 211 annotate_series( 212 series, 213 axes, 214 color=color, 215 rounding=swce["rounding"][i], 216 fontsize=swce["fontsize"][i], 217 fontname=swce["fontname"][i], 218 rotation=swce["rotation"][i], 219 ) 220 221 # --- set the labels 222 if saved_pi is not None: 223 set_labels(axes, saved_pi[1], kwargs_d.get("max_ticks", get_setting("max_ticks"))) 224 225 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
170def line_plot_finalise( 171 data: DataT, 172 **kwargs: Unpack[LPFKwargs], 173) -> None: 174 """Call line_plot() then finalise_plot(). 175 176 Args: 177 data: The data to be plotted. 178 kwargs: Combined line plot and finalise plot keyword arguments. 179 180 """ 181 validate_kwargs(schema=LPFKwargs, caller="line_plot_finalise", **kwargs) 182 kwargs = impose_legend(kwargs=kwargs, data=data) 183 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.
271def multi_column( 272 data: DataFrame, 273 function: Callable | list[Callable], 274 **kwargs: Any, 275) -> None: 276 """Create multiple plots, one for each column in a DataFrame. 277 278 Args: 279 data: DataFrame - The data to be plotted. 280 function: Callable | list[Callable] - The plotting function(s) to be used. 281 kwargs: Any - Additional keyword arguments passed to plotting functions. 282 283 Returns: 284 None 285 286 Raises: 287 TypeError: If data is not a DataFrame. 288 ValueError: If DataFrame is empty or has no columns. 289 290 Note: 291 The plot title will be kwargs["title"] plus the column name. 292 293 """ 294 # --- sanity checks 295 me = "multi_column" 296 report_kwargs(caller=me, **kwargs) 297 if not isinstance(data, DataFrame): 298 raise TypeError("data must be a pandas DataFrame for multi_column()") 299 if data.empty: 300 raise ValueError("DataFrame cannot be empty") 301 if len(data.columns) == 0: 302 raise ValueError("DataFrame must have at least one column") 303 304 # --- check the function argument 305 title_stem = kwargs.get("title", "") 306 tag: Final[str] = kwargs.get("tag", "") 307 first, kwargs["function"] = first_unchain(function) 308 if not kwargs["function"]: 309 del kwargs["function"] # remove the function key if it is empty 310 311 # --- iterate over the columns 312 for i, col in enumerate(data.columns): 313 series = data[col] # Extract as Series, not single-column DataFrame 314 kwargs["title"] = f"{title_stem}{col}" if title_stem else str(col) 315 kwargs["tag"] = _generate_tag(tag, i) 316 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.
213def multi_start( 214 data: DataT, 215 function: Callable | list[Callable], 216 starts: Iterable[None | Period | int], 217 **kwargs: Any, 218) -> None: 219 """Create multiple plots with different starting points. 220 221 Args: 222 data: Series | DataFrame - The data to be plotted. 223 function: Callable | list[Callable] - desired plotting function(s). 224 starts: Iterable[Period | int | None] - The starting points for each plot. 225 kwargs: Any - Additional keyword arguments passed to plotting functions. 226 227 Returns: 228 None 229 230 Raises: 231 TypeError: If starts is not an iterable of None, Period or int. 232 ValueError: If starts contains invalid values or is empty. 233 234 Note: 235 kwargs['tag'] is used to create a unique tag for each plot. 236 237 """ 238 # --- sanity checks 239 me = "multi_start" 240 report_kwargs(caller=me, **kwargs) 241 if not isinstance(starts, Iterable): 242 raise TypeError("starts must be an iterable of None, Period or int") 243 244 # Convert to list to validate contents and check if empty 245 starts_list = list(starts) 246 if not starts_list: 247 raise ValueError("starts cannot be empty") 248 249 # Validate each start value 250 for i, start in enumerate(starts_list): 251 if start is not None and not isinstance(start, (Period, int)): 252 raise TypeError( 253 f"Start value at index {i} must be None, Period, or int, got {type(start).__name__}" 254 ) 255 256 # --- check the function argument 257 original_tag: Final[str] = kwargs.get("tag", "") 258 first, kwargs["function"] = first_unchain(function) 259 if not kwargs["function"]: 260 del kwargs["function"] # remove the function key if it is empty 261 262 # --- iterate over the starts 263 for i, start in enumerate(starts_list): 264 kw = kwargs.copy() # copy to avoid modifying the original kwargs 265 this_tag = _generate_tag(original_tag, i) 266 kw["tag"] = this_tag 267 kw["plot_from"] = start # rely on plotting function to constrain the data 268 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.
150def plot_then_finalise( 151 data: DataT, 152 function: Callable | list[Callable], 153 **kwargs: Any, 154) -> None: 155 """Chain a plotting function with the finalise_plot() function. 156 157 Args: 158 data: Series | DataFrame - The data to be plotted. 159 function: Callable | list[Callable] - the desired plotting function(s). 160 kwargs: Any - Additional keyword arguments. 161 162 Returns None. 163 164 """ 165 # --- checks 166 me = "plot_then_finalise" 167 report_kwargs(caller=me, **kwargs) 168 # validate once we have established the first function 169 170 # data is not checked here, assume it is checked by the called 171 # plot function. 172 173 first, kwargs["function"] = first_unchain(function) 174 if not kwargs["function"]: 175 del kwargs["function"] # remove the function key if it is empty 176 177 # Check that forbidden functions are not called first 178 if hasattr(first, "__name__") and first.__name__ in FORBIDDEN_FIRST_FUNCTIONS: 179 raise ValueError( 180 f"Function '{first.__name__}' should not be called by {me}. Call it before calling {me}." 181 ) 182 183 if first in EXPECTED_CALLABLES: 184 expected = EXPECTED_CALLABLES[first] 185 plot_kwargs = limit_kwargs(expected, **kwargs) 186 else: 187 # this is an unexpected Callable, so we will give it a try 188 print(f"Unknown proposed function: {first}; nonetheless, will give it a try.") 189 expected = BaseKwargs 190 plot_kwargs = kwargs.copy() 191 192 # --- validate the original kwargs (could not do before now) 193 kw_types = ( 194 # combine the expected kwargs types with the finalise kwargs types 195 dict(cast("dict[str, Any]", expected.__annotations__)) 196 | dict(cast("dict[str, Any]", FinaliseKwargs.__annotations__)) 197 ) 198 validate_kwargs(schema=kw_types, caller=me, **kwargs) 199 200 # --- call the first function with the data and selected plot kwargs 201 axes = first(data, **plot_kwargs) 202 203 # --- prepare finalise kwargs (remove overlapping arguments) 204 fp_kwargs = limit_kwargs(FinaliseKwargs, **kwargs) 205 # Remove any arguments that were already used in the plot function 206 used_plot_args = set(plot_kwargs.keys()) 207 fp_kwargs = {k: v for k, v in fp_kwargs.items() if k not in used_plot_args} 208 209 # --- finalise the plot 210 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.
63def postcovid_plot(data: DataT, **kwargs: Unpack[PostcovidKwargs]) -> Axes: 64 """Plot a series with a PeriodIndex, including a post-COVID projection. 65 66 Args: 67 data: Series - the series to be plotted. 68 kwargs: PostcovidKwargs - plotting arguments. 69 70 Raises: 71 TypeError if series is not a pandas Series 72 TypeError if series does not have a PeriodIndex 73 ValueError if series does not have a D, M or Q frequency 74 ValueError if regression start is after regression end 75 76 """ 77 # --- check the kwargs 78 report_kwargs(caller=ME, **kwargs) 79 validate_kwargs(schema=PostcovidKwargs, caller=ME, **kwargs) 80 81 # --- check the data 82 data = check_clean_timeseries(data, ME) 83 if not isinstance(data, Series): 84 raise TypeError("The series argument must be a pandas Series") 85 86 series_index = PeriodIndex(data.index) 87 freq_str = series_index.freqstr 88 if not freq_str or freq_str[0] not in ("Q", "M", "D"): 89 raise ValueError("The series index must have a D, M or Q frequency") 90 91 freq_key = freq_str[0] 92 93 # rely on line_plot() to validate kwargs 94 if "plot_from" in kwargs: 95 print("Warning: the 'plot_from' argument is ignored in postcovid_plot().") 96 del kwargs["plot_from"] 97 98 # --- plot COVID counterfactual 99 default_periods = DEFAULT_PERIODS[freq_key] 100 start_regression = Period(default_periods["start"], freq=freq_str) 101 end_regression = Period(default_periods["end"], freq=freq_str) 102 103 # Override defaults with user-provided periods if specified 104 user_start = kwargs.pop("start_r", None) 105 user_end = kwargs.pop("end_r", None) 106 107 if user_start is not None: 108 start_regression = Period(user_start, freq=freq_str) 109 if user_end is not None: 110 end_regression = Period(user_end, freq=freq_str) 111 112 # Validate regression period 113 if start_regression >= end_regression: 114 raise ValueError("Start period must be before end period") 115 116 if start_regression not in data.index: 117 raise ValueError(f"Regression start period {start_regression} not found in series") 118 if end_regression not in data.index: 119 raise ValueError(f"Regression end period {end_regression} not found in series") 120 121 # --- combine data and projection 122 recent_data = data[data.index >= start_regression].copy() 123 recent_data.name = "Series" 124 projection_data = get_projection(recent_data, end_regression) 125 projection_data.name = "Pre-COVID projection" 126 127 # Create DataFrame with proper column alignment 128 combined_data = DataFrame( 129 { 130 projection_data.name: projection_data, 131 recent_data.name: recent_data, 132 } 133 ) 134 135 # --- activate plot settings 136 kwargs["width"] = kwargs.pop( 137 "width", 138 (get_setting("line_normal"), get_setting("line_wide")), 139 ) # series line is thicker than projection 140 kwargs["style"] = kwargs.pop("style", ("--", "-")) # dashed regression line 141 kwargs["label_series"] = kwargs.pop("label_series", True) 142 kwargs["annotate"] = kwargs.pop("annotate", (False, True)) # annotate series only 143 kwargs["color"] = kwargs.pop("color", ("darkblue", "#dd0000")) 144 145 return line_plot( 146 combined_data, 147 **cast("LineKwargs", kwargs), 148 )
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
186def postcovid_plot_finalise( 187 data: DataT, 188 **kwargs: Unpack[PCFKwargs], 189) -> None: 190 """Call postcovid_plot() and finalise_plot(). 191 192 Args: 193 data: The data to be plotted. 194 kwargs: Combined postcovid plot and finalise plot keyword arguments. 195 196 """ 197 validate_kwargs(schema=PCFKwargs, caller="postcovid_plot_finalise", **kwargs) 198 kwargs = impose_legend(kwargs=kwargs, force=True) 199 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.
202def revision_plot_finalise( 203 data: DataT, 204 **kwargs: Unpack[RevPFKwargs], 205) -> None: 206 """Call revision_plot() and finalise_plot(). 207 208 Args: 209 data: The revision data to be plotted. 210 kwargs: Combined revision plot and finalise plot keyword arguments. 211 212 """ 213 validate_kwargs(schema=RevPFKwargs, caller="revision_plot_finalise", **kwargs) 214 kwargs = impose_legend(kwargs=kwargs, force=True) 215 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
218def run_plot_finalise( 219 data: DataT, 220 **kwargs: Unpack[RunPFKwargs], 221) -> None: 222 """Call run_plot() and finalise_plot(). 223 224 Args: 225 data: The data to be plotted. 226 kwargs: Combined run plot and finalise plot keyword arguments. 227 228 """ 229 validate_kwargs(schema=RunPFKwargs, caller="run_plot_finalise", **kwargs) 230 kwargs = impose_legend(kwargs=kwargs, force=("highlight_label" in kwargs)) 231 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
234def seastrend_plot_finalise( 235 data: DataT, 236 **kwargs: Unpack[SFKwargs], 237) -> None: 238 """Call seastrend_plot() and finalise_plot(). 239 240 Args: 241 data: The seasonal and trend data to be plotted. 242 kwargs: Combined seastrend plot and finalise plot keyword arguments. 243 244 """ 245 validate_kwargs(schema=SFKwargs, caller="seastrend_plot_finalise", **kwargs) 246 kwargs = impose_legend(kwargs=kwargs, force=True) 247 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
250def series_growth_plot_finalise(data: DataT, **kwargs: Unpack[SGFPKwargs]) -> None: 251 """Call series_growth_plot() and finalise_plot(). 252 253 Args: 254 data: The series data to calculate and plot growth for. 255 kwargs: Combined series growth plot and finalise plot keyword arguments. 256 257 """ 258 validate_kwargs(schema=SGFPKwargs, caller="series_growth_plot_finalise", **kwargs) 259 kwargs = impose_legend(kwargs=kwargs, force=True) 260 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.
263def summary_plot_finalise( 264 data: DataT, 265 **kwargs: Unpack[SumPFKwargs], 266) -> None: 267 """Call summary_plot() and finalise_plot(). 268 269 This is more complex than most of the above convenience methods as it 270 creates multiple plots (one for each plot type). 271 272 Args: 273 data: DataFrame containing the summary data. The index must be a PeriodIndex. 274 kwargs: Combined summary plot and finalise plot keyword arguments. 275 276 Raises: 277 TypeError: If data is not a DataFrame with a PeriodIndex. 278 IndexError: If DataFrame is empty. 279 280 """ 281 # --- validate data type and structure 282 if not isinstance(data, DataFrame) or not isinstance(data.index, PeriodIndex): 283 raise TypeError("Data must be a DataFrame with a PeriodIndex.") 284 285 if data.empty or len(data.index) == 0: 286 raise ValueError("DataFrame cannot be empty") 287 288 validate_kwargs(schema=SumPFKwargs, caller="summary_plot_finalise", **kwargs) 289 290 # --- set default title with bounds checking 291 kwargs["title"] = kwargs.get("title", f"Summary at {label_period(data.index[-1])}") 292 kwargs["preserve_lims"] = kwargs.get("preserve_lims", True) 293 294 # --- handle plot_from parameter with bounds checking 295 start: int | Period | None = kwargs.get("plot_from", 0) 296 if start is None: 297 start = data.index[0] 298 elif isinstance(start, int): 299 if abs(start) >= len(data.index): 300 raise IndexError( 301 f"plot_from index {start} out of range for DataFrame with {len(data.index)} rows" 302 ) 303 start = data.index[start] 304 305 kwargs["plot_from"] = start 306 if not isinstance(start, Period): 307 raise TypeError("plot_from must be a Period or convertible to one") 308 309 # --- create plots for each plot type 310 pre_tag: str = kwargs.get("pre_tag", "") 311 for plot_type in SUMMARY_PLOT_TYPES: 312 plot_kwargs = kwargs.copy() # Avoid modifying original kwargs 313 plot_kwargs["plot_type"] = plot_type 314 plot_kwargs["pre_tag"] = pre_tag + plot_type 315 316 plot_then_finalise( 317 data, 318 function=summary_plot, 319 **plot_kwargs, 320 )
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.