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.
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 dropna: NotRequired[bool | Sequence[bool]] 41 annotate: NotRequired[bool | Sequence[bool]] 42 rounding: NotRequired[Sequence[int | bool] | int | bool | None] 43 fontsize: NotRequired[Sequence[str | int | float] | str | int | float] 44 fontname: NotRequired[str | Sequence[str]] 45 rotation: NotRequired[Sequence[int | float] | int | float] 46 annotate_color: NotRequired[str | Sequence[str] | bool | Sequence[bool] | None] 47 plot_from: NotRequired[int | Period | None] 48 label_series: NotRequired[bool | Sequence[bool] | None] 49 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.
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.
132def bar_plot_finalise( 133 data: DataT, 134 **kwargs: Unpack[BPFKwargs], 135) -> None: 136 """Call bar_plot() and finalise_plot(). 137 138 Args: 139 data: The data to be plotted. 140 kwargs: Combined bar plot and finalise plot keyword arguments. 141 142 """ 143 validate_kwargs(schema=BPFKwargs, caller="bar_plot_finalise", **kwargs) 144 kwargs = impose_legend(kwargs=kwargs, data=data) 145 plot_then_finalise( 146 data, 147 function=bar_plot, 148 **kwargs, 149 )
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.
152def growth_plot_finalise(data: DataT, **kwargs: Unpack[GrowthPFKwargs]) -> None: 153 """Call growth_plot() and finalise_plot(). 154 155 Args: 156 data: The growth data to be plotted. 157 kwargs: Combined growth plot and finalise plot keyword arguments. 158 159 Note: 160 Use this when you are providing the raw growth data. Don't forget to 161 set the ylabel in kwargs. 162 163 """ 164 validate_kwargs(schema=GrowthPFKwargs, caller="growth_plot_finalise", **kwargs) 165 kwargs = impose_legend(kwargs=kwargs, force=True) 166 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.
147def line_plot(data: DataT, **kwargs: Unpack[LineKwargs]) -> Axes: 148 """Build a single or multi-line plot. 149 150 Args: 151 data: DataFrame | Series - data to plot 152 kwargs: LineKwargs - keyword arguments for the line plot 153 154 Returns: 155 - axes: Axes - the axes object for the plot 156 157 """ 158 # --- check the kwargs 159 report_kwargs(caller=ME, **kwargs) 160 validate_kwargs(schema=LineKwargs, caller=ME, **kwargs) 161 162 # --- check the data 163 data = check_clean_timeseries(data, ME) 164 df = DataFrame(data) # we are only plotting DataFrames 165 df, kwargs_d = constrain_data(df, **kwargs) 166 167 # --- convert PeriodIndex to Integer Index 168 saved_pi = map_periodindex(df) 169 if saved_pi is not None: 170 df = saved_pi[0] 171 172 if isinstance(df.index, PeriodIndex): 173 print("Internal error: data is still a PeriodIndex - come back here and fix it") 174 175 # --- Let's plot 176 axes, kwargs_d = get_axes(**kwargs_d) # get the axes to plot on 177 if df.empty or df.isna().all().all(): 178 # Note: finalise plot should ignore an empty axes object 179 print(f"Warning: No data to plot in {ME}().") 180 return axes 181 182 # --- get the arguments for each line we will plot ... 183 item_count = len(df.columns) 184 num_data_points = len(df) 185 swce, kwargs_d = get_style_width_color_etc(item_count, num_data_points, **kwargs_d) 186 187 for i, column in enumerate(df.columns): 188 series = df[column] 189 series = series.dropna() if "dropna" in swce and swce["dropna"][i] else series 190 if series.empty or series.isna().all(): 191 print(f"Warning: No data to plot for {column} in line_plot().") 192 continue 193 194 axes.plot( 195 # using matplotlib, as pandas can set xlabel/ylabel 196 series.index, # x 197 series, # y 198 ls=swce["style"][i], 199 lw=swce["width"][i], 200 color=swce["color"][i], 201 alpha=swce["alpha"][i], 202 marker=swce["marker"][i], 203 ms=swce["markersize"][i], 204 drawstyle=swce["drawstyle"][i], 205 label=(column if "label_series" in swce and swce["label_series"][i] else f"_{column}_"), 206 ) 207 208 if swce["annotate"][i] is None or not swce["annotate"][i]: 209 continue 210 211 color = swce["color"][i] if swce["annotate_color"][i] is True else swce["annotate_color"][i] 212 annotate_series( 213 series, 214 axes, 215 color=color, 216 rounding=swce["rounding"][i], 217 fontsize=swce["fontsize"][i], 218 fontname=swce["fontname"][i], 219 rotation=swce["rotation"][i], 220 ) 221 222 # --- set the labels 223 if saved_pi is not None: 224 set_labels(axes, saved_pi[1], kwargs_d.get("max_ticks", get_setting("max_ticks"))) 225 226 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
169def line_plot_finalise( 170 data: DataT, 171 **kwargs: Unpack[LPFKwargs], 172) -> None: 173 """Call line_plot() then finalise_plot(). 174 175 Args: 176 data: The data to be plotted. 177 kwargs: Combined line plot and finalise plot keyword arguments. 178 179 """ 180 validate_kwargs(schema=LPFKwargs, caller="line_plot_finalise", **kwargs) 181 kwargs = impose_legend(kwargs=kwargs, data=data) 182 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.
115def postcovid_plot(data: DataT, **kwargs: Unpack[PostcovidKwargs]) -> Axes: 116 """Plot a series with a PeriodIndex, including a post-COVID projection. 117 118 Args: 119 data: Series - the series to be plotted. 120 kwargs: PostcovidKwargs - plotting arguments. 121 122 Raises: 123 TypeError if series is not a pandas Series 124 TypeError if series does not have a PeriodIndex 125 ValueError if series does not have a D, M or Q frequency 126 ValueError if regression start is after regression end 127 128 """ 129 # --- check the kwargs 130 report_kwargs(caller=ME, **kwargs) 131 validate_kwargs(schema=PostcovidKwargs, caller=ME, **kwargs) 132 133 # --- check the data 134 data = check_clean_timeseries(data, ME) 135 if not isinstance(data, Series): 136 raise TypeError("The series argument must be a pandas Series") 137 138 # rely on line_plot() to validate kwargs, but remove any that are not relevant 139 if "plot_from" in kwargs: 140 print("Warning: the 'plot_from' argument is ignored in postcovid_plot().") 141 del kwargs["plot_from"] 142 143 # --- set the regression period 144 start_r, end_r, robust = regression_period(data, **kwargs) 145 kwargs.pop("start_r", None) # remove from kwargs to avoid confusion 146 kwargs.pop("end_r", None) # remove from kwargs to avoid confusion 147 if not robust: 148 print("No valid regression period found; plotting raw data only.") 149 return line_plot( 150 data, 151 **cast("LineKwargs", kwargs), 152 ) 153 154 # --- combine data and projection 155 if start_r < data.dropna().index.min(): 156 print(f"Caution: Regression start period pre-dates the series index: {start_r=}") 157 recent_data = data[data.index >= start_r].copy() 158 recent_data.name = "Series" 159 projection_data = get_projection(recent_data, end_r) 160 projection_data.name = "Pre-COVID projection" 161 162 # --- Create DataFrame with proper column alignment 163 combined_data = DataFrame( 164 { 165 projection_data.name: projection_data, 166 recent_data.name: recent_data, 167 } 168 ) 169 170 # --- activate plot settings 171 kwargs["width"] = kwargs.pop( 172 "width", 173 (get_setting("line_normal"), get_setting("line_wide")), 174 ) # series line is thicker than projection 175 kwargs["style"] = kwargs.pop("style", ("--", "-")) # dashed regression line 176 kwargs["label_series"] = kwargs.pop("label_series", True) 177 kwargs["annotate"] = kwargs.pop("annotate", (False, True)) # annotate series only 178 kwargs["color"] = kwargs.pop("color", ("darkblue", "#dd0000")) 179 kwargs["dropna"] = kwargs.pop("dropna", False) # drop NaN values 180 181 return line_plot( 182 combined_data, 183 **cast("LineKwargs", kwargs), 184 )
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
185def postcovid_plot_finalise( 186 data: DataT, 187 **kwargs: Unpack[PCFKwargs], 188) -> None: 189 """Call postcovid_plot() and finalise_plot(). 190 191 Args: 192 data: The data to be plotted. 193 kwargs: Combined postcovid plot and finalise plot keyword arguments. 194 195 """ 196 validate_kwargs(schema=PCFKwargs, caller="postcovid_plot_finalise", **kwargs) 197 kwargs = impose_legend(kwargs=kwargs, force=True) 198 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.
201def revision_plot_finalise( 202 data: DataT, 203 **kwargs: Unpack[RevPFKwargs], 204) -> None: 205 """Call revision_plot() and finalise_plot(). 206 207 Args: 208 data: The revision data to be plotted. 209 kwargs: Combined revision plot and finalise plot keyword arguments. 210 211 """ 212 validate_kwargs(schema=RevPFKwargs, caller="revision_plot_finalise", **kwargs) 213 kwargs = impose_legend(kwargs=kwargs, force=True) 214 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
217def run_plot_finalise( 218 data: DataT, 219 **kwargs: Unpack[RunPFKwargs], 220) -> None: 221 """Call run_plot() and finalise_plot(). 222 223 Args: 224 data: The data to be plotted. 225 kwargs: Combined run plot and finalise plot keyword arguments. 226 227 """ 228 validate_kwargs(schema=RunPFKwargs, caller="run_plot_finalise", **kwargs) 229 kwargs = impose_legend(kwargs=kwargs, force="highlight_label" in kwargs) 230 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
233def seastrend_plot_finalise( 234 data: DataT, 235 **kwargs: Unpack[SFKwargs], 236) -> None: 237 """Call seastrend_plot() and finalise_plot(). 238 239 Args: 240 data: The seasonal and trend data to be plotted. 241 kwargs: Combined seastrend plot and finalise plot keyword arguments. 242 243 """ 244 validate_kwargs(schema=SFKwargs, caller="seastrend_plot_finalise", **kwargs) 245 kwargs = impose_legend(kwargs=kwargs, force=True) 246 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
249def series_growth_plot_finalise(data: DataT, **kwargs: Unpack[SGFPKwargs]) -> None: 250 """Call series_growth_plot() and finalise_plot(). 251 252 Args: 253 data: The series data to calculate and plot growth for. 254 kwargs: Combined series growth plot and finalise plot keyword arguments. 255 256 """ 257 validate_kwargs(schema=SGFPKwargs, caller="series_growth_plot_finalise", **kwargs) 258 kwargs = impose_legend(kwargs=kwargs, force=True) 259 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.
262def summary_plot_finalise( 263 data: DataT, 264 **kwargs: Unpack[SumPFKwargs], 265) -> None: 266 """Call summary_plot() and finalise_plot(). 267 268 This is more complex than most of the above convenience methods as it 269 creates multiple plots (one for each plot type). 270 271 Args: 272 data: DataFrame containing the summary data. The index must be a PeriodIndex. 273 kwargs: Combined summary plot and finalise plot keyword arguments. 274 275 Raises: 276 TypeError: If data is not a DataFrame with a PeriodIndex. 277 IndexError: If DataFrame is empty. 278 279 """ 280 # --- validate data type and structure 281 if not isinstance(data, DataFrame) or not isinstance(data.index, PeriodIndex): 282 raise TypeError("Data must be a DataFrame with a PeriodIndex.") 283 284 if data.empty or len(data.index) == 0: 285 raise ValueError("DataFrame cannot be empty") 286 287 validate_kwargs(schema=SumPFKwargs, caller="summary_plot_finalise", **kwargs) 288 289 # --- set default title with bounds checking 290 kwargs["title"] = kwargs.get("title", f"Summary at {label_period(data.index[-1])}") 291 kwargs["preserve_lims"] = kwargs.get("preserve_lims", True) 292 293 # --- handle plot_from parameter with bounds checking 294 start: int | Period | None = kwargs.get("plot_from", 0) 295 if start is None: 296 start = data.index[0] 297 elif isinstance(start, int): 298 if abs(start) >= len(data.index): 299 raise IndexError( 300 f"plot_from index {start} out of range for DataFrame with {len(data.index)} rows" 301 ) 302 start = data.index[start] 303 304 kwargs["plot_from"] = start 305 if not isinstance(start, Period): 306 raise TypeError("plot_from must be a Period or convertible to one") 307 308 # --- create plots for each plot type 309 pre_tag: str = kwargs.get("pre_tag", "") 310 for plot_type in SUMMARY_PLOT_TYPES: 311 plot_kwargs = kwargs.copy() # Avoid modifying original kwargs 312 plot_kwargs["plot_type"] = plot_type 313 plot_kwargs["pre_tag"] = pre_tag + plot_type 314 315 plot_then_finalise( 316 data, 317 function=summary_plot, 318 **plot_kwargs, 319 )
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.