mgplot

Provide a frontend to matplotlib for working with timeseries data, indexed with a PeriodIndex.

This package simplifiers the creation of common plots used in economic and financial analysis, such as bar plots, line plots, growth plots, and seasonal trend plots. It also includes utilities for color management and finalising plots with consistent styling.

  1"""Provide a frontend to matplotlib for working with timeseries data, indexed with a PeriodIndex.
  2
  3This package simplifiers the creation of common plots used in economic and financial analysis,
  4such as bar plots, line plots, growth plots, and seasonal trend plots. It also includes utilities
  5for color management and finalising plots with consistent styling.
  6"""
  7
  8# --- version and author
  9import importlib.metadata
 10
 11# --- local imports
 12#    Do not import the utilities, axis_utils nor keyword_checking modules here.
 13from mgplot.bar_plot import BarKwargs, bar_plot
 14from mgplot.colors import (
 15    abbreviate_state,
 16    colorise_list,
 17    contrast,
 18    get_color,
 19    get_party_palette,
 20    state_abbrs,
 21    state_names,
 22)
 23from mgplot.fill_between_plot import FillBetweenKwargs, fill_between_plot
 24from mgplot.finalise_plot import FinaliseKwargs, finalise_plot
 25from mgplot.finalisers import (
 26    bar_plot_finalise,
 27    fill_between_plot_finalise,
 28    growth_plot_finalise,
 29    line_plot_finalise,
 30    postcovid_plot_finalise,
 31    revision_plot_finalise,
 32    run_plot_finalise,
 33    seastrend_plot_finalise,
 34    series_growth_plot_finalise,
 35    summary_plot_finalise,
 36)
 37from mgplot.growth_plot import (
 38    GrowthKwargs,
 39    SeriesGrowthKwargs,
 40    calc_growth,
 41    growth_plot,
 42    series_growth_plot,
 43)
 44from mgplot.line_plot import LineKwargs, line_plot
 45from mgplot.multi_plot import multi_column, multi_start, plot_then_finalise
 46from mgplot.postcovid_plot import PostcovidKwargs, postcovid_plot
 47from mgplot.revision_plot import revision_plot
 48from mgplot.run_plot import RunKwargs, run_plot
 49from mgplot.seastrend_plot import seastrend_plot
 50from mgplot.settings import (
 51    chart_subdir,
 52    clear_chart_dir,
 53    get_setting,
 54    set_chart_dir,
 55    set_setting,
 56)
 57from mgplot.summary_plot import SummaryKwargs, summary_plot
 58
 59# --- version and author
 60try:
 61    __version__ = importlib.metadata.version(__name__)
 62except importlib.metadata.PackageNotFoundError:
 63    __version__ = "0.0.0"  # Fallback for development mode
 64__author__ = "Bryan Palmer"
 65
 66
 67# --- public API
 68__all__ = (
 69    "BarKwargs",
 70    "FillBetweenKwargs",
 71    "FinaliseKwargs",
 72    "GrowthKwargs",
 73    "LineKwargs",
 74    "PostcovidKwargs",
 75    "RunKwargs",
 76    "SeriesGrowthKwargs",
 77    "SummaryKwargs",
 78    "__author__",
 79    "__version__",
 80    "abbreviate_state",
 81    "bar_plot",
 82    "bar_plot_finalise",
 83    "calc_growth",
 84    "chart_subdir",
 85    "clear_chart_dir",
 86    "colorise_list",
 87    "contrast",
 88    "fill_between_plot",
 89    "fill_between_plot_finalise",
 90    "finalise_plot",
 91    "get_color",
 92    "get_party_palette",
 93    "get_setting",
 94    "growth_plot",
 95    "growth_plot_finalise",
 96    "line_plot",
 97    "line_plot_finalise",
 98    "multi_column",
 99    "multi_start",
100    "plot_then_finalise",
101    "postcovid_plot",
102    "postcovid_plot_finalise",
103    "revision_plot",
104    "revision_plot_finalise",
105    "run_plot",
106    "run_plot",
107    "run_plot_finalise",
108    "seastrend_plot",
109    "seastrend_plot_finalise",
110    "series_growth_plot",
111    "series_growth_plot_finalise",
112    "set_chart_dir",
113    "set_setting",
114    "state_abbrs",
115    "state_names",
116    "summary_plot",
117    "summary_plot_finalise",
118)
class BarKwargs(mgplot.keyword_checking.BaseKwargs):
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.

ax: NotRequired[matplotlib.axes._axes.Axes | None]
stacked: NotRequired[bool]
horizontal: NotRequired[bool]
max_ticks: NotRequired[int]
tick_relabel: NotRequired[Callable[[str], str]]
plot_from: NotRequired[int | pandas.Period]
label_rotation: NotRequired[int | float]
color: NotRequired[str | Sequence[str]]
label_series: NotRequired[bool | Sequence[bool]]
width: NotRequired[float | int | Sequence[float | int]]
zorder: NotRequired[float | int | Sequence[float | int]]
annotate: NotRequired[bool]
fontsize: NotRequired[int | float | str]
fontname: NotRequired[str]
rounding: NotRequired[int]
rotation: NotRequired[int | float]
annotate_color: NotRequired[str]
above: NotRequired[bool]
class FillBetweenKwargs(mgplot.keyword_checking.BaseKwargs):
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.

ax: NotRequired[matplotlib.axes._axes.Axes | None]
color: NotRequired[str]
alpha: NotRequired[float]
label: NotRequired[str | None]
linewidth: NotRequired[float]
edgecolor: NotRequired[str | None]
zorder: NotRequired[int | float]
plot_from: NotRequired[int | None]
max_ticks: NotRequired[int]
tick_relabel: NotRequired[Callable[[str], str]]
class FinaliseKwargs(mgplot.keyword_checking.BaseKwargs):
33class FinaliseKwargs(BaseKwargs):
34    """Keyword arguments for the finalise_plot function."""
35
36    # --- value options
37    suptitle: NotRequired[str | None]
38    title: NotRequired[str | None]
39    xlabel: NotRequired[str | None]
40    ylabel: NotRequired[str | None]
41    xlim: NotRequired[tuple[float, float] | None]
42    ylim: NotRequired[tuple[float, float] | None]
43    xticks: NotRequired[list[float] | None]
44    yticks: NotRequired[list[float] | None]
45    xscale: NotRequired[str | None]
46    yscale: NotRequired[str | None]
47    # --- splat options
48    legend: NotRequired[bool | dict[str, Any] | None]
49    axhspan: NotRequired[dict[str, Any] | Sequence[dict[str, Any]] | None]
50    axvspan: NotRequired[dict[str, Any] | Sequence[dict[str, Any]] | None]
51    axhline: NotRequired[dict[str, Any] | Sequence[dict[str, Any]] | None]
52    axvline: NotRequired[dict[str, Any] | Sequence[dict[str, Any]] | None]
53    # --- options for annotations
54    lfooter: NotRequired[str]
55    rfooter: NotRequired[str]
56    lheader: NotRequired[str]
57    rheader: NotRequired[str]
58    # --- file/save options
59    pre_tag: NotRequired[str]
60    tag: NotRequired[str]
61    filename: NotRequired[str]
62    chart_dir: NotRequired[str]
63    file_type: NotRequired[str]
64    dpi: NotRequired[int]
65    figsize: NotRequired[tuple[float, float]]
66    show: NotRequired[bool]
67    # --- other options
68    preserve_lims: NotRequired[bool]
69    remove_legend: NotRequired[bool]
70    zero_y: NotRequired[bool]
71    y0: NotRequired[bool]
72    x0: NotRequired[bool]
73    axisbelow: NotRequired[bool]
74    dont_save: NotRequired[bool]
75    dont_close: NotRequired[bool]
76    axes_only: NotRequired[bool]

Keyword arguments for the finalise_plot function.

suptitle: NotRequired[str | None]
title: NotRequired[str | None]
xlabel: NotRequired[str | None]
ylabel: NotRequired[str | None]
xlim: NotRequired[tuple[float, float] | None]
ylim: NotRequired[tuple[float, float] | None]
xticks: NotRequired[list[float] | None]
yticks: NotRequired[list[float] | None]
xscale: NotRequired[str | None]
yscale: NotRequired[str | None]
legend: NotRequired[bool | dict[str, Any] | None]
axhspan: NotRequired[dict[str, Any] | Sequence[dict[str, Any]] | None]
axvspan: NotRequired[dict[str, Any] | Sequence[dict[str, Any]] | None]
axhline: NotRequired[dict[str, Any] | Sequence[dict[str, Any]] | None]
axvline: NotRequired[dict[str, Any] | Sequence[dict[str, Any]] | None]
lfooter: NotRequired[str]
rfooter: NotRequired[str]
lheader: NotRequired[str]
rheader: NotRequired[str]
pre_tag: NotRequired[str]
tag: NotRequired[str]
filename: NotRequired[str]
chart_dir: NotRequired[str]
file_type: NotRequired[str]
dpi: NotRequired[int]
figsize: NotRequired[tuple[float, float]]
show: NotRequired[bool]
preserve_lims: NotRequired[bool]
remove_legend: NotRequired[bool]
zero_y: NotRequired[bool]
y0: NotRequired[bool]
x0: NotRequired[bool]
axisbelow: NotRequired[bool]
dont_save: NotRequired[bool]
dont_close: NotRequired[bool]
axes_only: NotRequired[bool]
class GrowthKwargs(mgplot.keyword_checking.BaseKwargs):
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.

ax: NotRequired[matplotlib.axes._axes.Axes | None]
plot_from: NotRequired[int | pandas.Period]
label_series: NotRequired[bool]
max_ticks: NotRequired[int]
tick_relabel: NotRequired[Callable[[str], str]]
line_width: NotRequired[int | float]
line_color: NotRequired[str]
line_style: NotRequired[str]
annotate_line: NotRequired[bool]
line_rounding: NotRequired[bool | int]
line_fontsize: NotRequired[int | float | str]
line_fontname: NotRequired[str]
line_anno_color: NotRequired[str]
annotate_bars: NotRequired[bool]
bar_fontsize: NotRequired[int | float | str]
bar_fontname: NotRequired[str]
bar_rounding: NotRequired[int]
bar_width: NotRequired[float]
bar_color: NotRequired[str]
bar_anno_color: NotRequired[str]
bar_rotation: NotRequired[int | float]
class LineKwargs(mgplot.keyword_checking.BaseKwargs):
28class LineKwargs(BaseKwargs):
29    """Keyword arguments for the line_plot function."""
30
31    # --- options for the entire line plot
32    ax: NotRequired[Axes | None]
33    style: NotRequired[str | Sequence[str]]
34    width: NotRequired[float | int | Sequence[float | int]]
35    color: NotRequired[str | Sequence[str]]
36    alpha: NotRequired[float | Sequence[float]]
37    drawstyle: NotRequired[str | Sequence[str] | None]
38    marker: NotRequired[str | Sequence[str] | None]
39    markersize: NotRequired[float | Sequence[float] | int | None]
40    zorder: NotRequired[int | float | Sequence[int | float]]
41    dropna: NotRequired[bool | Sequence[bool]]
42    annotate: NotRequired[bool | Sequence[bool]]
43    rounding: NotRequired[Sequence[int | bool] | int | bool | None]
44    fontsize: NotRequired[Sequence[str | int | float] | str | int | float]
45    fontname: NotRequired[str | Sequence[str]]
46    rotation: NotRequired[Sequence[int | float] | int | float]
47    annotate_color: NotRequired[str | Sequence[str] | bool | Sequence[bool] | None]
48    plot_from: NotRequired[int | Period | None]
49    label_series: NotRequired[bool | Sequence[bool] | None]
50    max_ticks: NotRequired[int]
51    tick_relabel: NotRequired[Callable[[str], str]]

Keyword arguments for the line_plot function.

ax: NotRequired[matplotlib.axes._axes.Axes | None]
style: NotRequired[str | Sequence[str]]
width: NotRequired[float | int | Sequence[float | int]]
color: NotRequired[str | Sequence[str]]
alpha: NotRequired[float | Sequence[float]]
drawstyle: NotRequired[str | Sequence[str] | None]
marker: NotRequired[str | Sequence[str] | None]
markersize: NotRequired[float | Sequence[float] | int | None]
zorder: NotRequired[float | int | Sequence[float | int]]
dropna: NotRequired[bool | Sequence[bool]]
annotate: NotRequired[bool | Sequence[bool]]
rounding: NotRequired[Sequence[int | bool] | int | bool | None]
fontsize: NotRequired[Sequence[str | int | float] | str | int | float]
fontname: NotRequired[str | Sequence[str]]
rotation: NotRequired[float | int | Sequence[float | int]]
annotate_color: NotRequired[str | Sequence[str] | bool | Sequence[bool] | None]
plot_from: NotRequired[int | pandas.Period | None]
label_series: NotRequired[bool | Sequence[bool] | None]
max_ticks: NotRequired[int]
tick_relabel: NotRequired[Callable[[str], str]]
class PostcovidKwargs(mgplot.LineKwargs):
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.

start_r: NotRequired[pandas.Period]
end_r: NotRequired[pandas.Period]
class RunKwargs(mgplot.LineKwargs):
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.

threshold: NotRequired[float]
direction: NotRequired[str]
highlight_color: NotRequired[str | Sequence[str]]
highlight_label: NotRequired[str | Sequence[str]]
class SeriesGrowthKwargs(mgplot.GrowthKwargs):
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.

ylabel: NotRequired[str | None]
class SummaryKwargs(mgplot.keyword_checking.BaseKwargs):
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.

ax: NotRequired[matplotlib.axes._axes.Axes | None]
verbose: NotRequired[bool]
middle: NotRequired[float]
plot_type: NotRequired[str]
plot_from: NotRequired[int | pandas.Period]
legend: NotRequired[bool | dict[str, Any] | None]
xlabel: NotRequired[str | None]
__author__ = 'Bryan Palmer'
__version__ = '0.2.26a0'
def abbreviate_state(state: str) -> str:
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.

def bar_plot( data: ~DataT, **kwargs: Unpack[BarKwargs]) -> matplotlib.axes._axes.Axes:
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.

def bar_plot_finalise(data: ~DataT, **kwargs: Unpack[mgplot.finalisers.BPFKwargs]) -> None:
138def bar_plot_finalise(
139    data: DataT,
140    **kwargs: Unpack[BPFKwargs],
141) -> None:
142    """Call bar_plot() and finalise_plot().
143
144    Args:
145        data: The data to be plotted.
146        kwargs: Combined bar plot and finalise plot keyword arguments.
147
148    """
149    validate_kwargs(schema=BPFKwargs, caller="bar_plot_finalise", **kwargs)
150    kwargs = impose_legend(kwargs=kwargs, data=data)
151    plot_then_finalise(
152        data,
153        function=bar_plot,
154        **kwargs,
155    )

Call bar_plot() and finalise_plot().

Args: data: The data to be plotted. kwargs: Combined bar plot and finalise plot keyword arguments.

def calc_growth(series: pandas.Series) -> pandas.DataFrame:
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.

@contextmanager
def chart_subdir(name: str, *, clear: bool = False) -> Generator[str]:
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.

def clear_chart_dir() -> None:
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.

def colorise_list(party_list: Iterable[str]) -> list[str]:
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.

def contrast(orig_color: str) -> str:
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.

def fill_between_plot( data: pandas.DataFrame, **kwargs: Unpack[FillBetweenKwargs]) -> matplotlib.axes._axes.Axes:
 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.

def fill_between_plot_finalise( data: pandas.DataFrame, **kwargs: Unpack[mgplot.finalisers.FBPFKwargs]) -> None:
158def fill_between_plot_finalise(
159    data: DataFrame,
160    **kwargs: Unpack[FBPFKwargs],
161) -> None:
162    """Call fill_between_plot() and finalise_plot().
163
164    Args:
165        data: DataFrame with two columns (lower bound, upper bound).
166        kwargs: Combined fill_between plot and finalise plot keyword arguments.
167
168    """
169    validate_kwargs(schema=FBPFKwargs, caller="fill_between_plot_finalise", **kwargs)
170    kwargs = impose_legend(kwargs=kwargs, data=data)
171    plot_then_finalise(
172        data,
173        function=fill_between_plot,
174        **kwargs,
175    )

Call fill_between_plot() and finalise_plot().

Args: data: DataFrame with two columns (lower bound, upper bound). kwargs: Combined fill_between plot and finalise plot keyword arguments.

def finalise_plot( axes: matplotlib.axes._axes.Axes, **kwargs: Unpack[FinaliseKwargs]) -> None:
407def finalise_plot(axes: Axes, **kwargs: Unpack[FinaliseKwargs]) -> None:
408    """Finalise and save plots to the file system.
409
410    The filename for the saved plot is constructed from the global
411    chart_dir, the plot's title, any specified tag text, and the
412    file_type for the plot.
413
414    Args:
415        axes: Axes - matplotlib axes object - required
416        kwargs: FinaliseKwargs
417
418    """
419    # --- check the kwargs
420    report_kwargs(caller=ME, **kwargs)
421    validate_kwargs(schema=FinaliseKwargs, caller=ME, **kwargs)
422
423    # --- sanity checks
424    if len(axes.get_children()) < 1:
425        print(f"Warning: {ME}() called with an empty axes, which was ignored.")
426        return
427
428    # --- remember axis-limits should we need to restore thems
429    xlim, ylim = axes.get_xlim(), axes.get_ylim()
430
431    # margins
432    axes.margins(DEFAULT_MARGIN)
433    axes.autoscale(tight=False)  # This is problematic ...
434
435    apply_kwargs(axes, **kwargs)
436
437    # tight layout and save the figure
438    fig = axes.figure
439    axes_only = kwargs.get("axes_only", False)
440    if not axes_only and (suptitle := kwargs.get("suptitle")):
441        fig.suptitle(suptitle)
442    if kwargs.get("preserve_lims"):
443        # restore the original limits of the axes
444        axes.set_xlim(xlim)
445        axes.set_ylim(ylim)
446    if not axes_only and not isinstance(fig, SubFigure):
447        fig.tight_layout(pad=TIGHT_LAYOUT_PAD)
448    apply_late_kwargs(axes, **kwargs)
449    # axvspan/axvline in late_kwargs may have widened xlim beyond what
450    # set_labels() last saw; regenerate ticks from the updated view.
451    refresh_period_labels(axes)
452    legend = axes.get_legend()
453    if legend and kwargs.get("remove_legend", False):
454        legend.remove()
455    if not axes_only and not isinstance(fig, SubFigure):
456        save_to_file(fig, **kwargs)
457
458    # show the plot in Jupyter Lab
459    if not axes_only and kwargs.get("show"):
460        plt.show()
461
462    # And close - the figure this axes belongs to, not pyplot's current figure
463    if not axes_only and not kwargs.get("dont_close", False):
464        root = fig
465        while isinstance(root, SubFigure):
466            root = root.figure
467        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

def get_color(s: str) -> str:
 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.

def get_party_palette(party_text: str) -> str:
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.

def get_setting(setting: str) -> Any:
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

def growth_plot( data: ~DataT, **kwargs: Unpack[GrowthKwargs]) -> matplotlib.axes._axes.Axes:
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.

def growth_plot_finalise(data: ~DataT, **kwargs: Unpack[mgplot.finalisers.GrowthPFKwargs]) -> None:
178def growth_plot_finalise(data: DataT, **kwargs: Unpack[GrowthPFKwargs]) -> None:
179    """Call growth_plot() and finalise_plot().
180
181    Args:
182        data: The growth data to be plotted.
183        kwargs: Combined growth plot and finalise plot keyword arguments.
184
185    Note:
186        Use this when you are providing the raw growth data. Don't forget to
187        set the ylabel in kwargs.
188
189    """
190    validate_kwargs(schema=GrowthPFKwargs, caller="growth_plot_finalise", **kwargs)
191    kwargs = impose_legend(kwargs=kwargs, force=True)
192    plot_then_finalise(data=data, function=growth_plot, **kwargs)

Call growth_plot() and finalise_plot().

Args: data: The growth data to be plotted. kwargs: Combined growth plot and finalise plot keyword arguments.

Note: Use this when you are providing the raw growth data. Don't forget to set the ylabel in kwargs.

def line_plot( data: ~DataT, **kwargs: Unpack[LineKwargs]) -> matplotlib.axes._axes.Axes:
150def line_plot(data: DataT, **kwargs: Unpack[LineKwargs]) -> Axes:
151    """Build a single or multi-line plot.
152
153    Args:
154        data: DataFrame | Series - data to plot
155        kwargs: LineKwargs - keyword arguments for the line plot
156
157    Returns:
158    - axes: Axes - the axes object for the plot
159
160    """
161    # --- check the kwargs
162    report_kwargs(caller=ME, **kwargs)
163    validate_kwargs(schema=LineKwargs, caller=ME, **kwargs)
164
165    # --- check the data
166    data = check_clean_timeseries(data, ME)
167    df = DataFrame(data)  # we are only plotting DataFrames
168    df, kwargs_d = constrain_data(df, **kwargs)
169
170    # --- convert PeriodIndex to Integer Index
171    saved_pi = map_periodindex(df)
172    if saved_pi is not None:
173        df = saved_pi[0]
174
175    if isinstance(df.index, PeriodIndex):
176        print("Internal error: data is still a PeriodIndex - come back here and fix it")
177
178    # --- Let's plot
179    axes, kwargs_d = get_axes(**kwargs_d)  # get the axes to plot on
180    if df.empty or df.isna().all().all():
181        # Note: finalise plot should ignore an empty axes object
182        print(f"Warning: No data to plot in {ME}().")
183        return axes
184
185    # --- get the arguments for each line we will plot ...
186    item_count = len(df.columns)
187    num_data_points = len(df)
188    swce, kwargs_d = get_style_width_color_etc(item_count, num_data_points, **kwargs_d)
189
190    for i, column in enumerate(df.columns):
191        series = df[column]
192        series = series.dropna() if "dropna" in swce and swce["dropna"][i] else series
193        if series.empty or series.isna().all():
194            print(f"Warning: No data to plot for {column} in line_plot().")
195            continue
196
197        axes.plot(
198            # using matplotlib, as pandas can set xlabel/ylabel
199            series.index,  # x
200            series,  # y
201            ls=swce["style"][i],
202            lw=swce["width"][i],
203            color=swce["color"][i],
204            alpha=swce["alpha"][i],
205            marker=swce["marker"][i],
206            ms=swce["markersize"][i],
207            drawstyle=swce["drawstyle"][i],
208            zorder=swce["zorder"][i],
209            label=(column if "label_series" in swce and swce["label_series"][i] else f"_{column}_"),
210        )
211
212        if swce["annotate"][i] is None or not swce["annotate"][i]:
213            continue
214
215        color = swce["color"][i] if swce["annotate_color"][i] is True else swce["annotate_color"][i]
216        annotate_series(
217            series,
218            axes,
219            color=color,
220            rounding=swce["rounding"][i],
221            fontsize=swce["fontsize"][i],
222            fontname=swce["fontname"][i],
223            rotation=swce["rotation"][i],
224        )
225
226    # --- set the labels
227    if saved_pi is not None:
228        set_labels(
229            axes,
230            saved_pi[1],
231            kwargs_d.get("max_ticks", get_setting("max_ticks")),
232            tick_relabel=kwargs_d.get("tick_relabel"),
233        )
234
235    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
def line_plot_finalise(data: ~DataT, **kwargs: Unpack[mgplot.finalisers.LPFKwargs]) -> None:
195def line_plot_finalise(
196    data: DataT,
197    **kwargs: Unpack[LPFKwargs],
198) -> None:
199    """Call line_plot() then finalise_plot().
200
201    Args:
202        data: The data to be plotted.
203        kwargs: Combined line plot and finalise plot keyword arguments.
204
205    """
206    validate_kwargs(schema=LPFKwargs, caller="line_plot_finalise", **kwargs)
207    kwargs = impose_legend(kwargs=kwargs, data=data)
208    plot_then_finalise(data, function=line_plot, **kwargs)

Call line_plot() then finalise_plot().

Args: data: The data to be plotted. kwargs: Combined line plot and finalise plot keyword arguments.

def multi_column( data: pandas.DataFrame, function: Callable | list[Callable], **kwargs: Any) -> None:
273def multi_column(
274    data: DataFrame,
275    function: Callable | list[Callable],
276    **kwargs: Any,
277) -> None:
278    """Create multiple plots, one for each column in a DataFrame.
279
280    Args:
281        data: DataFrame - The data to be plotted.
282        function: Callable | list[Callable] - The plotting function(s) to be used.
283        kwargs: Any - Additional keyword arguments passed to plotting functions.
284
285    Returns:
286        None
287
288    Raises:
289        TypeError: If data is not a DataFrame.
290        ValueError: If DataFrame is empty or has no columns.
291
292    Note:
293        The plot title will be kwargs["title"] plus the column name.
294
295    """
296    # --- sanity checks
297    me = "multi_column"
298    report_kwargs(caller=me, **kwargs)
299    if not isinstance(data, DataFrame):
300        raise TypeError("data must be a pandas DataFrame for multi_column()")
301    if data.empty:
302        raise ValueError("DataFrame cannot be empty")
303    if len(data.columns) == 0:
304        raise ValueError("DataFrame must have at least one column")
305
306    # --- check the function argument
307    title_stem = kwargs.get("title", "")
308    tag: Final[str] = kwargs.get("tag", "")
309    first, kwargs["function"] = first_unchain(function)
310    if not kwargs["function"]:
311        del kwargs["function"]  # remove the function key if it is empty
312
313    # --- iterate over the columns
314    for i, col in enumerate(data.columns):
315        series = data[col]  # Extract as Series, not single-column DataFrame
316        kwargs["title"] = f"{title_stem}{col}" if title_stem else str(col)
317        kwargs["tag"] = _generate_tag(tag, i)
318        first(series, **kwargs)

Create multiple plots, one for each column in a DataFrame.

Args: data: DataFrame - The data to be plotted. function: Callable | list[Callable] - The plotting function(s) to be used. kwargs: Any - Additional keyword arguments passed to plotting functions.

Returns: None

Raises: TypeError: If data is not a DataFrame. ValueError: If DataFrame is empty or has no columns.

Note: The plot title will be kwargs["title"] plus the column name.

def multi_start( data: ~DataT, function: Callable | list[Callable], starts: Iterable[None | pandas.Period | int], **kwargs: Any) -> None:
215def multi_start(
216    data: DataT,
217    function: Callable | list[Callable],
218    starts: Iterable[None | Period | int],
219    **kwargs: Any,
220) -> None:
221    """Create multiple plots with different starting points.
222
223    Args:
224        data: Series | DataFrame - The data to be plotted.
225        function: Callable | list[Callable] - desired plotting function(s).
226        starts: Iterable[Period | int | None] - The starting points for each plot.
227        kwargs: Any - Additional keyword arguments passed to plotting functions.
228
229    Returns:
230        None
231
232    Raises:
233        TypeError: If starts is not an iterable of None, Period or int.
234        ValueError: If starts contains invalid values or is empty.
235
236    Note:
237        kwargs['tag'] is used to create a unique tag for each plot.
238
239    """
240    # --- sanity checks
241    me = "multi_start"
242    report_kwargs(caller=me, **kwargs)
243    if not isinstance(starts, Iterable):
244        raise TypeError("starts must be an iterable of None, Period or int")
245
246    # Convert to list to validate contents and check if empty
247    starts_list = list(starts)
248    if not starts_list:
249        raise ValueError("starts cannot be empty")
250
251    # Validate each start value
252    for i, start in enumerate(starts_list):
253        if start is not None and not isinstance(start, (Period, int)):
254            raise TypeError(
255                f"Start value at index {i} must be None, Period, or int, got {type(start).__name__}"
256            )
257
258    # --- check the function argument
259    original_tag: Final[str] = kwargs.get("tag", "")
260    first, kwargs["function"] = first_unchain(function)
261    if not kwargs["function"]:
262        del kwargs["function"]  # remove the function key if it is empty
263
264    # --- iterate over the starts
265    for i, start in enumerate(starts_list):
266        kw = kwargs.copy()  # copy to avoid modifying the original kwargs
267        this_tag = _generate_tag(original_tag, i)
268        kw["tag"] = this_tag
269        kw["plot_from"] = start  # rely on plotting function to constrain the data
270        first(data, **kw)

Create multiple plots with different starting points.

Args: data: Series | DataFrame - The data to be plotted. function: Callable | list[Callable] - desired plotting function(s). starts: Iterable[Period | int | None] - The starting points for each plot. kwargs: Any - Additional keyword arguments passed to plotting functions.

Returns: None

Raises: TypeError: If starts is not an iterable of None, Period or int. ValueError: If starts contains invalid values or is empty.

Note: kwargs['tag'] is used to create a unique tag for each plot.

def plot_then_finalise(data: ~DataT, function: Callable | list[Callable], **kwargs: Any) -> None:
152def plot_then_finalise(
153    data: DataT,
154    function: Callable | list[Callable],
155    **kwargs: Any,
156) -> None:
157    """Chain a plotting function with the finalise_plot() function.
158
159    Args:
160        data: Series | DataFrame - The data to be plotted.
161        function: Callable | list[Callable] - the desired plotting function(s).
162        kwargs: Any - Additional keyword arguments.
163
164    Returns None.
165
166    """
167    # --- checks
168    me = "plot_then_finalise"
169    report_kwargs(caller=me, **kwargs)
170    # validate once we have established the first function
171
172    # data is not checked here, assume it is checked by the called
173    # plot function.
174
175    first, kwargs["function"] = first_unchain(function)
176    if not kwargs["function"]:
177        del kwargs["function"]  # remove the function key if it is empty
178
179    # Check that forbidden functions are not called first
180    if hasattr(first, "__name__") and first.__name__ in FORBIDDEN_FIRST_FUNCTIONS:
181        raise ValueError(
182            f"Function '{first.__name__}' should not be called by {me}. Call it before calling {me}."
183        )
184
185    if first in EXPECTED_CALLABLES:
186        expected = EXPECTED_CALLABLES[first]
187        plot_kwargs = limit_kwargs(expected, **kwargs)
188    else:
189        # this is an unexpected Callable, so we will give it a try
190        print(f"Unknown proposed function: {first}; nonetheless, will give it a try.")
191        expected = BaseKwargs
192        plot_kwargs = kwargs.copy()
193
194    # --- validate the original kwargs (could not do before now)
195    kw_types = (
196        # combine the expected kwargs types with the finalise kwargs types
197        dict(cast("dict[str, Any]", expected.__annotations__))
198        | dict(cast("dict[str, Any]", FinaliseKwargs.__annotations__))
199    )
200    validate_kwargs(schema=kw_types, caller=me, **kwargs)
201
202    # --- call the first function with the data and selected plot kwargs
203    axes = first(data, **plot_kwargs)
204
205    # --- prepare finalise kwargs (remove overlapping arguments)
206    fp_kwargs = limit_kwargs(FinaliseKwargs, **kwargs)
207    # Remove any arguments that were already used in the plot function
208    used_plot_args = set(plot_kwargs.keys())
209    fp_kwargs = {k: v for k, v in fp_kwargs.items() if k not in used_plot_args}
210
211    # --- finalise the plot
212    finalise_plot(axes, **fp_kwargs)

Chain a plotting function with the finalise_plot() function.

Args: data: Series | DataFrame - The data to be plotted. function: Callable | list[Callable] - the desired plotting function(s). kwargs: Any - Additional keyword arguments.

Returns None.

def postcovid_plot( data: ~DataT, **kwargs: Unpack[PostcovidKwargs]) -> matplotlib.axes._axes.Axes:
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

def postcovid_plot_finalise(data: ~DataT, **kwargs: Unpack[mgplot.finalisers.PCFKwargs]) -> None:
211def postcovid_plot_finalise(
212    data: DataT,
213    **kwargs: Unpack[PCFKwargs],
214) -> None:
215    """Call postcovid_plot() and finalise_plot().
216
217    Args:
218        data: The data to be plotted.
219        kwargs: Combined postcovid plot and finalise plot keyword arguments.
220
221    """
222    validate_kwargs(schema=PCFKwargs, caller="postcovid_plot_finalise", **kwargs)
223    kwargs = impose_legend(kwargs=kwargs, force=True)
224    plot_then_finalise(data, function=postcovid_plot, **kwargs)

Call postcovid_plot() and finalise_plot().

Args: data: The data to be plotted. kwargs: Combined postcovid plot and finalise plot keyword arguments.

def revision_plot( data: ~DataT, **kwargs: Unpack[LineKwargs]) -> matplotlib.axes._axes.Axes:
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.

def revision_plot_finalise(data: ~DataT, **kwargs: Unpack[mgplot.finalisers.RevPFKwargs]) -> None:
227def revision_plot_finalise(
228    data: DataT,
229    **kwargs: Unpack[RevPFKwargs],
230) -> None:
231    """Call revision_plot() and finalise_plot().
232
233    Args:
234        data: The revision data to be plotted.
235        kwargs: Combined revision plot and finalise plot keyword arguments.
236
237    """
238    validate_kwargs(schema=RevPFKwargs, caller="revision_plot_finalise", **kwargs)
239    kwargs = impose_legend(kwargs=kwargs, force=True)
240    plot_then_finalise(data=data, function=revision_plot, **kwargs)

Call revision_plot() and finalise_plot().

Args: data: The revision data to be plotted. kwargs: Combined revision plot and finalise plot keyword arguments.

def run_plot( data: ~DataT, **kwargs: Unpack[RunKwargs]) -> matplotlib.axes._axes.Axes:
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(
213            axes,
214            saved_pi[1],
215            kwargs.get("max_ticks", get_setting("max_ticks")),
216            tick_relabel=kwargs.get("tick_relabel"),
217        )
218
219    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
def run_plot_finalise(data: ~DataT, **kwargs: Unpack[mgplot.finalisers.RunPFKwargs]) -> None:
243def run_plot_finalise(
244    data: DataT,
245    **kwargs: Unpack[RunPFKwargs],
246) -> None:
247    """Call run_plot() and finalise_plot().
248
249    Args:
250        data: The data to be plotted.
251        kwargs: Combined run plot and finalise plot keyword arguments.
252
253    """
254    validate_kwargs(schema=RunPFKwargs, caller="run_plot_finalise", **kwargs)
255    kwargs = impose_legend(kwargs=kwargs, force="highlight_label" in kwargs)
256    plot_then_finalise(data=data, function=run_plot, **kwargs)

Call run_plot() and finalise_plot().

Args: data: The data to be plotted. kwargs: Combined run plot and finalise plot keyword arguments.

def seastrend_plot( data: ~DataT, **kwargs: Unpack[LineKwargs]) -> matplotlib.axes._axes.Axes:
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

def seastrend_plot_finalise(data: ~DataT, **kwargs: Unpack[mgplot.finalisers.SFKwargs]) -> None:
259def seastrend_plot_finalise(
260    data: DataT,
261    **kwargs: Unpack[SFKwargs],
262) -> None:
263    """Call seastrend_plot() and finalise_plot().
264
265    Args:
266        data: The seasonal and trend data to be plotted.
267        kwargs: Combined seastrend plot and finalise plot keyword arguments.
268
269    """
270    validate_kwargs(schema=SFKwargs, caller="seastrend_plot_finalise", **kwargs)
271    kwargs = impose_legend(kwargs=kwargs, force=True)
272    plot_then_finalise(data, function=seastrend_plot, **kwargs)

Call seastrend_plot() and finalise_plot().

Args: data: The seasonal and trend data to be plotted. kwargs: Combined seastrend plot and finalise plot keyword arguments.

def series_growth_plot( data: ~DataT, **kwargs: Unpack[SeriesGrowthKwargs]) -> matplotlib.axes._axes.Axes:
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

def series_growth_plot_finalise(data: ~DataT, **kwargs: Unpack[mgplot.finalisers.SGFPKwargs]) -> None:
275def series_growth_plot_finalise(data: DataT, **kwargs: Unpack[SGFPKwargs]) -> None:
276    """Call series_growth_plot() and finalise_plot().
277
278    Args:
279        data: The series data to calculate and plot growth for.
280        kwargs: Combined series growth plot and finalise plot keyword arguments.
281
282    """
283    validate_kwargs(schema=SGFPKwargs, caller="series_growth_plot_finalise", **kwargs)
284    kwargs = impose_legend(kwargs=kwargs, force=True)
285    plot_then_finalise(data=data, function=series_growth_plot, **kwargs)

Call series_growth_plot() and finalise_plot().

Args: data: The series data to calculate and plot growth for. kwargs: Combined series growth plot and finalise plot keyword arguments.

def set_chart_dir(chart_dir: str) -> None:
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.

def set_setting(setting: str, value: Any) -> None:
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

state_abbrs = ('NSW', 'Vic', 'Qld', 'SA', 'WA', 'Tas', 'NT', 'ACT')
state_names = ('New South Wales', 'Victoria', 'Queensland', 'South Australia', 'Western Australia', 'Tasmania', 'Northern Territory', 'Australian Capital Territory')
def summary_plot( data: ~DataT, **kwargs: Unpack[SummaryKwargs]) -> matplotlib.axes._axes.Axes:
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.

def summary_plot_finalise(data: ~DataT, **kwargs: Unpack[mgplot.finalisers.SumPFKwargs]) -> None:
288def summary_plot_finalise(
289    data: DataT,
290    **kwargs: Unpack[SumPFKwargs],
291) -> None:
292    """Call summary_plot() and finalise_plot().
293
294    This is more complex than most of the above convenience methods as it
295    creates multiple plots (one for each plot type).
296
297    Args:
298        data: DataFrame containing the summary data. The index must be a PeriodIndex.
299        kwargs: Combined summary plot and finalise plot keyword arguments.
300
301    Raises:
302        TypeError: If data is not a DataFrame with a PeriodIndex.
303        IndexError: If DataFrame is empty.
304
305    """
306    # --- validate data type and structure
307    if not isinstance(data, DataFrame) or not isinstance(data.index, PeriodIndex):
308        raise TypeError("Data must be a DataFrame with a PeriodIndex.")
309
310    if data.empty or len(data.index) == 0:
311        raise ValueError("DataFrame cannot be empty")
312
313    validate_kwargs(schema=SumPFKwargs, caller="summary_plot_finalise", **kwargs)
314
315    # --- set default title with bounds checking
316    kwargs["title"] = kwargs.get("title", f"Summary at {label_period(data.index[-1])}")
317    kwargs["preserve_lims"] = kwargs.get("preserve_lims", True)
318
319    # --- handle plot_from parameter with bounds checking
320    start: int | Period | None = kwargs.get("plot_from", 0)
321    if start is None:
322        start = data.index[0]
323    elif isinstance(start, int):
324        if abs(start) >= len(data.index):
325            raise IndexError(
326                f"plot_from index {start} out of range for DataFrame with {len(data.index)} rows"
327            )
328        start = data.index[start]
329
330    kwargs["plot_from"] = start
331    if not isinstance(start, Period):
332        raise TypeError("plot_from must be a Period or convertible to one")
333
334    # --- create plots for each plot type
335    pre_tag: str = kwargs.get("pre_tag", "")
336    for plot_type in SUMMARY_PLOT_TYPES:
337        plot_kwargs = kwargs.copy()  # Avoid modifying original kwargs
338        plot_kwargs["plot_type"] = plot_type
339        plot_kwargs["pre_tag"] = pre_tag + plot_type
340
341        plot_then_finalise(
342            data,
343            function=summary_plot,
344            **plot_kwargs,
345        )

Call summary_plot() and finalise_plot().

This is more complex than most of the above convenience methods as it creates multiple plots (one for each plot type).

Args: data: DataFrame containing the summary data. The index must be a PeriodIndex. kwargs: Combined summary plot and finalise plot keyword arguments.

Raises: TypeError: If data is not a DataFrame with a PeriodIndex. IndexError: If DataFrame is empty.