mgplot
Provide a frontend to matplotlib for working with timeseries data, indexed with a PeriodIndex.
This package simplifiers the creation of common plots used in economic and financial analysis, such as bar plots, line plots, growth plots, and seasonal trend plots. It also includes utilities for color management and finalising plots with consistent styling.
1"""Provide a frontend to matplotlib for working with timeseries data, indexed with a PeriodIndex. 2 3This package simplifiers the creation of common plots used in economic and financial analysis, 4such as bar plots, line plots, growth plots, and seasonal trend plots. It also includes utilities 5for color management and finalising plots with consistent styling. 6""" 7 8# --- version and author 9import importlib.metadata 10 11# --- local imports 12# Do not import the utilities, axis_utils nor keyword_checking modules here. 13from mgplot.bar_plot import BarKwargs, bar_plot 14from mgplot.colors import ( 15 abbreviate_state, 16 colorise_list, 17 contrast, 18 get_color, 19 get_party_palette, 20 state_abbrs, 21 state_names, 22) 23from mgplot.fill_between_plot import FillBetweenKwargs, fill_between_plot 24from mgplot.finalise_plot import FinaliseKwargs, finalise_plot 25from mgplot.finalisers import ( 26 bar_plot_finalise, 27 fill_between_plot_finalise, 28 growth_plot_finalise, 29 line_plot_finalise, 30 postcovid_plot_finalise, 31 revision_plot_finalise, 32 run_plot_finalise, 33 scatter_plot_finalise, 34 seastrend_plot_finalise, 35 series_growth_plot_finalise, 36 summary_plot_finalise, 37) 38from mgplot.growth_plot import ( 39 GrowthKwargs, 40 SeriesGrowthKwargs, 41 calc_growth, 42 growth_plot, 43 series_growth_plot, 44) 45from mgplot.line_plot import LineKwargs, line_plot 46from mgplot.multi_plot import multi_column, multi_start, plot_then_finalise 47from mgplot.postcovid_plot import PostcovidKwargs, postcovid_plot 48from mgplot.revision_plot import revision_plot 49from mgplot.run_plot import RunKwargs, run_plot 50from mgplot.scatter_plot import ScatterKwargs, scatter_plot 51from mgplot.seastrend_plot import seastrend_plot 52from mgplot.settings import ( 53 chart_subdir, 54 clear_chart_dir, 55 get_setting, 56 set_chart_dir, 57 set_setting, 58) 59from mgplot.summary_plot import SummaryKwargs, summary_plot 60 61# --- version and author 62try: 63 __version__ = importlib.metadata.version(__name__) 64except importlib.metadata.PackageNotFoundError: 65 __version__ = "0.0.0" # Fallback for development mode 66__author__ = "Bryan Palmer" 67 68 69# --- public API 70__all__ = ( 71 "BarKwargs", 72 "FillBetweenKwargs", 73 "FinaliseKwargs", 74 "GrowthKwargs", 75 "LineKwargs", 76 "PostcovidKwargs", 77 "RunKwargs", 78 "ScatterKwargs", 79 "SeriesGrowthKwargs", 80 "SummaryKwargs", 81 "__author__", 82 "__version__", 83 "abbreviate_state", 84 "bar_plot", 85 "bar_plot_finalise", 86 "calc_growth", 87 "chart_subdir", 88 "clear_chart_dir", 89 "colorise_list", 90 "contrast", 91 "fill_between_plot", 92 "fill_between_plot_finalise", 93 "finalise_plot", 94 "get_color", 95 "get_party_palette", 96 "get_setting", 97 "growth_plot", 98 "growth_plot_finalise", 99 "line_plot", 100 "line_plot_finalise", 101 "multi_column", 102 "multi_start", 103 "plot_then_finalise", 104 "postcovid_plot", 105 "postcovid_plot_finalise", 106 "revision_plot", 107 "revision_plot_finalise", 108 "run_plot", 109 "run_plot_finalise", 110 "scatter_plot", 111 "scatter_plot_finalise", 112 "seastrend_plot", 113 "seastrend_plot_finalise", 114 "series_growth_plot", 115 "series_growth_plot_finalise", 116 "set_chart_dir", 117 "set_setting", 118 "state_abbrs", 119 "state_names", 120 "summary_plot", 121 "summary_plot_finalise", 122)
40class BarKwargs(BaseKwargs): 41 """Keyword arguments for the bar_plot function.""" 42 43 # --- options for the entire bar plot 44 ax: NotRequired[Axes | None] 45 stacked: NotRequired[bool] 46 horizontal: NotRequired[bool] 47 max_ticks: NotRequired[int] 48 tick_relabel: NotRequired[Callable[[str], str]] 49 plot_from: NotRequired[int | Period] 50 label_rotation: NotRequired[int | float] 51 # --- options for each bar ... 52 color: NotRequired[str | Sequence[str]] 53 label_series: NotRequired[bool | Sequence[bool]] 54 width: NotRequired[float | int | Sequence[float | int]] 55 zorder: NotRequired[int | float | Sequence[int | float]] 56 # --- options for bar annotations 57 annotate: NotRequired[bool] 58 fontsize: NotRequired[int | float | str] 59 fontname: NotRequired[str] 60 rounding: NotRequired[int] 61 rotation: NotRequired[int | float] 62 annotate_color: NotRequired[str] 63 above: NotRequired[bool]
Keyword arguments for the bar_plot function.
22class FillBetweenKwargs(BaseKwargs): 23 """Keyword arguments for the fill_between_plot function.""" 24 25 ax: NotRequired[Axes | None] 26 color: NotRequired[str] 27 alpha: NotRequired[float] 28 label: NotRequired[str | None] 29 linewidth: NotRequired[float] 30 edgecolor: NotRequired[str | None] 31 zorder: NotRequired[int | float] 32 plot_from: NotRequired[int | None] 33 max_ticks: NotRequired[int] 34 tick_relabel: NotRequired[Callable[[str], str]]
Keyword arguments for the fill_between_plot function.
44class FinaliseKwargs(BaseKwargs): 45 """Keyword arguments for the finalise_plot function.""" 46 47 # --- value options 48 suptitle: NotRequired[str | None] 49 title: NotRequired[str | None] 50 xlabel: NotRequired[str | None] 51 ylabel: NotRequired[str | None] 52 xlim: NotRequired[tuple[float | int | Period, float | int | Period] | None] 53 ylim: NotRequired[tuple[float, float] | None] 54 xticks: NotRequired[list[float | int | Period] | None] 55 yticks: NotRequired[list[float] | None] 56 xscale: NotRequired[str | None] 57 yscale: NotRequired[str | None] 58 # --- splat options 59 legend: NotRequired[bool | dict[str, Any] | None] 60 axhspan: NotRequired[dict[str, Any] | Sequence[dict[str, Any]] | None] 61 axvspan: NotRequired[dict[str, Any] | Sequence[dict[str, Any]] | None] 62 axhline: NotRequired[dict[str, Any] | Sequence[dict[str, Any]] | None] 63 axvline: NotRequired[dict[str, Any] | Sequence[dict[str, Any]] | None] 64 # --- options for annotations 65 lfooter: NotRequired[str] 66 rfooter: NotRequired[str] 67 lheader: NotRequired[str] 68 rheader: NotRequired[str] 69 # --- file/save options 70 pre_tag: NotRequired[str] 71 tag: NotRequired[str] 72 filename: NotRequired[str] 73 chart_dir: NotRequired[str] 74 file_type: NotRequired[str] 75 dpi: NotRequired[int] 76 figsize: NotRequired[tuple[float, float]] 77 show: NotRequired[bool] 78 # --- other options 79 preserve_lims: NotRequired[bool] 80 remove_legend: NotRequired[bool] 81 zero_y: NotRequired[bool] 82 y0: NotRequired[bool] 83 x0: NotRequired[bool] 84 axisbelow: NotRequired[bool] 85 dont_save: NotRequired[bool] 86 dont_close: NotRequired[bool] 87 axes_only: NotRequired[bool]
Keyword arguments for the finalise_plot function.
39class GrowthKwargs(BaseKwargs): 40 """Keyword arguments for the growth_plot function.""" 41 42 # --- common options 43 ax: NotRequired[Axes | None] 44 plot_from: NotRequired[int | Period] 45 label_series: NotRequired[bool] 46 max_ticks: NotRequired[int] 47 tick_relabel: NotRequired[Callable[[str], str]] 48 # --- options passed to the line plot 49 line_width: NotRequired[float | int] 50 line_color: NotRequired[str] 51 line_style: NotRequired[str] 52 annotate_line: NotRequired[bool] 53 line_rounding: NotRequired[bool | int] 54 line_fontsize: NotRequired[str | int | float] 55 line_fontname: NotRequired[str] 56 line_anno_color: NotRequired[str] 57 # --- options passed to the bar plot 58 annotate_bars: NotRequired[bool] 59 bar_fontsize: NotRequired[str | int | float] 60 bar_fontname: NotRequired[str] 61 bar_rounding: NotRequired[int] 62 bar_width: NotRequired[float] 63 bar_color: NotRequired[str] 64 bar_anno_color: NotRequired[str] 65 bar_rotation: NotRequired[int | float]
Keyword arguments for the growth_plot function.
34class LineKwargs(BaseKwargs): 35 """Keyword arguments for the line_plot function.""" 36 37 # --- options for the entire line plot 38 ax: NotRequired[Axes | None] 39 style: NotRequired[str | Sequence[str]] 40 width: NotRequired[float | int | Sequence[float | int]] 41 color: NotRequired[str | Sequence[str]] 42 alpha: NotRequired[float | Sequence[float]] 43 drawstyle: NotRequired[str | Sequence[str] | None] 44 marker: NotRequired[str | Sequence[str] | None] 45 markersize: NotRequired[float | Sequence[float] | int | None] 46 zorder: NotRequired[int | float | Sequence[int | float]] 47 dropna: NotRequired[bool | Sequence[bool]] 48 annotate: NotRequired[bool | str | Sequence[bool | str]] 49 rounding: NotRequired[Sequence[int | bool] | int | bool | None] 50 fontsize: NotRequired[Sequence[str | int | float] | str | int | float] 51 fontname: NotRequired[str | Sequence[str]] 52 rotation: NotRequired[Sequence[int | float] | int | float] 53 annotate_color: NotRequired[str | Sequence[str] | bool | Sequence[bool] | None] 54 near_end: NotRequired[float] 55 force_right: NotRequired[bool] 56 leader_lines: NotRequired[bool] 57 plot_from: NotRequired[int | Period | None] 58 label_series: NotRequired[bool | Sequence[bool] | None] 59 max_ticks: NotRequired[int] 60 tick_relabel: NotRequired[Callable[[str], str]]
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.
33class ScatterKwargs(BaseKwargs): 34 """Keyword arguments for the scatter_plot function.""" 35 36 ax: NotRequired[Axes | None] 37 color: NotRequired[str] 38 size: NotRequired[float | int] 39 alpha: NotRequired[float] 40 marker: NotRequired[str] 41 label: NotRequired[str | None] 42 zorder: NotRequired[int | float] 43 dropna: NotRequired[bool] 44 plot_from: NotRequired[int | Period | None] 45 diagonal: NotRequired[bool | dict[str, Any]] 46 fit: NotRequired[bool | dict[str, Any]] 47 highlight_latest: NotRequired[bool | dict[str, Any]] 48 report_corr: NotRequired[bool]
Keyword arguments for the scatter_plot function.
68class SeriesGrowthKwargs(GrowthKwargs): 69 """Keyword arguments for the series_growth_plot function.""" 70 71 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.
252def bar_plot(data: DataT, **kwargs: Unpack[BarKwargs]) -> Axes: 253 """Create a bar plot from the given data. 254 255 Each column in the DataFrame will be stacked on top of each other, 256 with positive values above zero and negative values below zero. 257 258 Args: 259 data: Series | DataFrame - The data to plot. Can be a DataFrame or a Series. 260 **kwargs: BarKwargs - Additional keyword arguments for customization. 261 (see BarKwargs for details) 262 263 Note: This function does not assume all data is timeseries with a PeriodIndex. 264 265 Returns: 266 axes: Axes - The axes for the plot. 267 268 """ 269 # --- check the kwargs 270 report_kwargs(caller=ME, **kwargs) 271 validate_kwargs(schema=BarKwargs, caller=ME, **kwargs) 272 273 # --- get the data 274 # no call to check_clean_timeseries here, as bar plots are not 275 # necessarily timeseries data. If the data is a Series, it will be 276 # converted to a DataFrame with a single column. 277 df = DataFrame(data) # really we are only plotting DataFrames 278 df, kwargs_d = constrain_data(df, **kwargs) 279 item_count = len(df.columns) 280 281 # --- deal with string indices 282 saved_strings = map_stringindex(df) 283 if saved_strings is not None: 284 df = saved_strings[0] 285 286 # --- deal with complete PeriodIndex indices 287 saved_pi = map_periodindex(df) 288 if saved_pi is not None: 289 df = saved_pi[0] # extract the reindexed DataFrame from the PeriodIndex 290 291 # --- set up the default arguments 292 chart_defaults: dict[str, bool | int] = { 293 "stacked": False, 294 "horizontal": False, 295 "max_ticks": DEFAULT_MAX_TICKS, 296 "label_series": item_count > 1, 297 "label_rotation": 0, 298 } 299 chart_args = {k: kwargs_d.get(k, v) for k, v in chart_defaults.items()} 300 301 # --- horizontal bars are for categorical data, not PeriodIndex timeseries 302 horizontal = bool(chart_args["horizontal"]) 303 if horizontal and saved_pi is not None: 304 print(f"Warning: horizontal=True is not supported with a PeriodIndex in {ME}(); plotting vertical.") 305 horizontal = False 306 307 # --- single series + one colour per bar => per-bar colours 308 user_color = kwargs_d.get("color") 309 if ( 310 item_count == 1 311 and isinstance(user_color, Sequence) 312 and not isinstance(user_color, str) 313 and len(user_color) == len(df) 314 ): 315 kwargs_d["color"] = [list(user_color)] # one series whose colour is a per-bar list 316 317 bar_defaults = { 318 "color": get_color_list(item_count), 319 "width": get_setting("bar_width"), 320 "label_series": item_count > 1, 321 "zorder": None, 322 } 323 above = kwargs_d.get("above", False) 324 anno_args: AnnoKwargs = { 325 "annotate": kwargs_d.get("annotate", False), 326 "fontsize": kwargs_d.get("fontsize", "small"), 327 "fontname": kwargs_d.get("fontname", "Helvetica"), 328 "rotation": kwargs_d.get("rotation", 0), 329 "rounding": kwargs_d.get("rounding", True), 330 "color": kwargs_d.get("annotate_color", "black" if above else "white"), 331 "above": above, 332 } 333 bar_args, remaining_kwargs = apply_defaults(item_count, bar_defaults, kwargs_d) 334 335 # --- plot the data 336 axes, remaining_kwargs = get_axes(**dict(remaining_kwargs)) 337 if chart_args["stacked"]: 338 stacked(axes, df, anno_args, horizontal=horizontal, **bar_args) 339 else: 340 grouped(axes, df, anno_args, horizontal=horizontal, **bar_args) 341 342 # --- handle index labels and rotation 343 if saved_strings is not None: 344 if horizontal: 345 axes.set_yticks(range(len(saved_strings[1]))) 346 axes.set_yticklabels(saved_strings[1], rotation=chart_args["label_rotation"]) 347 else: 348 axes.set_xticks(range(len(saved_strings[1]))) 349 axes.set_xticklabels(saved_strings[1], rotation=chart_args["label_rotation"]) 350 elif saved_pi is not None: 351 set_labels( 352 axes, 353 saved_pi[1], 354 chart_args["max_ticks"], 355 rotation=chart_args["label_rotation"], 356 tick_relabel=kwargs_d.get("tick_relabel"), 357 ) 358 359 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.
145def bar_plot_finalise( 146 data: DataT, 147 **kwargs: Unpack[BPFKwargs], 148) -> None: 149 """Call bar_plot() and finalise_plot(). 150 151 Args: 152 data: The data to be plotted. 153 kwargs: Combined bar plot and finalise plot keyword arguments. 154 155 """ 156 validate_kwargs(schema=BPFKwargs, caller="bar_plot_finalise", **kwargs) 157 kwargs = impose_legend(kwargs=kwargs, data=data) 158 plot_then_finalise( 159 data, 160 function=bar_plot, 161 **kwargs, 162 )
Call bar_plot() and finalise_plot().
Args: data: The data to be plotted. kwargs: Combined bar plot and finalise plot keyword arguments.
113def calc_growth(series: Series) -> DataFrame: 114 """Calculate annual and periodic growth for a pandas Series. 115 116 Args: 117 series: Series - a pandas series with a date-like PeriodIndex. 118 119 Returns: 120 DataFrame: A two column DataFrame with annual and periodic growth rates. 121 122 Raises: 123 TypeError if the series is not a pandas Series. 124 TypeError if the series index is not a PeriodIndex. 125 ValueError if the series is empty. 126 ValueError if the series index does not have a frequency of Q, M, or D. 127 ValueError if the series index has duplicates. 128 129 """ 130 # --- sanity checks 131 if not isinstance(series, Series): 132 raise TypeError("The series argument must be a pandas Series") 133 if not isinstance(series.index, PeriodIndex): 134 raise TypeError("The series index must be a pandas PeriodIndex") 135 if series.empty: 136 raise ValueError("The series argument must not be empty") 137 freq = series.index.freqstr 138 if not freq or freq[0] not in FREQUENCY_TO_PERIODS: 139 raise ValueError("The series index must have a frequency of Q, M, or D") 140 if series.index.has_duplicates: 141 raise ValueError("The series index must not have duplicate values") 142 143 # --- ensure the index is complete and the date is sorted 144 complete = period_range(start=series.index.min(), end=series.index.max()) 145 series = series.reindex(complete, fill_value=nan) 146 series = series.sort_index(ascending=True) 147 148 # --- calculate annual and periodic growth 149 freq = PeriodIndex(series.index).freqstr 150 if not freq or freq[0] not in FREQUENCY_TO_PERIODS: 151 raise ValueError("The series index must have a frequency of Q, M, or D") 152 153 freq_key = freq[0] 154 ppy = FREQUENCY_TO_PERIODS[freq_key] 155 annual = series.pct_change(periods=ppy) * 100 156 periodic = series.pct_change(periods=1) * 100 157 periodic_name = FREQUENCY_TO_NAME[freq_key] + " Growth" 158 return DataFrame( 159 { 160 "Annual Growth": annual, 161 periodic_name: periodic, 162 }, 163 )
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.
176@contextmanager 177def chart_subdir(name: str, *, clear: bool = False) -> Generator[str]: 178 """Temporarily redirect chart output to a subdirectory of the current chart_dir. 179 180 Args: 181 name: str - subdirectory name (relative to the current chart_dir). 182 clear: bool - if True, clear the subdirectory of graph-image files 183 on entry. Only set this on the first use of a subdirectory in a 184 notebook; later cells writing to the same subdirectory should 185 leave it False. 186 187 Yields: 188 str - the subdirectory path. 189 190 Note: The previous chart directory is restored on exit, even if an 191 exception is raised. 192 193 """ 194 main_dir = get_setting("chart_dir") 195 sub_dir = str(Path(main_dir) / name) 196 set_chart_dir(sub_dir) 197 if clear: 198 clear_chart_dir() 199 try: 200 yield sub_dir 201 finally: 202 set_setting("chart_dir", main_dir)
Temporarily redirect chart output to a subdirectory of the current chart_dir.
Args: name: str - subdirectory name (relative to the current chart_dir). clear: bool - if True, clear the subdirectory of graph-image files on entry. Only set this on the first use of a subdirectory in a notebook; later cells writing to the same subdirectory should leave it False.
Yields: str - the subdirectory path.
Note: The previous chart directory is restored on exit, even if an exception is raised.
148def clear_chart_dir() -> None: 149 """Remove all graph-image files from the global chart_dir.""" 150 chart_dir = get_setting("chart_dir") 151 Path(chart_dir).mkdir(parents=True, exist_ok=True) 152 for ext in IMAGE_EXTENSIONS: 153 for fs_object in Path(chart_dir).glob(f"*.{ext}"): 154 if fs_object.is_file(): 155 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.
37def fill_between_plot(data: DataFrame, **kwargs: Unpack[FillBetweenKwargs]) -> Axes: 38 """Plot a filled region between lower and upper bounds. 39 40 Args: 41 data: DataFrame - A two-column DataFrame with PeriodIndex. 42 The first column is the lower bound, the second is the upper bound. 43 kwargs: FillBetweenKwargs - keyword arguments for the plot. 44 45 Returns: 46 Axes - matplotlib Axes object. 47 48 Raises: 49 TypeError: If data is not a DataFrame. 50 ValueError: If data does not have exactly two columns. 51 52 """ 53 # --- validate inputs 54 report_kwargs(caller=ME, **kwargs) 55 validate_kwargs(schema=FillBetweenKwargs, caller=ME, **kwargs) 56 57 if not isinstance(data, DataFrame): 58 raise TypeError(f"data must be a DataFrame for {ME}()") 59 60 if len(data.columns) != REQUIRED_COLUMNS: 61 raise ValueError(f"data must have exactly two columns for {ME}(), got {len(data.columns)}") 62 63 # --- check and constrain data 64 data = check_clean_timeseries(data, ME) 65 data, kwargs_d = constrain_data(data, **kwargs) 66 67 # --- handle PeriodIndex conversion 68 saved_pi = map_periodindex(data) 69 if saved_pi is not None: 70 data = saved_pi[0] 71 72 # --- get axes 73 axes, kwargs_d = get_axes(**kwargs_d) 74 75 if data.empty or data.isna().all().all(): 76 print(f"Warning: No data to plot in {ME}().") 77 return axes 78 79 # --- extract bounds 80 lower = data.iloc[:, 0] 81 upper = data.iloc[:, 1] 82 83 # --- extract plot arguments 84 color = kwargs_d.get("color", DEFAULT_COLOR) 85 alpha = kwargs_d.get("alpha", DEFAULT_ALPHA) 86 label = kwargs_d.get("label", None) 87 linewidth = kwargs_d.get("linewidth", 0) 88 edgecolor = kwargs_d.get("edgecolor", None) 89 zorder = kwargs_d.get("zorder", None) 90 91 # --- plot 92 axes.fill_between( 93 data.index, 94 lower, 95 upper, 96 color=color, 97 alpha=alpha, 98 label=label, 99 linewidth=linewidth, 100 edgecolor=edgecolor, 101 zorder=zorder, 102 ) 103 104 # --- set axis labels 105 if saved_pi is not None: 106 set_labels( 107 axes, 108 saved_pi[1], 109 kwargs_d.get("max_ticks", get_setting("max_ticks")), 110 tick_relabel=kwargs_d.get("tick_relabel"), 111 ) 112 113 return axes
Plot a filled region between lower and upper bounds.
Args: data: DataFrame - A two-column DataFrame with PeriodIndex. The first column is the lower bound, the second is the upper bound. kwargs: FillBetweenKwargs - keyword arguments for the plot.
Returns: Axes - matplotlib Axes object.
Raises: TypeError: If data is not a DataFrame. ValueError: If data does not have exactly two columns.
165def fill_between_plot_finalise( 166 data: DataFrame, 167 **kwargs: Unpack[FBPFKwargs], 168) -> None: 169 """Call fill_between_plot() and finalise_plot(). 170 171 Args: 172 data: DataFrame with two columns (lower bound, upper bound). 173 kwargs: Combined fill_between plot and finalise plot keyword arguments. 174 175 """ 176 validate_kwargs(schema=FBPFKwargs, caller="fill_between_plot_finalise", **kwargs) 177 kwargs = impose_legend(kwargs=kwargs, data=data) 178 plot_then_finalise( 179 data, 180 function=fill_between_plot, 181 **kwargs, 182 )
Call fill_between_plot() and finalise_plot().
Args: data: DataFrame with two columns (lower bound, upper bound). kwargs: Combined fill_between plot and finalise plot keyword arguments.
599def finalise_plot(axes: Axes, **kwargs: Unpack[FinaliseKwargs]) -> None: 600 """Finalise and save plots to the file system. 601 602 The filename for the saved plot is constructed from the global 603 chart_dir, the plot's title, any specified tag text, and the 604 file_type for the plot. 605 606 Args: 607 axes: Axes - matplotlib axes object - required 608 kwargs: FinaliseKwargs 609 610 """ 611 # --- check the kwargs 612 report_kwargs(caller=ME, **kwargs) 613 validate_kwargs(schema=FinaliseKwargs, caller=ME, **kwargs) 614 615 # --- sanity checks 616 if len(axes.get_children()) < 1: 617 print(f"Warning: {ME}() called with an empty axes, which was ignored.") 618 return 619 620 # --- remember axis-limits should we need to restore thems 621 xlim, ylim = axes.get_xlim(), axes.get_ylim() 622 623 # margins 624 axes.margins(DEFAULT_MARGIN) 625 axes.autoscale(tight=False) # This is problematic ... 626 627 apply_kwargs(axes, **kwargs) 628 629 # tight layout and save the figure 630 fig = axes.figure 631 axes_only = kwargs.get("axes_only", False) 632 if not axes_only and (suptitle := kwargs.get("suptitle")): 633 fig.suptitle(suptitle) 634 if kwargs.get("preserve_lims"): 635 # restore the original limits of the axes 636 axes.set_xlim(xlim) 637 axes.set_ylim(ylim) 638 if not axes_only and not isinstance(fig, SubFigure): 639 fig.tight_layout(pad=TIGHT_LAYOUT_PAD) 640 apply_late_kwargs(axes, **kwargs) 641 # axvspan/axvline in late_kwargs may have widened xlim beyond what 642 # set_labels() last saw; regenerate ticks from the updated view. 643 refresh_period_labels(axes) 644 # de-collide end-of-line annotations now that the layout/limits are final 645 resolve_annotation_collisions(axes) 646 legend = axes.get_legend() 647 if legend and kwargs.get("remove_legend", False): 648 legend.remove() 649 if not axes_only and not isinstance(fig, SubFigure): 650 save_to_file(fig, **kwargs) 651 652 # show the plot in Jupyter Lab 653 if not axes_only and kwargs.get("show"): 654 plt.show() 655 656 # And close - the figure this axes belongs to, not pyplot's current figure 657 if not axes_only and not kwargs.get("dont_close", False): 658 root = fig 659 while isinstance(root, SubFigure): 660 root = root.figure 661 plt.close(root)
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.
104def get_setting(setting: str) -> Any: 105 """Get a setting from the global settings. 106 107 Args: 108 setting: str - name of the setting to get. 109 110 Raises: 111 KeyError: if the setting is not found 112 113 Returns: 114 value: Any - the value of the setting 115 116 """ 117 if setting not in get_fields(): 118 raise KeyError(f"Setting '{setting}' not found in mgplot_defaults.") 119 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
166def growth_plot( 167 data: DataT, 168 **kwargs: Unpack[GrowthKwargs], 169) -> Axes: 170 """Plot annual growth and periodic growth on the same axes. 171 172 Args: 173 data: A pandas DataFrame with two columns: 174 kwargs: GrowthKwargs 175 176 Returns: 177 axes: The matplotlib Axes object. 178 179 Raises: 180 TypeError if the data is not a 2-column DataFrame. 181 TypeError if the annual index is not a PeriodIndex. 182 ValueError if the annual and periodic series do not have the same index. 183 184 """ 185 # --- check the kwargs 186 me = "growth_plot" 187 report_kwargs(caller=me, **kwargs) 188 validate_kwargs(GrowthKwargs, caller=me, **kwargs) 189 190 # --- data checks 191 data = check_clean_timeseries(data, me) 192 if len(data.columns) != TWO_COLUMNS: 193 raise TypeError("The data argument must be a pandas DataFrame with two columns") 194 data, kwargsd = constrain_data(data, **kwargs) 195 196 # --- get the series of interest ... 197 annual = data[data.columns[0]] 198 periodic = data[data.columns[1]] 199 200 # --- series names 201 annual.name = "Annual Growth" 202 freq = PeriodIndex(periodic.index).freqstr 203 if freq and freq[0] in FREQUENCY_TO_NAME: 204 periodic.name = FREQUENCY_TO_NAME[freq[0]] + " Growth" 205 else: 206 periodic.name = "Periodic Growth" 207 208 # --- convert PeriodIndex periodic growth data to integer indexed data. 209 saved_pi = map_periodindex(periodic) 210 if saved_pi is not None: 211 periodic = saved_pi[0] # extract the reindexed DataFrame 212 213 # --- simple bar chart for the periodic growth 214 if "bar_anno_color" not in kwargsd or kwargsd["bar_anno_color"] is None: 215 kwargsd["bar_anno_color"] = "black" if kwargsd.get("above", False) else "white" 216 selected = package_kwargs(to_bar_plot, **kwargsd) 217 axes = bar_plot(periodic, **selected) 218 219 # --- and now the annual growth as a line 220 selected = package_kwargs(to_line_plot, **kwargsd) 221 line_plot(annual, ax=axes, **selected) 222 223 # --- fix the x-axis labels 224 if saved_pi is not None: 225 set_labels( 226 axes, 227 saved_pi[1], 228 kwargsd.get("max_ticks", 10), 229 tick_relabel=kwargsd.get("tick_relabel"), 230 ) 231 232 # --- and done ... 233 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.
185def growth_plot_finalise(data: DataT, **kwargs: Unpack[GrowthPFKwargs]) -> None: 186 """Call growth_plot() and finalise_plot(). 187 188 Args: 189 data: The growth data to be plotted. 190 kwargs: Combined growth plot and finalise plot keyword arguments. 191 192 Note: 193 Use this when you are providing the raw growth data. Don't forget to 194 set the ylabel in kwargs. 195 196 """ 197 validate_kwargs(schema=GrowthPFKwargs, caller="growth_plot_finalise", **kwargs) 198 kwargs = impose_legend(kwargs=kwargs, force=True) 199 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.
173def line_plot(data: DataT, **kwargs: Unpack[LineKwargs]) -> Axes: 174 """Build a single or multi-line plot. 175 176 Args: 177 data: DataFrame | Series - data to plot 178 kwargs: LineKwargs - keyword arguments for the line plot 179 180 The annotate key labels the right-hand end of each line. It takes either a 181 flag or a string, as a scalar broadcast to every series or as a per-series 182 sequence: 183 annotate=True - label with the end-point value (default 184 behaviour) 185 annotate="26Q2" - label every series with that text 186 annotate=["26Q2", "26Q3"] - label each series with its own text 187 annotate=False (or "") - no label 188 A string label is printed as given, so rounding has no effect on it; 189 annotate_color still applies and remains the way to colour the label. 190 191 Returns: 192 - axes: Axes - the axes object for the plot 193 194 """ 195 # --- check the kwargs 196 report_kwargs(caller=ME, **kwargs) 197 validate_kwargs(schema=LineKwargs, caller=ME, **kwargs) 198 199 # --- check the data 200 data = check_clean_timeseries(data, ME) 201 df = DataFrame(data) # we are only plotting DataFrames 202 df, kwargs_d = constrain_data(df, **kwargs) 203 204 # --- convert PeriodIndex to Integer Index 205 saved_pi = map_periodindex(df) 206 if saved_pi is not None: 207 df = saved_pi[0] 208 209 if isinstance(df.index, PeriodIndex): 210 print("Internal error: data is still a PeriodIndex - come back here and fix it") 211 212 # --- Let's plot 213 axes, kwargs_d = get_axes(**kwargs_d) # get the axes to plot on 214 if df.empty or df.isna().all().all(): 215 # Note: finalise plot should ignore an empty axes object 216 print(f"Warning: No data to plot in {ME}().") 217 return axes 218 219 # --- get the arguments for each line we will plot ... 220 item_count = len(df.columns) 221 num_data_points = len(df) 222 swce, kwargs_d = get_style_width_color_etc(item_count, num_data_points, **kwargs_d) 223 224 drawn_lines: list[Line2D] = [] # every data line - obstacles for label de-collision 225 annotations: list[tuple[Text, Line2D | None]] = [] # (label, the line it annotates) 226 for i, column in enumerate(df.columns): 227 series = df[column] 228 series = series.dropna() if "dropna" in swce and swce["dropna"][i] else series 229 if series.empty or series.isna().all(): 230 print(f"Warning: No data to plot for {column} in line_plot().") 231 continue 232 233 lines = axes.plot( 234 # using matplotlib, as pandas can set xlabel/ylabel 235 series.index, # x 236 series, # y 237 ls=swce["style"][i], 238 lw=swce["width"][i], 239 color=swce["color"][i], 240 alpha=swce["alpha"][i], 241 marker=swce["marker"][i], 242 ms=swce["markersize"][i], 243 drawstyle=swce["drawstyle"][i], 244 zorder=swce["zorder"][i], 245 label=(column if "label_series" in swce and swce["label_series"][i] else f"_{column}_"), 246 ) 247 drawn_lines.extend(lines) 248 249 if swce["annotate"][i] is None or not swce["annotate"][i]: 250 continue 251 252 color = swce["color"][i] if swce["annotate_color"][i] is True else swce["annotate_color"][i] 253 text = annotate_series( 254 series, 255 axes, 256 color=color, 257 rounding=swce["rounding"][i], 258 fontsize=swce["fontsize"][i], 259 fontname=swce["fontname"][i], 260 rotation=swce["rotation"][i], 261 text=swce["annotate"][i], 262 ) 263 if text is not None: 264 annotations.append((text, lines[0] if lines else None)) 265 266 # --- register annotations so finalise_plot() can de-collide them 267 if annotations: 268 register_annotations( 269 axes, 270 annotations, 271 drawn_lines, 272 AnnotationOptions( 273 near_end=kwargs_d.get("near_end", DEFAULT_NEAR_END), 274 force_right=bool(kwargs_d.get("force_right", False)), 275 leader_lines=bool(kwargs_d.get("leader_lines", False)), 276 ), 277 ) 278 279 # --- set the labels 280 if saved_pi is not None: 281 set_labels( 282 axes, 283 saved_pi[1], 284 kwargs_d.get("max_ticks", get_setting("max_ticks")), 285 tick_relabel=kwargs_d.get("tick_relabel"), 286 ) 287 288 return axes
Build a single or multi-line plot.
Args: data: DataFrame | Series - data to plot kwargs: LineKwargs - keyword arguments for the line plot
The annotate key labels the right-hand end of each line. It takes either a flag or a string, as a scalar broadcast to every series or as a per-series sequence: annotate=True - label with the end-point value (default behaviour) annotate="26Q2" - label every series with that text annotate=["26Q2", "26Q3"] - label each series with its own text annotate=False (or "") - no label A string label is printed as given, so rounding has no effect on it; annotate_color still applies and remains the way to colour the label.
Returns:
- axes: Axes - the axes object for the plot
202def line_plot_finalise( 203 data: DataT, 204 **kwargs: Unpack[LPFKwargs], 205) -> None: 206 """Call line_plot() then finalise_plot(). 207 208 Args: 209 data: The data to be plotted. 210 kwargs: Combined line plot and finalise plot keyword arguments. 211 212 """ 213 validate_kwargs(schema=LPFKwargs, caller="line_plot_finalise", **kwargs) 214 kwargs = impose_legend(kwargs=kwargs, data=data) 215 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.
275def multi_column( 276 data: DataFrame, 277 function: Callable | list[Callable], 278 **kwargs: Any, 279) -> None: 280 """Create multiple plots, one for each column in a DataFrame. 281 282 Args: 283 data: DataFrame - The data to be plotted. 284 function: Callable | list[Callable] - The plotting function(s) to be used. 285 kwargs: Any - Additional keyword arguments passed to plotting functions. 286 287 Returns: 288 None 289 290 Raises: 291 TypeError: If data is not a DataFrame. 292 ValueError: If DataFrame is empty or has no columns. 293 294 Note: 295 The plot title will be kwargs["title"] plus the column name. 296 297 """ 298 # --- sanity checks 299 me = "multi_column" 300 report_kwargs(caller=me, **kwargs) 301 if not isinstance(data, DataFrame): 302 raise TypeError("data must be a pandas DataFrame for multi_column()") 303 if data.empty: 304 raise ValueError("DataFrame cannot be empty") 305 if len(data.columns) == 0: 306 raise ValueError("DataFrame must have at least one column") 307 308 # --- check the function argument 309 title_stem = kwargs.get("title", "") 310 tag: Final[str] = kwargs.get("tag", "") 311 first, kwargs["function"] = first_unchain(function) 312 if not kwargs["function"]: 313 del kwargs["function"] # remove the function key if it is empty 314 315 # --- iterate over the columns 316 for i, col in enumerate(data.columns): 317 series = data[col] # Extract as Series, not single-column DataFrame 318 kwargs["title"] = f"{title_stem}{col}" if title_stem else str(col) 319 kwargs["tag"] = _generate_tag(tag, i) 320 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.
217def multi_start( 218 data: DataT, 219 function: Callable | list[Callable], 220 starts: Iterable[Period | int | None], 221 **kwargs: Any, 222) -> None: 223 """Create multiple plots with different starting points. 224 225 Args: 226 data: Series | DataFrame - The data to be plotted. 227 function: Callable | list[Callable] - desired plotting function(s). 228 starts: Iterable[Period | int | None] - The starting points for each plot. 229 kwargs: Any - Additional keyword arguments passed to plotting functions. 230 231 Returns: 232 None 233 234 Raises: 235 TypeError: If starts is not an iterable of None, Period or int. 236 ValueError: If starts contains invalid values or is empty. 237 238 Note: 239 kwargs['tag'] is used to create a unique tag for each plot. 240 241 """ 242 # --- sanity checks 243 me = "multi_start" 244 report_kwargs(caller=me, **kwargs) 245 if not isinstance(starts, Iterable): 246 raise TypeError("starts must be an iterable of None, Period or int") 247 248 # Convert to list to validate contents and check if empty 249 starts_list = list(starts) 250 if not starts_list: 251 raise ValueError("starts cannot be empty") 252 253 # Validate each start value 254 for i, start in enumerate(starts_list): 255 if start is not None and not isinstance(start, (Period, int)): 256 raise TypeError( 257 f"Start value at index {i} must be None, Period, or int, got {type(start).__name__}" 258 ) 259 260 # --- check the function argument 261 original_tag: Final[str] = kwargs.get("tag", "") 262 first, kwargs["function"] = first_unchain(function) 263 if not kwargs["function"]: 264 del kwargs["function"] # remove the function key if it is empty 265 266 # --- iterate over the starts 267 for i, start in enumerate(starts_list): 268 kw = kwargs.copy() # copy to avoid modifying the original kwargs 269 this_tag = _generate_tag(original_tag, i) 270 kw["tag"] = this_tag 271 kw["plot_from"] = start # rely on plotting function to constrain the data 272 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.
154def plot_then_finalise( 155 data: DataT, 156 function: Callable | list[Callable], 157 **kwargs: Any, 158) -> None: 159 """Chain a plotting function with the finalise_plot() function. 160 161 Args: 162 data: Series | DataFrame - The data to be plotted. 163 function: Callable | list[Callable] - the desired plotting function(s). 164 kwargs: Any - Additional keyword arguments. 165 166 Returns None. 167 168 """ 169 # --- checks 170 me = "plot_then_finalise" 171 report_kwargs(caller=me, **kwargs) 172 # validate once we have established the first function 173 174 # data is not checked here, assume it is checked by the called 175 # plot function. 176 177 first, kwargs["function"] = first_unchain(function) 178 if not kwargs["function"]: 179 del kwargs["function"] # remove the function key if it is empty 180 181 # Check that forbidden functions are not called first 182 if hasattr(first, "__name__") and first.__name__ in FORBIDDEN_FIRST_FUNCTIONS: 183 raise ValueError( 184 f"Function '{first.__name__}' should not be called by {me}. Call it before calling {me}." 185 ) 186 187 if first in EXPECTED_CALLABLES: 188 expected = EXPECTED_CALLABLES[first] 189 plot_kwargs = limit_kwargs(expected, **kwargs) 190 else: 191 # this is an unexpected Callable, so we will give it a try 192 print(f"Unknown proposed function: {first}; nonetheless, will give it a try.") 193 expected = BaseKwargs 194 plot_kwargs = kwargs.copy() 195 196 # --- validate the original kwargs (could not do before now) 197 kw_types = ( 198 # combine the expected kwargs types with the finalise kwargs types 199 dict(cast("dict[str, Any]", expected.__annotations__)) 200 | dict(cast("dict[str, Any]", FinaliseKwargs.__annotations__)) 201 ) 202 validate_kwargs(schema=kw_types, caller=me, **kwargs) 203 204 # --- call the first function with the data and selected plot kwargs 205 axes = first(data, **plot_kwargs) 206 207 # --- prepare finalise kwargs (remove overlapping arguments) 208 fp_kwargs = limit_kwargs(FinaliseKwargs, **kwargs) 209 # Remove any arguments that were already used in the plot function 210 used_plot_args = set(plot_kwargs.keys()) 211 fp_kwargs = {k: v for k, v in fp_kwargs.items() if k not in used_plot_args} 212 213 # --- finalise the plot 214 finalise_plot(axes, **fp_kwargs)
Chain a plotting function with the finalise_plot() function.
Args: data: Series | DataFrame - The data to be plotted. function: Callable | list[Callable] - the desired plotting function(s). kwargs: Any - Additional keyword arguments.
Returns None.
132def postcovid_plot(data: DataT, **kwargs: Unpack[PostcovidKwargs]) -> Axes: 133 """Plot a series with a PeriodIndex, including a post-COVID projection. 134 135 Args: 136 data: Series - the series to be plotted. 137 kwargs: PostcovidKwargs - plotting arguments. 138 139 Raises: 140 TypeError if series is not a pandas Series 141 TypeError if series does not have a PeriodIndex 142 ValueError if series does not have a D, M or Q frequency 143 ValueError if regression start is after regression end 144 145 """ 146 147 # --- failure 148 def failure() -> Axes: 149 print("postcovid_plot(): plotting the raw data only.") 150 remove: list[Literal["plot_from", "start_r", "end_r"]] = ["plot_from", "start_r", "end_r"] 151 for key in remove: 152 kwargs.pop(key, None) 153 return line_plot( 154 data, 155 **cast("LineKwargs", kwargs), 156 ) 157 158 # --- check the kwargs 159 report_kwargs(caller=ME, **kwargs) 160 validate_kwargs(schema=PostcovidKwargs, caller=ME, **kwargs) 161 162 # --- check the data 163 data = check_clean_timeseries(data, ME) 164 if not isinstance(data, Series): 165 raise TypeError("The series argument must be a pandas Series") 166 167 # --- rely on line_plot() to validate kwargs, but remove any that are not relevant 168 if "plot_from" in kwargs: 169 print("Warning: the 'plot_from' argument is ignored in postcovid_plot().") 170 kwargs.pop("plot_from", None) 171 172 # --- set the regression period 173 start_r, end_r, robust = regression_period(data, **kwargs) 174 kwargs.pop("start_r", None) # remove from kwargs to avoid confusion 175 kwargs.pop("end_r", None) # remove from kwargs to avoid confusion 176 if not robust: 177 return failure() 178 179 # --- combine data and projection 180 if start_r < data.dropna().index.min(): 181 print(f"Caution: Regression start period pre-dates the series index: {start_r=}") 182 recent_data = data[data.index >= start_r].copy() 183 recent_data.name = "Series" 184 projection_data = get_projection(recent_data, end_r) 185 if projection_data.empty: 186 return failure() 187 projection_data.name = "Pre-COVID projection" 188 189 # --- Create DataFrame with proper column alignment 190 combined_data = DataFrame( 191 { 192 projection_data.name: projection_data, 193 recent_data.name: recent_data, 194 } 195 ) 196 197 # --- activate plot settings 198 kwargs["width"] = kwargs.pop( 199 "width", 200 (get_setting("line_normal"), get_setting("line_wide")), 201 ) # series line is thicker than projection 202 kwargs["style"] = kwargs.pop("style", ("--", "-")) # dashed regression line 203 kwargs["label_series"] = kwargs.pop("label_series", True) 204 kwargs["annotate"] = kwargs.pop("annotate", (False, True)) # annotate series only 205 kwargs["color"] = kwargs.pop("color", ("darkblue", "#dd0000")) 206 kwargs["dropna"] = kwargs.pop("dropna", False) # drop NaN values 207 208 return line_plot( 209 combined_data, 210 **cast("LineKwargs", kwargs), 211 )
Plot a series with a PeriodIndex, including a post-COVID projection.
Args: data: Series - the series to be plotted. kwargs: PostcovidKwargs - plotting arguments.
Raises: TypeError if series is not a pandas Series TypeError if series does not have a PeriodIndex ValueError if series does not have a D, M or Q frequency ValueError if regression start is after regression end
218def postcovid_plot_finalise( 219 data: DataT, 220 **kwargs: Unpack[PCFKwargs], 221) -> None: 222 """Call postcovid_plot() and finalise_plot(). 223 224 Args: 225 data: The data to be plotted. 226 kwargs: Combined postcovid plot and finalise plot keyword arguments. 227 228 """ 229 validate_kwargs(schema=PCFKwargs, caller="postcovid_plot_finalise", **kwargs) 230 kwargs = impose_legend(kwargs=kwargs, force=True) 231 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.
22def revision_plot(data: DataT, **kwargs: Unpack[LineKwargs]) -> Axes: 23 """Plot the revisions to ABS data. 24 25 Args: 26 data: DataFrame - the data to plot, with a column for each data revision. 27 Must have at least 2 columns to show meaningful revision comparisons. 28 kwargs: LineKwargs - additional keyword arguments for the line_plot function. 29 30 Returns: 31 Axes: A matplotlib Axes object containing the revision plot. 32 33 Raises: 34 TypeError: If data is not a DataFrame. 35 ValueError: If DataFrame has fewer than 2 columns for revision comparison. 36 37 """ 38 # --- check the kwargs and data 39 report_kwargs(caller=ME, **kwargs) 40 validate_kwargs(schema=LineKwargs, caller=ME, **kwargs) 41 data = check_clean_timeseries(data, ME) 42 43 # --- additional checks 44 if not isinstance(data, DataFrame): 45 print(f"{ME}() requires a DataFrame with columns for each revision, not a Series or any other type.") 46 raise TypeError(f"{ME}() requires a DataFrame, got {type(data).__name__}") 47 48 if data.shape[1] < MIN_REVISION_COLUMNS: 49 raise ValueError( 50 f"{ME}() requires at least {MIN_REVISION_COLUMNS} columns for revision comparison, " 51 f"but got {data.shape[1]} columns" 52 ) 53 54 # --- set defaults for revision visualization 55 kwargs["plot_from"] = kwargs.get("plot_from", DEFAULT_PLOT_FROM) 56 kwargs["annotate"] = kwargs.get("annotate", True) 57 kwargs["annotate_color"] = kwargs.get("annotate_color", "black") 58 kwargs["rounding"] = kwargs.get("rounding", 3) 59 kwargs["near_end"] = kwargs.get("near_end", DEFAULT_NEAR_END) 60 61 # --- plot 62 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.
234def revision_plot_finalise( 235 data: DataT, 236 **kwargs: Unpack[RevPFKwargs], 237) -> None: 238 """Call revision_plot() and finalise_plot(). 239 240 Args: 241 data: The revision data to be plotted. 242 kwargs: Combined revision plot and finalise plot keyword arguments. 243 244 """ 245 validate_kwargs(schema=RevPFKwargs, caller="revision_plot_finalise", **kwargs) 246 kwargs = impose_legend(kwargs=kwargs, force=True) 247 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.
162def run_plot(data: DataT, **kwargs: Unpack[RunKwargs]) -> Axes: 163 """Plot a series of percentage rates, highlighting the increasing runs. 164 165 Arguments: 166 data: Series - ordered pandas Series of percentages, with PeriodIndex. 167 kwargs: RunKwargs - keyword arguments for the run_plot function. 168 169 Return: 170 - matplotlib Axes object 171 172 """ 173 # --- validate inputs 174 report_kwargs(caller=ME, **kwargs) 175 validate_kwargs(schema=RunKwargs, caller=ME, **kwargs) 176 177 series = check_clean_timeseries(data, ME) 178 if not isinstance(series, Series): 179 raise TypeError("series must be a pandas Series for run_plot()") 180 series, kwargs_d = constrain_data(series, **kwargs) 181 182 # --- configure defaults and validate 183 direction = kwargs_d.get("direction", "both") 184 _configure_defaults(kwargs_d, direction) 185 186 threshold = kwargs_d["threshold"] 187 if threshold <= 0: 188 raise ValueError("Threshold must be positive") 189 190 # --- handle PeriodIndex conversion 191 saved_pi = map_periodindex(series) 192 if saved_pi is not None: 193 series = saved_pi[0] 194 195 # --- plot the line 196 lp_kwargs = limit_kwargs(LineKwargs, **kwargs_d) 197 axes = line_plot(series, **lp_kwargs) 198 199 # --- plot runs based on direction 200 run_label = kwargs_d.pop("highlight_label", None) 201 up_label, down_label = _resolve_labels(run_label, direction) 202 203 if direction in ("up", "both"): 204 _plot_runs(axes, series, run_label=up_label, up=True, **kwargs_d) 205 if direction in ("down", "both"): 206 _plot_runs(axes, series, run_label=down_label, up=False, **kwargs_d) 207 208 if direction not in ("up", "down", "both"): 209 raise ValueError(f"Invalid direction: {direction}. Expected 'up', 'down', or 'both'.") 210 211 # --- set axis labels 212 if saved_pi is not None: 213 set_labels( 214 axes, 215 saved_pi[1], 216 kwargs.get("max_ticks", get_setting("max_ticks")), 217 tick_relabel=kwargs.get("tick_relabel"), 218 ) 219 220 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
250def run_plot_finalise( 251 data: DataT, 252 **kwargs: Unpack[RunPFKwargs], 253) -> None: 254 """Call run_plot() and finalise_plot(). 255 256 Args: 257 data: The data to be plotted. 258 kwargs: Combined run plot and finalise plot keyword arguments. 259 260 """ 261 validate_kwargs(schema=RunPFKwargs, caller="run_plot_finalise", **kwargs) 262 kwargs = impose_legend(kwargs=kwargs, force="highlight_label" in kwargs) 263 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.
152def scatter_plot(data: DataFrame, **kwargs: Unpack[ScatterKwargs]) -> Axes: 153 """Plot the second column of a DataFrame (y) against the first (x). 154 155 Args: 156 data: DataFrame - exactly two numeric columns, x first and y second. 157 The index is not plotted; it identifies the latest point. 158 kwargs: ScatterKwargs - keyword arguments for the scatter plot 159 160 The overlays - diagonal, fit and highlight_latest - each take True for the 161 house style, or a dict of matplotlib arguments merged over the house style. 162 diagonal - the y = x line (dashed, labelled "45° (equal)") 163 fit - an OLS line of best fit, in the colour of the points 164 highlight_latest - the point with the latest index value, as a star 165 report_corr=True appends the correlation to the label, e.g. "GDP (r = 0.83)". 166 167 To layer several groups (each with its own colour and fit line), make 168 repeated calls with the same ax=, then call finalise_plot(). 169 170 Returns: 171 - axes: Axes - the axes object for the plot 172 173 """ 174 # --- check the kwargs 175 report_kwargs(caller=ME, **kwargs) 176 validate_kwargs(schema=ScatterKwargs, caller=ME, **kwargs) 177 178 # --- check and prepare the data 179 df, kwargs_d = _constrain(_check_data(data), dict(kwargs)) 180 if kwargs_d.pop("dropna", True): 181 df = df.dropna() 182 183 # --- Let's plot 184 axes, kwargs_d = get_axes(**kwargs_d) 185 if df.empty or df.isna().all().all(): 186 # Note: finalise plot should ignore an empty axes object 187 print(f"Warning: No data to plot in {ME}().") 188 return axes 189 190 x, y = df.iloc[:, 0], df.iloc[:, 1] 191 points: dict[str, Any] = { 192 "color": kwargs_d.get("color", get_color_list(1)[0]), 193 "s": kwargs_d.get("size", DEFAULT_SIZE), 194 "alpha": kwargs_d.get("alpha", DEFAULT_ALPHA), 195 "marker": kwargs_d.get("marker", DEFAULT_MARKER), 196 "zorder": kwargs_d.get("zorder", DEFAULT_POINTS_ZORDER), 197 } 198 label = _points_label(kwargs_d.get("label"), x, y, report_corr=bool(kwargs_d.get("report_corr"))) 199 axes.scatter(x, y, label=label, **points) 200 201 # --- overlays 202 _draw_fit(axes, x, y, kwargs_d.get("fit"), points["color"]) 203 _draw_latest(axes, df, kwargs_d.get("highlight_latest"), points) 204 _draw_diagonal(axes, kwargs_d.get("diagonal")) # last, so its range covers the points 205 206 return axes
Plot the second column of a DataFrame (y) against the first (x).
Args: data: DataFrame - exactly two numeric columns, x first and y second. The index is not plotted; it identifies the latest point. kwargs: ScatterKwargs - keyword arguments for the scatter plot
The overlays - diagonal, fit and highlight_latest - each take True for the house style, or a dict of matplotlib arguments merged over the house style. diagonal - the y = x line (dashed, labelled "45° (equal)") fit - an OLS line of best fit, in the colour of the points highlight_latest - the point with the latest index value, as a star report_corr=True appends the correlation to the label, e.g. "GDP (r = 0.83)".
To layer several groups (each with its own colour and fit line), make repeated calls with the same ax=, then call finalise_plot().
Returns:
- axes: Axes - the axes object for the plot
266def scatter_plot_finalise( 267 data: DataFrame, 268 **kwargs: Unpack[ScatterPFKwargs], 269) -> None: 270 """Call scatter_plot() and finalise_plot(). 271 272 Args: 273 data: DataFrame with two numeric columns (x first, y second). 274 kwargs: Combined scatter plot and finalise plot keyword arguments. 275 276 Note: 277 The legend is shown by default only when something labelled is drawn. 278 279 """ 280 validate_kwargs(schema=ScatterPFKwargs, caller="scatter_plot_finalise", **kwargs) 281 labelled = any(kwargs.get(k) for k in ("label", "report_corr", "diagonal", "highlight_latest")) 282 kwargs = impose_legend(kwargs=kwargs, force=labelled) 283 plot_then_finalise(data=data, function=scatter_plot, **kwargs)
Call scatter_plot() and finalise_plot().
Args: data: DataFrame with two numeric columns (x first, y second). kwargs: Combined scatter plot and finalise plot keyword arguments.
Note: The legend is shown by default only when something labelled is drawn.
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
286def seastrend_plot_finalise( 287 data: DataT, 288 **kwargs: Unpack[SFKwargs], 289) -> None: 290 """Call seastrend_plot() and finalise_plot(). 291 292 Args: 293 data: The seasonal and trend data to be plotted. 294 kwargs: Combined seastrend plot and finalise plot keyword arguments. 295 296 """ 297 validate_kwargs(schema=SFKwargs, caller="seastrend_plot_finalise", **kwargs) 298 kwargs = impose_legend(kwargs=kwargs, force=True) 299 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.
236def series_growth_plot( 237 data: DataT, 238 **kwargs: Unpack[SeriesGrowthKwargs], 239) -> Axes: 240 """Plot annual and periodic growth in percentage terms from a pandas Series. 241 242 Args: 243 data: A pandas Series with an appropriate PeriodIndex. 244 kwargs: SeriesGrowthKwargs 245 246 """ 247 # --- check the kwargs 248 me = "series_growth_plot" 249 report_kwargs(caller=me, **kwargs) 250 validate_kwargs(SeriesGrowthKwargs, caller=me, **kwargs) 251 252 # --- sanity checks 253 if not isinstance(data, Series): 254 raise TypeError("The data argument to series_growth_plot() must be a pandas Series") 255 256 # --- calculate growth and plot - add ylabel 257 ylabel: str | None = kwargs.pop("ylabel", None) 258 if ylabel is not None: 259 print(f"Did you intend to specify a value for the 'ylabel' in {me}()?") 260 ylabel = "Growth (%)" if ylabel is None else ylabel 261 growth = calc_growth(data) 262 ax = growth_plot(growth, **cast("GrowthKwargs", kwargs)) 263 ax.set_ylabel(ylabel) 264 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
302def series_growth_plot_finalise(data: DataT, **kwargs: Unpack[SGFPKwargs]) -> None: 303 """Call series_growth_plot() and finalise_plot(). 304 305 Args: 306 data: The series data to calculate and plot growth for. 307 kwargs: Combined series growth plot and finalise plot keyword arguments. 308 309 """ 310 validate_kwargs(schema=SGFPKwargs, caller="series_growth_plot_finalise", **kwargs) 311 kwargs = impose_legend(kwargs=kwargs, force=True) 312 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.
158def set_chart_dir(chart_dir: str) -> None: 159 """Set a global chart directory for finalise_plot(). 160 161 Args: 162 chart_dir: str - the directory to set as the chart directory 163 164 Note: Path.mkdir() may raise an exception if a directory cannot be created. 165 166 Note: This is a wrapper for set_setting() to set the chart_dir setting, and 167 create the directory if it does not exist. 168 169 """ 170 if not chart_dir or chart_dir.isspace(): 171 chart_dir = DEFAULT_CHART_DIR # avoid empty/whitespace strings 172 Path(chart_dir).mkdir(parents=True, exist_ok=True) 173 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.
122def set_setting(setting: str, value: Any) -> None: 123 """Set a setting in the global settings. 124 125 Args: 126 setting: str - name of the setting to set (see get_setting()) 127 value: Any - the value to set the setting to 128 129 Raises: 130 KeyError: if the setting is not found 131 ValueError: if the value is invalid for the setting 132 133 """ 134 if setting not in get_fields(): 135 raise KeyError(f"Setting '{setting}' not found in mgplot_defaults.") 136 137 # Basic validation for some settings 138 if setting == "chart_dir" and not isinstance(value, str): 139 raise ValueError(f"chart_dir must be a string, got {type(value)}") 140 if setting == "dpi" and (not isinstance(value, int) or value <= 0): 141 raise ValueError(f"dpi must be a positive integer, got {value}") 142 if setting == "max_ticks" and (not isinstance(value, int) or value <= 0): 143 raise ValueError(f"max_ticks must be a positive integer, got {value}") 144 145 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
298def summary_plot(data: DataT, **kwargs: Unpack[SummaryKwargs]) -> Axes: 299 """Plot a summary of historical data for a given DataFrame. 300 301 Args: 302 data: DataFrame containing the summary data. The column names are 303 used as labels for the plot. 304 kwargs: Additional arguments for the plot, including middle (float), 305 plot_type (str), verbose (bool), and standard plotting options. 306 307 Returns: 308 Axes: A matplotlib Axes object containing the summary plot. 309 310 Raises: 311 TypeError: If data is not a DataFrame. 312 313 """ 314 # --- check the kwargs 315 report_kwargs(caller=ME, **kwargs) 316 validate_kwargs(schema=SummaryKwargs, caller=ME, **kwargs) 317 318 # --- check the data 319 data = check_clean_timeseries(data, ME) 320 if not isinstance(data, DataFrame): 321 raise TypeError("data must be a pandas DataFrame for summary_plot()") 322 323 # --- legend 324 kwargs["legend"] = kwargs.get( 325 "legend", 326 { 327 # put the legend below the x-axis label 328 "loc": "upper center", 329 "fontsize": "xx-small", 330 "bbox_to_anchor": (0.5, -0.125), 331 "ncol": 4, 332 }, 333 ) 334 335 # --- and plot it ... 336 ax, plot_type = plot_the_data(data, **kwargs) 337 label_x_axis( 338 kwargs.get("plot_from", DEFAULT_PLOT_FROM), 339 label=kwargs.get("xlabel", ""), 340 plot_type=plot_type, 341 ax=ax, 342 df=data, 343 ) 344 mark_reference_lines(plot_type, ax) 345 346 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.
315def summary_plot_finalise( 316 data: DataT, 317 **kwargs: Unpack[SumPFKwargs], 318) -> None: 319 """Call summary_plot() and finalise_plot(). 320 321 This is more complex than most of the above convenience methods as it 322 creates multiple plots (one for each plot type). 323 324 Args: 325 data: DataFrame containing the summary data. The index must be a PeriodIndex. 326 kwargs: Combined summary plot and finalise plot keyword arguments. 327 328 Raises: 329 TypeError: If data is not a DataFrame with a PeriodIndex. 330 IndexError: If DataFrame is empty. 331 332 """ 333 # --- validate data type and structure 334 if not isinstance(data, DataFrame) or not isinstance(data.index, PeriodIndex): 335 raise TypeError("Data must be a DataFrame with a PeriodIndex.") 336 337 if data.empty or len(data.index) == 0: 338 raise ValueError("DataFrame cannot be empty") 339 340 validate_kwargs(schema=SumPFKwargs, caller="summary_plot_finalise", **kwargs) 341 342 # --- set default title with bounds checking 343 kwargs["title"] = kwargs.get("title", f"Summary at {label_period(data.index[-1])}") 344 kwargs["preserve_lims"] = kwargs.get("preserve_lims", True) 345 346 # --- handle plot_from parameter with bounds checking 347 start: int | Period | None = kwargs.get("plot_from", 0) 348 if start is None: 349 start = data.index[0] 350 elif isinstance(start, int): 351 if abs(start) >= len(data.index): 352 raise IndexError( 353 f"plot_from index {start} out of range for DataFrame with {len(data.index)} rows" 354 ) 355 start = data.index[start] 356 357 kwargs["plot_from"] = start 358 if not isinstance(start, Period): 359 raise TypeError("plot_from must be a Period or convertible to one") 360 361 # --- create plots for each plot type 362 pre_tag: str = kwargs.get("pre_tag", "") 363 for plot_type in SUMMARY_PLOT_TYPES: 364 plot_kwargs = kwargs.copy() # Avoid modifying original kwargs 365 plot_kwargs["plot_type"] = plot_type 366 plot_kwargs["pre_tag"] = pre_tag + plot_type 367 368 plot_then_finalise( 369 data, 370 function=summary_plot, 371 **plot_kwargs, 372 )
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.