mgplot

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

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

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

Keyword arguments for the bar_plot function.

ax: NotRequired[matplotlib.axes._axes.Axes | None]
stacked: NotRequired[bool]
max_ticks: NotRequired[int]
plot_from: NotRequired[int | pandas._libs.tslibs.period.Period]
color: NotRequired[str | Sequence[str]]
label_series: NotRequired[bool | Sequence[bool]]
width: 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 FinaliseKwargs(mgplot.keyword_checking.BaseKwargs):
31class FinaliseKwargs(BaseKwargs):
32    """Keyword arguments for the finalise_plot function."""
33
34    # --- value options
35    title: NotRequired[str | None]
36    xlabel: NotRequired[str | None]
37    ylabel: NotRequired[str | None]
38    xlim: NotRequired[tuple[float, float] | None]
39    ylim: NotRequired[tuple[float, float] | None]
40    xticks: NotRequired[list[float] | None]
41    yticks: NotRequired[list[float] | None]
42    xscale: NotRequired[str | None]
43    yscale: NotRequired[str | None]
44    # --- splat options
45    legend: NotRequired[bool | dict[str, Any] | None]
46    axhspan: NotRequired[dict[str, Any]]
47    axvspan: NotRequired[dict[str, Any]]
48    axhline: NotRequired[dict[str, Any]]
49    axvline: NotRequired[dict[str, Any]]
50    # --- options for annotations
51    lfooter: NotRequired[str]
52    rfooter: NotRequired[str]
53    lheader: NotRequired[str]
54    rheader: NotRequired[str]
55    # --- file/save options
56    pre_tag: NotRequired[str]
57    tag: NotRequired[str]
58    chart_dir: NotRequired[str]
59    file_type: NotRequired[str]
60    dpi: NotRequired[int]
61    figsize: NotRequired[tuple[float, float]]
62    show: NotRequired[bool]
63    # --- other options
64    preserve_lims: NotRequired[bool]
65    remove_legend: NotRequired[bool]
66    zero_y: NotRequired[bool]
67    y0: NotRequired[bool]
68    x0: NotRequired[bool]
69    dont_save: NotRequired[bool]
70    dont_close: NotRequired[bool]

Keyword arguments for the finalise_plot function.

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]]
axvspan: NotRequired[dict[str, Any]]
axhline: NotRequired[dict[str, Any]]
axvline: NotRequired[dict[str, Any]]
lfooter: NotRequired[str]
rfooter: NotRequired[str]
lheader: NotRequired[str]
rheader: NotRequired[str]
pre_tag: NotRequired[str]
tag: 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]
dont_save: NotRequired[bool]
dont_close: NotRequired[bool]
class GrowthKwargs(mgplot.keyword_checking.BaseKwargs):
38class GrowthKwargs(BaseKwargs):
39    """Keyword arguments for the growth_plot function."""
40
41    # --- common options
42    ax: NotRequired[Axes | None]
43    plot_from: NotRequired[int | Period]
44    label_series: NotRequired[bool]
45    max_ticks: NotRequired[int]
46    # --- options passed to the line plot
47    line_width: NotRequired[float | int]
48    line_color: NotRequired[str]
49    line_style: NotRequired[str]
50    annotate_line: NotRequired[bool]
51    line_rounding: NotRequired[bool | int]
52    line_fontsize: NotRequired[str | int | float]
53    line_fontname: NotRequired[str]
54    line_anno_color: NotRequired[str]
55    # --- options passed to the bar plot
56    annotate_bars: NotRequired[bool]
57    bar_fontsize: NotRequired[str | int | float]
58    bar_fontname: NotRequired[str]
59    bar_rounding: NotRequired[int]
60    bar_width: NotRequired[float]
61    bar_color: NotRequired[str]
62    bar_anno_color: NotRequired[str]
63    bar_rotation: NotRequired[int | float]

Keyword arguments for the growth_plot function.

ax: NotRequired[matplotlib.axes._axes.Axes | None]
plot_from: NotRequired[int | pandas._libs.tslibs.period.Period]
label_series: NotRequired[bool]
max_ticks: NotRequired[int]
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    dropna: NotRequired[bool | Sequence[bool]]
41    annotate: NotRequired[bool | Sequence[bool]]
42    rounding: NotRequired[Sequence[int | bool] | int | bool | None]
43    fontsize: NotRequired[Sequence[str | int | float] | str | int | float]
44    fontname: NotRequired[str | Sequence[str]]
45    rotation: NotRequired[Sequence[int | float] | int | float]
46    annotate_color: NotRequired[str | Sequence[str] | bool | Sequence[bool] | None]
47    plot_from: NotRequired[int | Period | None]
48    label_series: NotRequired[bool | Sequence[bool] | None]
49    max_ticks: NotRequired[int]

Keyword arguments for the line_plot function.

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]
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._libs.tslibs.period.Period | None]
label_series: NotRequired[bool | Sequence[bool] | None]
max_ticks: NotRequired[int]
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._libs.tslibs.period.Period]
end_r: NotRequired[pandas._libs.tslibs.period.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):
66class SeriesGrowthKwargs(GrowthKwargs):
67    """Keyword arguments for the series_growth_plot function."""
68
69    ylabel: NotRequired[str | None]

Keyword arguments for the series_growth_plot function.

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._libs.tslibs.period.Period]
legend: NotRequired[bool | dict[str, Any] | None]
xlabel: NotRequired[str | None]
__author__ = 'Bryan Palmer'
__version__ = '0.2.10a1'
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:
209def bar_plot(data: DataT, **kwargs: Unpack[BarKwargs]) -> Axes:
210    """Create a bar plot from the given data.
211
212    Each column in the DataFrame will be stacked on top of each other,
213    with positive values above zero and negative values below zero.
214
215    Args:
216        data: Series | DataFrame - The data to plot. Can be a DataFrame or a Series.
217        **kwargs: BarKwargs - Additional keyword arguments for customization.
218        (see BarKwargs for details)
219
220    Note: This function does not assume all data is timeseries with a PeriodIndex.
221
222    Returns:
223        axes: Axes - The axes for the plot.
224
225    """
226    # --- check the kwargs
227    report_kwargs(caller=ME, **kwargs)
228    validate_kwargs(schema=BarKwargs, caller=ME, **kwargs)
229
230    # --- get the data
231    # no call to check_clean_timeseries here, as bar plots are not
232    # necessarily timeseries data. If the data is a Series, it will be
233    # converted to a DataFrame with a single column.
234    df = DataFrame(data)  # really we are only plotting DataFrames
235    df, kwargs_d = constrain_data(df, **kwargs)
236    item_count = len(df.columns)
237
238    # --- deal with complete PeriodIndex indices
239    saved_pi = map_periodindex(df)
240    if saved_pi is not None:
241        df = saved_pi[0]  # extract the reindexed DataFrame from the PeriodIndex
242
243    # --- set up the default arguments
244    chart_defaults: dict[str, bool | int] = {
245        "stacked": False,
246        "max_ticks": DEFAULT_MAX_TICKS,
247        "label_series": item_count > 1,
248    }
249    chart_args = {k: kwargs_d.get(k, v) for k, v in chart_defaults.items()}
250
251    bar_defaults = {
252        "color": get_color_list(item_count),
253        "width": get_setting("bar_width"),
254        "label_series": item_count > 1,
255    }
256    above = kwargs_d.get("above", False)
257    anno_args: AnnoKwargs = {
258        "annotate": kwargs_d.get("annotate", False),
259        "fontsize": kwargs_d.get("fontsize", "small"),
260        "fontname": kwargs_d.get("fontname", "Helvetica"),
261        "rotation": kwargs_d.get("rotation", 0),
262        "rounding": kwargs_d.get("rounding", True),
263        "color": kwargs_d.get("annotate_color", "black" if above else "white"),
264        "above": above,
265    }
266    bar_args, remaining_kwargs = apply_defaults(item_count, bar_defaults, kwargs_d)
267
268    # --- plot the data
269    axes, remaining_kwargs = get_axes(**dict(remaining_kwargs))
270    if chart_args["stacked"]:
271        stacked(axes, df, anno_args, **bar_args)
272    else:
273        grouped(axes, df, anno_args, **bar_args)
274
275    # --- handle complete periodIndex data and label rotation
276    if saved_pi is not None:
277        set_labels(axes, saved_pi[1], chart_args["max_ticks"])
278    else:
279        plt.xticks(rotation=90)
280
281    return axes

Create a bar plot from the given data.

Each column in the DataFrame will be stacked on top of each other, with positive values above zero and negative values below zero.

Args: data: Series | DataFrame - The data to plot. Can be a DataFrame or a Series. **kwargs: BarKwargs - Additional keyword arguments for customization. (see BarKwargs for details)

Note: This function does not assume all data is timeseries with a PeriodIndex.

Returns: axes: Axes - The axes for the plot.

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

Call bar_plot() and finalise_plot().

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

def calc_growth(series: pandas.core.series.Series) -> pandas.core.frame.DataFrame:
111def calc_growth(series: Series) -> DataFrame:
112    """Calculate annual and periodic growth for a pandas Series.
113
114    Args:
115        series: Series - a pandas series with a date-like PeriodIndex.
116
117    Returns:
118        DataFrame: A two column DataFrame with annual and periodic growth rates.
119
120    Raises:
121        TypeError if the series is not a pandas Series.
122        TypeError if the series index is not a PeriodIndex.
123        ValueError if the series is empty.
124        ValueError if the series index does not have a frequency of Q, M, or D.
125        ValueError if the series index has duplicates.
126
127    """
128    # --- sanity checks
129    if not isinstance(series, Series):
130        raise TypeError("The series argument must be a pandas Series")
131    if not isinstance(series.index, PeriodIndex):
132        raise TypeError("The series index must be a pandas PeriodIndex")
133    if series.empty:
134        raise ValueError("The series argument must not be empty")
135    freq = series.index.freqstr
136    if not freq or freq[0] not in FREQUENCY_TO_PERIODS:
137        raise ValueError("The series index must have a frequency of Q, M, or D")
138    if series.index.has_duplicates:
139        raise ValueError("The series index must not have duplicate values")
140
141    # --- ensure the index is complete and the date is sorted
142    complete = period_range(start=series.index.min(), end=series.index.max())
143    series = series.reindex(complete, fill_value=nan)
144    series = series.sort_index(ascending=True)
145
146    # --- calculate annual and periodic growth
147    freq = PeriodIndex(series.index).freqstr
148    if not freq or freq[0] not in FREQUENCY_TO_PERIODS:
149        raise ValueError("The series index must have a frequency of Q, M, or D")
150
151    freq_key = freq[0]
152    ppy = FREQUENCY_TO_PERIODS[freq_key]
153    annual = series.pct_change(periods=ppy) * 100
154    periodic = series.pct_change(periods=1) * 100
155    periodic_name = FREQUENCY_TO_NAME[freq_key] + " Growth"
156    return DataFrame(
157        {
158            "Annual Growth": annual,
159            periodic_name: periodic,
160        },
161    )

Calculate annual and periodic growth for a pandas Series.

Args: series: Series - a pandas series with a date-like PeriodIndex.

Returns: DataFrame: A two column DataFrame with annual and periodic growth rates.

Raises: TypeError if the series is not a pandas Series. TypeError if the series index is not a PeriodIndex. ValueError if the series is empty. ValueError if the series index does not have a frequency of Q, M, or D. ValueError if the series index has duplicates.

def clear_chart_dir() -> None:
146def clear_chart_dir() -> None:
147    """Remove all graph-image files from the global chart_dir."""
148    chart_dir = get_setting("chart_dir")
149    Path(chart_dir).mkdir(parents=True, exist_ok=True)
150    for ext in IMAGE_EXTENSIONS:
151        for fs_object in Path(chart_dir).glob(f"*.{ext}"):
152            if fs_object.is_file():
153                fs_object.unlink()

Remove all graph-image files from the global chart_dir.

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 finalise_plot( axes: matplotlib.axes._axes.Axes, **kwargs: Unpack[FinaliseKwargs]) -> None:
338def finalise_plot(axes: Axes, **kwargs: Unpack[FinaliseKwargs]) -> None:
339    """Finalise and save plots to the file system.
340
341    The filename for the saved plot is constructed from the global
342    chart_dir, the plot's title, any specified tag text, and the
343    file_type for the plot.
344
345    Args:
346        axes: Axes - matplotlib axes object - required
347        kwargs: FinaliseKwargs
348
349    """
350    # --- check the kwargs
351    report_kwargs(caller=ME, **kwargs)
352    validate_kwargs(schema=FinaliseKwargs, caller=ME, **kwargs)
353
354    # --- sanity checks
355    if len(axes.get_children()) < 1:
356        print(f"Warning: {ME}() called with an empty axes, which was ignored.")
357        return
358
359    # --- remember axis-limits should we need to restore thems
360    xlim, ylim = axes.get_xlim(), axes.get_ylim()
361
362    # margins
363    axes.margins(DEFAULT_MARGIN)
364    axes.autoscale(tight=False)  # This is problematic ...
365
366    apply_kwargs(axes, **kwargs)
367
368    # tight layout and save the figure
369    fig = axes.figure
370    if kwargs.get("preserve_lims"):
371        # restore the original limits of the axes
372        axes.set_xlim(xlim)
373        axes.set_ylim(ylim)
374    if not isinstance(fig, SubFigure):
375        fig.tight_layout(pad=TIGHT_LAYOUT_PAD)
376    apply_late_kwargs(axes, **kwargs)
377    legend = axes.get_legend()
378    if legend and kwargs.get("remove_legend", False):
379        legend.remove()
380    if not isinstance(fig, SubFigure):
381        save_to_file(fig, **kwargs)
382
383    # show the plot in Jupyter Lab
384    if kwargs.get("show"):
385        plt.show()
386
387    # And close
388    if not kwargs.get("dont_close", False):
389        plt.close()

Finalise and save plots to the file system.

The filename for the saved plot is constructed from the global chart_dir, the plot's title, any specified tag text, and the file_type for the plot.

Args: axes: Axes - matplotlib axes object - required kwargs: FinaliseKwargs

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:
102def get_setting(setting: str) -> Any:
103    """Get a setting from the global settings.
104
105    Args:
106        setting: str - name of the setting to get.
107
108    Raises:
109        KeyError: if the setting is not found
110
111    Returns:
112        value: Any - the value of the setting
113
114    """
115    if setting not in get_fields():
116        raise KeyError(f"Setting '{setting}' not found in mgplot_defaults.")
117    return getattr(mgplot_defaults, setting)

Get a setting from the global settings.

Args: setting: str - name of the setting to get.

Raises: KeyError: if the setting is not found

Returns: value: Any - the value of the setting

def growth_plot( data: ~DataT, **kwargs: Unpack[GrowthKwargs]) -> matplotlib.axes._axes.Axes:
164def growth_plot(
165    data: DataT,
166    **kwargs: Unpack[GrowthKwargs],
167) -> Axes:
168    """Plot annual growth and periodic growth on the same axes.
169
170    Args:
171        data: A pandas DataFrame with two columns:
172        kwargs: GrowthKwargs
173
174    Returns:
175        axes: The matplotlib Axes object.
176
177    Raises:
178        TypeError if the data is not a 2-column DataFrame.
179        TypeError if the annual index is not a PeriodIndex.
180        ValueError if the annual and periodic series do not have the same index.
181
182    """
183    # --- check the kwargs
184    me = "growth_plot"
185    report_kwargs(caller=me, **kwargs)
186    validate_kwargs(GrowthKwargs, caller=me, **kwargs)
187
188    # --- data checks
189    data = check_clean_timeseries(data, me)
190    if len(data.columns) != TWO_COLUMNS:
191        raise TypeError("The data argument must be a pandas DataFrame with two columns")
192    data, kwargsd = constrain_data(data, **kwargs)
193
194    # --- get the series of interest ...
195    annual = data[data.columns[0]]
196    periodic = data[data.columns[1]]
197
198    # --- series names
199    annual.name = "Annual Growth"
200    freq = PeriodIndex(periodic.index).freqstr
201    if freq and freq[0] in FREQUENCY_TO_NAME:
202        periodic.name = FREQUENCY_TO_NAME[freq[0]] + " Growth"
203    else:
204        periodic.name = "Periodic Growth"
205
206    # --- convert PeriodIndex periodic growth data to integer indexed data.
207    saved_pi = map_periodindex(periodic)
208    if saved_pi is not None:
209        periodic = saved_pi[0]  # extract the reindexed DataFrame
210
211    # --- simple bar chart for the periodic growth
212    if "bar_anno_color" not in kwargsd or kwargsd["bar_anno_color"] is None:
213        kwargsd["bar_anno_color"] = "black" if kwargsd.get("above", False) else "white"
214    selected = package_kwargs(to_bar_plot, **kwargsd)
215    axes = bar_plot(periodic, **selected)
216
217    # --- and now the annual growth as a line
218    selected = package_kwargs(to_line_plot, **kwargsd)
219    line_plot(annual, ax=axes, **selected)
220
221    # --- fix the x-axis labels
222    if saved_pi is not None:
223        set_labels(axes, saved_pi[1], kwargsd.get("max_ticks", 10))
224
225    # --- and done ...
226    return axes

Plot annual growth and periodic growth on the same axes.

Args: data: A pandas DataFrame with two columns: kwargs: GrowthKwargs

Returns: axes: The matplotlib Axes object.

Raises: TypeError if the data is not a 2-column DataFrame. TypeError if the annual index is not a PeriodIndex. ValueError if the annual and periodic series do not have the same index.

def growth_plot_finalise(data: ~DataT, **kwargs: Unpack[mgplot.finalisers.GrowthPFKwargs]) -> None:
152def growth_plot_finalise(data: DataT, **kwargs: Unpack[GrowthPFKwargs]) -> None:
153    """Call growth_plot() and finalise_plot().
154
155    Args:
156        data: The growth data to be plotted.
157        kwargs: Combined growth plot and finalise plot keyword arguments.
158
159    Note:
160        Use this when you are providing the raw growth data. Don't forget to
161        set the ylabel in kwargs.
162
163    """
164    validate_kwargs(schema=GrowthPFKwargs, caller="growth_plot_finalise", **kwargs)
165    kwargs = impose_legend(kwargs=kwargs, force=True)
166    plot_then_finalise(data=data, function=growth_plot, **kwargs)

Call growth_plot() and finalise_plot().

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

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

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

Build a single or multi-line plot.

Args: data: DataFrame | Series - data to plot kwargs: LineKwargs - keyword arguments for the line plot

Returns:

  • axes: Axes - the axes object for the plot
def line_plot_finalise(data: ~DataT, **kwargs: Unpack[mgplot.finalisers.LPFKwargs]) -> None:
169def line_plot_finalise(
170    data: DataT,
171    **kwargs: Unpack[LPFKwargs],
172) -> None:
173    """Call line_plot() then finalise_plot().
174
175    Args:
176        data: The data to be plotted.
177        kwargs: Combined line plot and finalise plot keyword arguments.
178
179    """
180    validate_kwargs(schema=LPFKwargs, caller="line_plot_finalise", **kwargs)
181    kwargs = impose_legend(kwargs=kwargs, data=data)
182    plot_then_finalise(data, function=line_plot, **kwargs)

Call line_plot() then finalise_plot().

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

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

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

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

Returns: None

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

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

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

Create multiple plots with different starting points.

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

Returns: None

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

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

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

Chain a plotting function with the finalise_plot() function.

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

Returns None.

def postcovid_plot( data: ~DataT, **kwargs: Unpack[PostcovidKwargs]) -> matplotlib.axes._axes.Axes:
115def postcovid_plot(data: DataT, **kwargs: Unpack[PostcovidKwargs]) -> Axes:
116    """Plot a series with a PeriodIndex, including a post-COVID projection.
117
118    Args:
119        data: Series - the series to be plotted.
120        kwargs: PostcovidKwargs - plotting arguments.
121
122    Raises:
123        TypeError if series is not a pandas Series
124        TypeError if series does not have a PeriodIndex
125        ValueError if series does not have a D, M or Q frequency
126        ValueError if regression start is after regression end
127
128    """
129    # --- check the kwargs
130    report_kwargs(caller=ME, **kwargs)
131    validate_kwargs(schema=PostcovidKwargs, caller=ME, **kwargs)
132
133    # --- check the data
134    data = check_clean_timeseries(data, ME)
135    if not isinstance(data, Series):
136        raise TypeError("The series argument must be a pandas Series")
137
138    # rely on line_plot() to validate kwargs, but remove any that are not relevant
139    if "plot_from" in kwargs:
140        print("Warning: the 'plot_from' argument is ignored in postcovid_plot().")
141        del kwargs["plot_from"]
142
143    # --- set the regression period
144    start_r, end_r, robust = regression_period(data, **kwargs)
145    kwargs.pop("start_r", None)  # remove from kwargs to avoid confusion
146    kwargs.pop("end_r", None)  # remove from kwargs to avoid confusion
147    if not robust:
148        print("No valid regression period found; plotting raw data only.")
149        return line_plot(
150            data,
151            **cast("LineKwargs", kwargs),
152        )
153
154    # --- combine data and projection
155    if start_r < data.dropna().index.min():
156        print(f"Caution: Regression start period pre-dates the series index: {start_r=}")
157    recent_data = data[data.index >= start_r].copy()
158    recent_data.name = "Series"
159    projection_data = get_projection(recent_data, end_r)
160    projection_data.name = "Pre-COVID projection"
161
162    # --- Create DataFrame with proper column alignment
163    combined_data = DataFrame(
164        {
165            projection_data.name: projection_data,
166            recent_data.name: recent_data,
167        }
168    )
169
170    # --- activate plot settings
171    kwargs["width"] = kwargs.pop(
172        "width",
173        (get_setting("line_normal"), get_setting("line_wide")),
174    )  # series line is thicker than projection
175    kwargs["style"] = kwargs.pop("style", ("--", "-"))  # dashed regression line
176    kwargs["label_series"] = kwargs.pop("label_series", True)
177    kwargs["annotate"] = kwargs.pop("annotate", (False, True))  # annotate series only
178    kwargs["color"] = kwargs.pop("color", ("darkblue", "#dd0000"))
179    kwargs["dropna"] = kwargs.pop("dropna", False)  # drop NaN values
180
181    return line_plot(
182        combined_data,
183        **cast("LineKwargs", kwargs),
184    )

Plot a series with a PeriodIndex, including a post-COVID projection.

Args: data: Series - the series to be plotted. kwargs: PostcovidKwargs - plotting arguments.

Raises: TypeError if series is not a pandas Series TypeError if series does not have a PeriodIndex ValueError if series does not have a D, M or Q frequency ValueError if regression start is after regression end

def postcovid_plot_finalise(data: ~DataT, **kwargs: Unpack[mgplot.finalisers.PCFKwargs]) -> None:
185def postcovid_plot_finalise(
186    data: DataT,
187    **kwargs: Unpack[PCFKwargs],
188) -> None:
189    """Call postcovid_plot() and finalise_plot().
190
191    Args:
192        data: The data to be plotted.
193        kwargs: Combined postcovid plot and finalise plot keyword arguments.
194
195    """
196    validate_kwargs(schema=PCFKwargs, caller="postcovid_plot_finalise", **kwargs)
197    kwargs = impose_legend(kwargs=kwargs, force=True)
198    plot_then_finalise(data, function=postcovid_plot, **kwargs)

Call postcovid_plot() and finalise_plot().

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

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:
201def revision_plot_finalise(
202    data: DataT,
203    **kwargs: Unpack[RevPFKwargs],
204) -> None:
205    """Call revision_plot() and finalise_plot().
206
207    Args:
208        data: The revision data to be plotted.
209        kwargs: Combined revision plot and finalise plot keyword arguments.
210
211    """
212    validate_kwargs(schema=RevPFKwargs, caller="revision_plot_finalise", **kwargs)
213    kwargs = impose_legend(kwargs=kwargs, force=True)
214    plot_then_finalise(data=data, function=revision_plot, **kwargs)

Call revision_plot() and finalise_plot().

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

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(axes, saved_pi[1], kwargs.get("max_ticks", get_setting("max_ticks")))
213
214    return axes

Plot a series of percentage rates, highlighting the increasing runs.

Arguments: data: Series - ordered pandas Series of percentages, with PeriodIndex. kwargs: RunKwargs - keyword arguments for the run_plot function.

Return:

  • matplotlib Axes object
def run_plot_finalise(data: ~DataT, **kwargs: Unpack[mgplot.finalisers.RunPFKwargs]) -> None:
217def run_plot_finalise(
218    data: DataT,
219    **kwargs: Unpack[RunPFKwargs],
220) -> None:
221    """Call run_plot() and finalise_plot().
222
223    Args:
224        data: The data to be plotted.
225        kwargs: Combined run plot and finalise plot keyword arguments.
226
227    """
228    validate_kwargs(schema=RunPFKwargs, caller="run_plot_finalise", **kwargs)
229    kwargs = impose_legend(kwargs=kwargs, force="highlight_label" in kwargs)
230    plot_then_finalise(data=data, function=run_plot, **kwargs)

Call run_plot() and finalise_plot().

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

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:
233def seastrend_plot_finalise(
234    data: DataT,
235    **kwargs: Unpack[SFKwargs],
236) -> None:
237    """Call seastrend_plot() and finalise_plot().
238
239    Args:
240        data: The seasonal and trend data to be plotted.
241        kwargs: Combined seastrend plot and finalise plot keyword arguments.
242
243    """
244    validate_kwargs(schema=SFKwargs, caller="seastrend_plot_finalise", **kwargs)
245    kwargs = impose_legend(kwargs=kwargs, force=True)
246    plot_then_finalise(data, function=seastrend_plot, **kwargs)

Call seastrend_plot() and finalise_plot().

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

def series_growth_plot( data: ~DataT, **kwargs: Unpack[SeriesGrowthKwargs]) -> matplotlib.axes._axes.Axes:
229def series_growth_plot(
230    data: DataT,
231    **kwargs: Unpack[SeriesGrowthKwargs],
232) -> Axes:
233    """Plot annual and periodic growth in percentage terms from a pandas Series.
234
235    Args:
236        data: A pandas Series with an appropriate PeriodIndex.
237        kwargs: SeriesGrowthKwargs
238
239    """
240    # --- check the kwargs
241    me = "series_growth_plot"
242    report_kwargs(caller=me, **kwargs)
243    validate_kwargs(SeriesGrowthKwargs, caller=me, **kwargs)
244
245    # --- sanity checks
246    if not isinstance(data, Series):
247        raise TypeError("The data argument to series_growth_plot() must be a pandas Series")
248
249    # --- calculate growth and plot - add ylabel
250    ylabel: str | None = kwargs.pop("ylabel", None)
251    if ylabel is not None:
252        print(f"Did you intend to specify a value for the 'ylabel' in {me}()?")
253    ylabel = "Growth (%)" if ylabel is None else ylabel
254    growth = calc_growth(data)
255    ax = growth_plot(growth, **cast("GrowthKwargs", kwargs))
256    ax.set_ylabel(ylabel)
257    return ax

Plot annual and periodic growth in percentage terms from a pandas Series.

Args: data: A pandas Series with an appropriate PeriodIndex. kwargs: SeriesGrowthKwargs

def series_growth_plot_finalise(data: ~DataT, **kwargs: Unpack[mgplot.finalisers.SGFPKwargs]) -> None:
249def series_growth_plot_finalise(data: DataT, **kwargs: Unpack[SGFPKwargs]) -> None:
250    """Call series_growth_plot() and finalise_plot().
251
252    Args:
253        data: The series data to calculate and plot growth for.
254        kwargs: Combined series growth plot and finalise plot keyword arguments.
255
256    """
257    validate_kwargs(schema=SGFPKwargs, caller="series_growth_plot_finalise", **kwargs)
258    kwargs = impose_legend(kwargs=kwargs, force=True)
259    plot_then_finalise(data=data, function=series_growth_plot, **kwargs)

Call series_growth_plot() and finalise_plot().

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

def set_chart_dir(chart_dir: str) -> None:
156def set_chart_dir(chart_dir: str) -> None:
157    """Set a global chart directory for finalise_plot().
158
159    Args:
160        chart_dir: str - the directory to set as the chart directory
161
162    Note: Path.mkdir() may raise an exception if a directory cannot be created.
163
164    Note: This is a wrapper for set_setting() to set the chart_dir setting, and
165    create the directory if it does not exist.
166
167    """
168    if not chart_dir or chart_dir.isspace():
169        chart_dir = DEFAULT_CHART_DIR  # avoid empty/whitespace strings
170    Path(chart_dir).mkdir(parents=True, exist_ok=True)
171    set_setting("chart_dir", chart_dir)

Set a global chart directory for finalise_plot().

Args: chart_dir: str - the directory to set as the chart directory

Note: Path.mkdir() may raise an exception if a directory cannot be created.

Note: This is a wrapper for set_setting() to set the chart_dir setting, and create the directory if it does not exist.

def set_setting(setting: str, value: Any) -> None:
120def set_setting(setting: str, value: Any) -> None:
121    """Set a setting in the global settings.
122
123    Args:
124        setting: str - name of the setting to set (see get_setting())
125        value: Any - the value to set the setting to
126
127    Raises:
128        KeyError: if the setting is not found
129        ValueError: if the value is invalid for the setting
130
131    """
132    if setting not in get_fields():
133        raise KeyError(f"Setting '{setting}' not found in mgplot_defaults.")
134
135    # Basic validation for some settings
136    if setting == "chart_dir" and not isinstance(value, str):
137        raise ValueError(f"chart_dir must be a string, got {type(value)}")
138    if setting == "dpi" and (not isinstance(value, int) or value <= 0):
139        raise ValueError(f"dpi must be a positive integer, got {value}")
140    if setting == "max_ticks" and (not isinstance(value, int) or value <= 0):
141        raise ValueError(f"max_ticks must be a positive integer, got {value}")
142
143    setattr(mgplot_defaults, setting, value)

Set a setting in the global settings.

Args: setting: str - name of the setting to set (see get_setting()) value: Any - the value to set the setting to

Raises: KeyError: if the setting is not found ValueError: if the value is invalid for the setting

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:
294def summary_plot(data: DataT, **kwargs: Unpack[SummaryKwargs]) -> Axes:
295    """Plot a summary of historical data for a given DataFrame.
296
297    Args:
298        data: DataFrame containing the summary data. The column names are
299              used as labels for the plot.
300        kwargs: Additional arguments for the plot, including middle (float),
301               plot_type (str), verbose (bool), and standard plotting options.
302
303    Returns:
304        Axes: A matplotlib Axes object containing the summary plot.
305
306    Raises:
307        TypeError: If data is not a DataFrame.
308
309    """
310    # --- check the kwargs
311    report_kwargs(caller=ME, **kwargs)
312    validate_kwargs(schema=SummaryKwargs, caller=ME, **kwargs)
313
314    # --- check the data
315    data = check_clean_timeseries(data, ME)
316    if not isinstance(data, DataFrame):
317        raise TypeError("data must be a pandas DataFrame for summary_plot()")
318
319    # --- legend
320    kwargs["legend"] = kwargs.get(
321        "legend",
322        {
323            # put the legend below the x-axis label
324            "loc": "upper center",
325            "fontsize": "xx-small",
326            "bbox_to_anchor": (0.5, -0.125),
327            "ncol": 4,
328        },
329    )
330
331    # --- and plot it ...
332    ax, plot_type = plot_the_data(data, **kwargs)
333    label_x_axis(
334        kwargs.get("plot_from", DEFAULT_PLOT_FROM),
335        label=kwargs.get("xlabel", ""),
336        plot_type=plot_type,
337        ax=ax,
338        df=data,
339    )
340    mark_reference_lines(plot_type, ax)
341
342    return ax

Plot a summary of historical data for a given DataFrame.

Args: data: DataFrame containing the summary data. The column names are used as labels for the plot. kwargs: Additional arguments for the plot, including middle (float), plot_type (str), verbose (bool), and standard plotting options.

Returns: Axes: A matplotlib Axes object containing the summary plot.

Raises: TypeError: If data is not a DataFrame.

def summary_plot_finalise(data: ~DataT, **kwargs: Unpack[mgplot.finalisers.SumPFKwargs]) -> None:
262def summary_plot_finalise(
263    data: DataT,
264    **kwargs: Unpack[SumPFKwargs],
265) -> None:
266    """Call summary_plot() and finalise_plot().
267
268    This is more complex than most of the above convenience methods as it
269    creates multiple plots (one for each plot type).
270
271    Args:
272        data: DataFrame containing the summary data. The index must be a PeriodIndex.
273        kwargs: Combined summary plot and finalise plot keyword arguments.
274
275    Raises:
276        TypeError: If data is not a DataFrame with a PeriodIndex.
277        IndexError: If DataFrame is empty.
278
279    """
280    # --- validate data type and structure
281    if not isinstance(data, DataFrame) or not isinstance(data.index, PeriodIndex):
282        raise TypeError("Data must be a DataFrame with a PeriodIndex.")
283
284    if data.empty or len(data.index) == 0:
285        raise ValueError("DataFrame cannot be empty")
286
287    validate_kwargs(schema=SumPFKwargs, caller="summary_plot_finalise", **kwargs)
288
289    # --- set default title with bounds checking
290    kwargs["title"] = kwargs.get("title", f"Summary at {label_period(data.index[-1])}")
291    kwargs["preserve_lims"] = kwargs.get("preserve_lims", True)
292
293    # --- handle plot_from parameter with bounds checking
294    start: int | Period | None = kwargs.get("plot_from", 0)
295    if start is None:
296        start = data.index[0]
297    elif isinstance(start, int):
298        if abs(start) >= len(data.index):
299            raise IndexError(
300                f"plot_from index {start} out of range for DataFrame with {len(data.index)} rows"
301            )
302        start = data.index[start]
303
304    kwargs["plot_from"] = start
305    if not isinstance(start, Period):
306        raise TypeError("plot_from must be a Period or convertible to one")
307
308    # --- create plots for each plot type
309    pre_tag: str = kwargs.get("pre_tag", "")
310    for plot_type in SUMMARY_PLOT_TYPES:
311        plot_kwargs = kwargs.copy()  # Avoid modifying original kwargs
312        plot_kwargs["plot_type"] = plot_type
313        plot_kwargs["pre_tag"] = pre_tag + plot_type
314
315        plot_then_finalise(
316            data,
317            function=summary_plot,
318            **plot_kwargs,
319        )

Call summary_plot() and finalise_plot().

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

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

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