mgplot

mgplot

Package to provide a frontend to matplotlib for working with timeseries data that is indexed with a PeriodIndex.

  1"""
  2mgplot
  3------
  4
  5Package to provide a frontend to matplotlib for working
  6with timeseries data that is indexed with a PeriodIndex.
  7"""
  8
  9# --- version and author
 10import importlib.metadata
 11
 12# --- local imports
 13#    Do not import the utilities, axis_utils nor keyword_checking modules here.
 14from mgplot.bar_plot import bar_plot, BarKwargs
 15from mgplot.line_plot import line_plot, LineKwargs
 16from mgplot.seastrend_plot import seastrend_plot
 17from mgplot.postcovid_plot import postcovid_plot, PostcovidKwargs
 18from mgplot.run_plot import run_plot, RunKwargs
 19from mgplot.revision_plot import revision_plot
 20from mgplot.growth_plot import (
 21    growth_plot,
 22    GrowthKwargs,
 23    series_growth_plot,
 24    SeriesGrowthKwargs,
 25    calc_growth,
 26)
 27from mgplot.summary_plot import summary_plot, SummaryKwargs
 28from mgplot.multi_plot import plot_then_finalise, multi_start, multi_column
 29from mgplot.finalisers import (
 30    bar_plot_finalise,
 31    line_plot_finalise,
 32    postcovid_plot_finalise,
 33    growth_plot_finalise,
 34    revision_plot_finalise,
 35    run_plot_finalise,
 36    seastrend_plot_finalise,
 37    series_growth_plot_finalise,
 38    summary_plot_finalise,
 39)
 40from mgplot.finalise_plot import finalise_plot, FinaliseKwargs
 41from mgplot.colors import (
 42    get_color,
 43    get_party_palette,
 44    colorise_list,
 45    contrast,
 46    abbreviate_state,
 47    state_names,
 48    state_abbrs,
 49)
 50from mgplot.settings import (
 51    get_setting,
 52    set_setting,
 53    set_chart_dir,
 54    clear_chart_dir,
 55)
 56
 57
 58# --- version and author
 59try:
 60    __version__ = importlib.metadata.version(__name__)
 61except importlib.metadata.PackageNotFoundError:
 62    __version__ = "0.0.0"  # Fallback for development mode
 63__author__ = "Bryan Palmer"
 64
 65
 66# --- public API
 67__all__ = (
 68    "__version__",
 69    "__author__",
 70    # --- settings
 71    "get_setting",
 72    "set_setting",
 73    "set_chart_dir",
 74    "clear_chart_dir",
 75    # --- colors
 76    "get_color",
 77    "get_party_palette",
 78    "colorise_list",
 79    "contrast",
 80    "abbreviate_state",
 81    "state_names",
 82    "state_abbrs",
 83    # --- bar plot
 84    "bar_plot",
 85    "BarKwargs",
 86    # --- line plot
 87    "line_plot",
 88    "LineKwargs",
 89    # --- seasonal + trend plot
 90    "seastrend_plot",
 91    # --- post-COVID plot
 92    "postcovid_plot",
 93    "PostcovidKwargs",
 94    # --- run plot
 95    "run_plot",
 96    "RunKwargs",
 97    # --- revision plot
 98    "revision_plot",
 99    # --- growth plot
100    "growth_plot",
101    "GrowthKwargs",
102    "series_growth_plot",
103    "SeriesGrowthKwargs",
104    "calc_growth",
105    # --- summary plot
106    "summary_plot",
107    "SummaryKwargs",
108    # --- multi plot
109    "multi_start",
110    "multi_column",
111    "plot_then_finalise",
112    # --- finalise plot
113    "finalise_plot",
114    "FinaliseKwargs",
115    # --- finalisers
116    "bar_plot_finalise",
117    "line_plot_finalise",
118    "postcovid_plot_finalise",
119    "growth_plot_finalise",
120    "revision_plot_finalise",
121    "run_plot_finalise",
122    "seastrend_plot_finalise",
123    "series_growth_plot_finalise",
124    "summary_plot_finalise",
125    # --- The rest are internal use only
126)
127# __pdoc__: dict[str, Any] = {"test": False}  # hide submodules from documentation
__version__ = '0.2.1a1'
__author__ = 'Bryan Palmer'
def get_setting(setting: str) -> Any:
 88def get_setting(setting: str) -> Any:
 89    """
 90    Get a setting from the global settings.
 91
 92    Arguments:
 93    - setting: str - name of the setting to get. The possible settings are:
 94        - file_type: str - the file type to use for saving plots
 95        - figsize: tuple[float, float] - the figure size to use for plots
 96        - file_dpi: int - the DPI to use for saving plots
 97        - line_narrow: float - the line width for narrow lines
 98        - line_normal: float - the line width for normal lines
 99        - line_wide: float - the line width for wide lines
100        - bar_width: float - the width of bars in bar plots
101        - legend_font_size: float | str - the font size for legends
102        - legend: dict[str, Any] - the legend settings
103        - colors: dict[int, list[str]] - a dictionary of colors for
104          different numbers of lines
105        - chart_dir: str - the directory to save charts in
106
107    Raises:
108        - KeyError: if the setting is not found
109
110    Returns:
111        - value: Any - the value of the setting
112    """
113    if setting not in _mgplot_defaults:
114        raise KeyError(f"Setting '{setting}' not found in _mgplot_defaults.")
115    return _mgplot_defaults[setting]  # type: ignore[literal-required]

Get a setting from the global settings.

Arguments:

  • setting: str - name of the setting to get. The possible settings are:
    • file_type: str - the file type to use for saving plots
    • figsize: tuple[float, float] - the figure size to use for plots
    • file_dpi: int - the DPI to use for saving plots
    • line_narrow: float - the line width for narrow lines
    • line_normal: float - the line width for normal lines
    • line_wide: float - the line width for wide lines
    • bar_width: float - the width of bars in bar plots
    • legend_font_size: float | str - the font size for legends
    • legend: dict[str, Any] - the legend settings
    • colors: dict[int, list[str]] - a dictionary of colors for different numbers of lines
    • chart_dir: str - the directory to save charts in

Raises: - KeyError: if the setting is not found

Returns: - value: Any - the value of the setting

def set_setting(setting: str, value: Any) -> None:
118def set_setting(setting: str, value: Any) -> None:
119    """
120    Set a setting in the global settings.
121    Raises KeyError if the setting is not found.
122
123    Arguments:
124        - setting: str - name of the setting to set (see get_setting())
125        - value: Any - the value to set the setting to
126    """
127
128    if setting not in _mgplot_defaults:
129        raise KeyError(f"Setting '{setting}' not found in _mgplot_defaults.")
130    _mgplot_defaults[setting] = value  # type: ignore[literal-required]

Set a setting in the global settings. Raises KeyError if the setting is not found.

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

def set_chart_dir(chart_dir: str) -> None:
149def set_chart_dir(chart_dir: str) -> None:
150    """
151    A function to set a global chart directory for finalise_plot(),
152    so that it does not need to be included as an argument in each
153    call to finalise_plot(). Create the directory if it does not exist.
154
155    Note: Path.mkdir() may raise an exception if a directory cannot be created.
156
157    Note: This is a wrapper for set_setting() to set the chart_dir setting, and
158    create the directory if it does not exist.
159
160    Arguments:
161        - chart_dir: str - the directory to set as the chart directory
162    """
163
164    if not chart_dir:
165        chart_dir = "."  # avoid the empty string
166    Path(chart_dir).mkdir(parents=True, exist_ok=True)
167    set_setting("chart_dir", chart_dir)

A function to set a global chart directory for finalise_plot(), so that it does not need to be included as an argument in each call to finalise_plot(). Create the directory if it does not exist.

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.

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

def clear_chart_dir() -> None:
133def clear_chart_dir() -> None:
134    """
135    Remove all graph-image files from the global chart_dir.
136    This is a convenience function to remove all files from the
137    chart_dir directory. It does not remove the directory itself.
138    Note: the function creates the directory if it does not exist.
139    """
140
141    chart_dir = get_setting("chart_dir")
142    Path(chart_dir).mkdir(parents=True, exist_ok=True)
143    for ext in ("png", "svg", "jpg", "jpeg"):
144        for fs_object in Path(chart_dir).glob(f"*.{ext}"):
145            if fs_object.is_file():
146                fs_object.unlink()

Remove all graph-image files from the global chart_dir. This is a convenience function to remove all files from the chart_dir directory. It does not remove the directory itself. Note: the function creates the directory if it does not exist.

def get_color(s: str) -> str:
35def get_color(s: str) -> str:
36    """
37    Return a matplotlib color for a party label
38    or an Australian state/territory.
39    """
40
41    color_map = {
42        # --- Australian states and territories
43        ("wa", "western australia"): "gold",
44        ("sa", "south australia"): "red",
45        ("nt", "northern territory"): "#CC7722",  # ochre
46        ("nsw", "new south wales"): "deepskyblue",
47        ("act", "australian capital territory"): "blue",
48        ("vic", "victoria"): "navy",
49        ("tas", "tasmania"): "seagreen",  # bottle green #006A4E?
50        ("qld", "queensland"): "#c32148",  # a lighter maroon
51        ("australia", "aus"): "grey",
52        # --- political parties
53        ("dissatisfied",): "darkorange",  # must be before satisfied
54        ("satisfied",): "mediumblue",
55        (
56            "lnp",
57            "l/np",
58            "liberal",
59            "liberals",
60            "coalition",
61            "dutton",
62            "ley",
63            "liberal and/or nationals",
64        ): "royalblue",
65        (
66            "nat",
67            "nats",
68            "national",
69            "nationals",
70        ): "forestgreen",
71        (
72            "alp",
73            "labor",
74            "albanese",
75        ): "#dd0000",
76        (
77            "grn",
78            "green",
79            "greens",
80        ): "limegreen",
81        (
82            "other",
83            "oth",
84        ): "darkorange",
85    }
86
87    for find_me, return_me in color_map.items():
88        if any(x == s.lower() for x in find_me):
89            return return_me
90
91    return "darkgrey"

Return a matplotlib color for a party label or an Australian state/territory.

def get_party_palette(party_text: str) -> str:
14def get_party_palette(party_text: str) -> str:
15    """
16    Return a matplotlib color-map name based on party_text.
17    Works for Australian major political parties.
18    """
19
20    # Note: light to dark maps work best
21    match party_text.lower():
22        case "alp" | "labor":
23            return "Reds"
24        case "l/np" | "coalition":
25            return "Blues"
26        case "grn" | "green" | "greens":
27            return "Greens"
28        case "oth" | "other":
29            return "YlOrBr"
30        case "onp" | "one nation":
31            return "YlGnBu"
32    return "Purples"

Return a matplotlib color-map name based on party_text. Works for Australian major political parties.

def colorise_list(party_list: Iterable) -> list[str]:
94def colorise_list(party_list: Iterable) -> list[str]:
95    """
96    Return a list of party/state colors for a party_list.
97    """
98
99    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:
102def contrast(orig_color: str) -> str:
103    """
104    Provide a constrasting color to any party color
105    generated by get_color() above.
106    """
107
108    new_color = "black"
109    match orig_color:
110        case "royalblue":
111            new_color = "indianred"
112        case "indianred":
113            new_color = "mediumblue"
114
115        case "darkorange":
116            new_color = "mediumblue"
117        case "mediumblue":
118            new_color = "darkorange"
119
120        case "mediumseagreen":
121            new_color = "darkblue"
122
123        case "darkgrey":
124            new_color = "hotpink"
125
126    return new_color

Provide a constrasting color to any party color generated by get_color() above.

def abbreviate_state(state: str) -> str:
157def abbreviate_state(state: str) -> str:
158    """
159    A function to abbreviate long-form state
160    names.
161
162    Arguments
163    -   state: the long-form state name.
164
165    Return the abbreviation for a state name.
166    """
167
168    return _state_names_multi.get(state.lower(), state)

A function to abbreviate long-form state names.

Arguments

  • state: the long-form state name.

Return the abbreviation for a state name.

state_names = ('New South Wales', 'Victoria', 'Queensland', 'South Australia', 'Western Australia', 'Tasmania', 'Northern Territory', 'Australian Capital Territory')
state_abbrs = ('NSW', 'Vic', 'Qld', 'SA', 'WA', 'Tas', 'NT', 'ACT')
def bar_plot( data: ~DataT, **kwargs: Unpack[BarKwargs]) -> matplotlib.axes._axes.Axes:
188def bar_plot(data: DataT, **kwargs: Unpack[BarKwargs]) -> Axes:
189    """
190    Create a bar plot from the given data. Each column in the DataFrame
191    will be stacked on top of each other, with positive values above
192    zero and negative values below zero.
193
194    Parameters
195    - data: Series - The data to plot. Can be a DataFrame or a Series.
196    - **kwargs: BarKwargs - Additional keyword arguments for customization.
197      (see BarKwargs for details)
198
199    Note: This function does not assume all data is timeseries with a PeriodIndex,
200
201    Returns
202    - axes: Axes - The axes for the plot.
203    """
204
205    # --- check the kwargs
206    report_kwargs(caller=ME, **kwargs)
207    validate_kwargs(schema=BarKwargs, caller=ME, **kwargs)
208
209    # --- get the data
210    # no call to check_clean_timeseries here, as bar plots are not
211    # necessarily timeseries data. If the data is a Series, it will be
212    # converted to a DataFrame with a single column.
213    df = DataFrame(data)  # really we are only plotting DataFrames
214    df, kwargs_d = constrain_data(df, **kwargs)
215    item_count = len(df.columns)
216
217    # --- deal with complete PeriodIdex indicies
218    if not is_categorical(df):
219        print("Warning: bar_plot is not designed for incomplete or non-categorical data indexes.")
220    saved_pi = map_periodindex(df)
221    if saved_pi is not None:
222        df = saved_pi[0]  # extract the reindexed DataFrame from the PeriodIndex
223
224    # --- set up the default arguments
225    chart_defaults: dict[str, Any] = {
226        "stacked": False,
227        "max_ticks": 10,
228        "label_series": item_count > 1,
229    }
230    chart_args = {k: kwargs_d.get(k, v) for k, v in chart_defaults.items()}
231
232    bar_defaults: dict[str, Any] = {
233        "color": get_color_list(item_count),
234        "width": get_setting("bar_width"),
235        "label_series": (item_count > 1),
236    }
237    above = kwargs_d.get("above", False)
238    anno_args = {
239        "annotate": kwargs_d.get("annotate", False),
240        "fontsize": kwargs_d.get("fontsize", "small"),
241        "fontname": kwargs_d.get("fontname", "Helvetica"),
242        "rotation": kwargs_d.get("rotation", 0),
243        "rounding": kwargs_d.get("rounding", True),
244        "color": kwargs_d.get("annotate_color", "black" if above else "white"),
245        "above": above,
246    }
247    bar_args, remaining_kwargs = apply_defaults(item_count, bar_defaults, kwargs_d)
248
249    # --- plot the data
250    axes, remaining_kwargs = get_axes(**remaining_kwargs)
251    if chart_args["stacked"]:
252        stacked(axes, df, anno_args, **bar_args)
253    else:
254        grouped(axes, df, anno_args, **bar_args)
255
256    # --- handle complete periodIndex data and label rotation
257    if saved_pi is not None:
258        set_labels(axes, saved_pi[1], chart_args["max_ticks"])
259    else:
260        plt.xticks(rotation=90)
261
262    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.

Parameters

  • data: Series - 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.
class BarKwargs(mgplot.keyword_checking.BaseKwargs):
38class BarKwargs(BaseKwargs):
39    """Keyword arguments for the bar_plot function."""
40
41    # --- options for the entire bar plot
42    ax: NotRequired[Axes | None]
43    stacked: NotRequired[bool]
44    max_ticks: NotRequired[int]
45    plot_from: NotRequired[int | Period]
46    # --- options for each bar ...
47    color: NotRequired[str | Sequence[str]]
48    label_series: NotRequired[bool | Sequence[bool]]
49    width: NotRequired[float | int | Sequence[float | int]]
50    # --- options for bar annotations
51    annotate: NotRequired[bool]
52    fontsize: NotRequired[int | float | str]
53    fontname: NotRequired[str]
54    rounding: NotRequired[int]
55    rotation: NotRequired[int | float]
56    annotate_color: NotRequired[str]
57    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]
def line_plot( data: ~DataT, **kwargs: Unpack[LineKwargs]) -> matplotlib.axes._axes.Axes:
128def line_plot(data: DataT, **kwargs: Unpack[LineKwargs]) -> Axes:
129    """
130    Build a single plot from the data passed in.
131    This can be a single- or multiple-line plot.
132    Return the axes object for the build.
133
134    Arguments:
135    - data: DataFrame | Series - data to plot
136    - kwargs: Unpack[LineKwargs]
137
138    Returns:
139    - axes: Axes - the axes object for the plot
140    """
141
142    # --- check the kwargs
143    report_kwargs(caller=ME, **kwargs)
144    validate_kwargs(schema=LineKwargs, caller=ME, **kwargs)
145
146    # --- check the data
147    data = check_clean_timeseries(data, ME)
148    df = DataFrame(data)  # we are only plotting DataFrames
149    df, kwargs_d = constrain_data(df, **kwargs)
150
151    # --- convert PeriodIndex to Integer Index
152    saved_pi = map_periodindex(df)
153    if saved_pi is not None:
154        df = saved_pi[0]
155
156    # --- some special defaults
157    kwargs_d["label_series"] = (
158        kwargs_d.get("label_series", True) if len(df.columns) > 1 else kwargs_d.get("label_series", False)
159    )
160
161    # --- Let's plot
162    axes, kwargs_d = get_axes(**kwargs_d)  # get the axes to plot on
163    if df.empty or df.isna().all().all():
164        # Note: finalise plot should ignore an empty axes object
165        print(f"Warning: No data to plot in {ME}().")
166        return axes
167
168    # --- get the arguments for each line we will plot ...
169    item_count = len(df.columns)
170    num_data_points = len(df)
171    swce, kwargs_d = _get_style_width_color_etc(item_count, num_data_points, **kwargs_d)
172
173    for i, column in enumerate(df.columns):
174        series = df[column]
175        series = series.dropna() if "dropna" in swce and swce["dropna"][i] else series
176        if series.empty or series.isna().all():
177            print(f"Warning: No data to plot for {column} in line_plot().")
178            continue
179
180        series.plot(
181            # Note: pandas will plot PeriodIndex against their ordinal values
182            ls=swce["style"][i],
183            lw=swce["width"][i],
184            color=swce["color"][i],
185            alpha=swce["alpha"][i],
186            marker=swce["marker"][i],
187            ms=swce["markersize"][i],
188            drawstyle=swce["drawstyle"][i],
189            label=(column if "label_series" in swce and swce["label_series"][i] else f"_{column}_"),
190            ax=axes,
191        )
192
193        if swce["annotate"][i] is None or not swce["annotate"][i]:
194            continue
195
196        color = swce["color"][i] if swce["annotate_color"][i] is True else swce["annotate_color"][i]
197        annotate_series(
198            series,
199            axes,
200            color=color,
201            rounding=swce["rounding"][i],
202            fontsize=swce["fontsize"][i],
203            fontname=swce["fontname"][i],
204            rotation=swce["rotation"][i],
205        )
206
207    # --- set the labels
208    if saved_pi is not None:
209        set_labels(axes, saved_pi[1], kwargs_d.get("max_ticks", get_setting("max_ticks")))
210
211    return axes

Build a single plot from the data passed in. This can be a single- or multiple-line plot. Return the axes object for the build.

Arguments:

  • data: DataFrame | Series - data to plot
  • kwargs: Unpack[LineKwargs]

Returns:

  • axes: Axes - the axes object for the plot
class LineKwargs(mgplot.keyword_checking.BaseKwargs):
31class LineKwargs(BaseKwargs):
32    """Keyword arguments for the line_plot function."""
33
34    # --- options for the entire line plot
35    ax: NotRequired[Axes | None]
36    style: NotRequired[str | Sequence[str]]
37    width: NotRequired[float | int | Sequence[float | int]]
38    color: NotRequired[str | Sequence[str]]
39    alpha: NotRequired[float | Sequence[float]]
40    drawstyle: NotRequired[str | Sequence[str] | None]
41    marker: NotRequired[str | Sequence[str] | None]
42    markersize: NotRequired[float | Sequence[float] | int | None]
43    dropna: NotRequired[bool | Sequence[bool]]
44    annotate: NotRequired[bool | Sequence[bool]]
45    rounding: NotRequired[Sequence[int | bool] | int | bool | None]
46    fontsize: NotRequired[Sequence[str | int | float] | str | int | float]
47    fontname: NotRequired[str | Sequence[str]]
48    rotation: NotRequired[Sequence[int | float] | int | float]
49    annotate_color: NotRequired[str | Sequence[str] | bool | Sequence[bool] | None]
50    plot_from: NotRequired[int | Period | None]
51    label_series: NotRequired[bool | Sequence[bool] | None]
52    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]
def seastrend_plot( data: ~DataT, **kwargs: Unpack[LineKwargs]) -> matplotlib.axes._axes.Axes:
22def seastrend_plot(data: DataT, **kwargs: Unpack[LineKwargs]) -> Axes:
23    """
24    Publish a DataFrame, where the first column is seasonally
25    adjusted data, and the second column is trend data.
26
27    Aguments:
28    - data: DataFrame - the data to plot with the first column
29      being the seasonally adjusted data, and the second column
30      being the trend data.
31    The remaining arguments are the same as those passed to
32    line_plot().
33
34    Returns:
35    - a matplotlib Axes object
36    """
37
38    # Note: we will rely on the line_plot() function to do most of the work.
39    # including constraining the data to the plot_from keyword argument.
40
41    # --- check the kwargs
42    report_kwargs(caller=ME, **kwargs)
43    validate_kwargs(schema=LineKwargs, caller=ME, **kwargs)
44
45    # --- check the data
46    data = check_clean_timeseries(data, ME)
47    if len(data.columns) < 2:
48        raise ValueError("seas_trend_plot() expects a DataFrame data item with at least 2 columns.")
49
50    # --- defaults if not in kwargs
51    kwargs["color"] = kwargs.get("color", get_color_list(2))
52    kwargs["width"] = kwargs.get("width", [get_setting("line_normal"), get_setting("line_wide")])
53    kwargs["style"] = kwargs.get("style", ["-", "-"])
54    kwargs["annotate"] = kwargs.get("annotate", [True, False])
55    kwargs["rounding"] = kwargs.get("rounding", True)
56
57    # series breaks are common in seas-trend data
58    kwargs["dropna"] = kwargs.get("dropna", False)
59
60    axes = line_plot(
61        data,
62        **kwargs,
63    )
64
65    return axes

Publish a DataFrame, where the first column is seasonally adjusted data, and the second column is trend data.

Aguments:

  • data: DataFrame - the data to plot with the first column being the seasonally adjusted data, and the second column being the trend data. The remaining arguments are the same as those passed to line_plot().

Returns:

  • a matplotlib Axes object
def postcovid_plot( data: ~DataT, **kwargs: Unpack[PostcovidKwargs]) -> matplotlib.axes._axes.Axes:
 51def postcovid_plot(data: DataT, **kwargs: Unpack[PostcovidKwargs]) -> Axes:
 52    """
 53    Plots a series with a PeriodIndex.
 54
 55    Arguments
 56    - data - the series to be plotted (note that this function
 57      is designed to work with a single series, not a DataFrame).
 58    - **kwargs - same as for line_plot() and finalise_plot().
 59
 60    Raises:
 61    - TypeError if series is not a pandas Series
 62    - TypeError if series does not have a PeriodIndex
 63    - ValueError if series does not have a D, M or Q frequency
 64    - ValueError if regression start is after regression end
 65    """
 66
 67    # --- check the kwargs
 68    report_kwargs(caller=ME, **kwargs)
 69    validate_kwargs(schema=PostcovidKwargs, caller=ME, **kwargs)
 70
 71    # --- check the data
 72    data = check_clean_timeseries(data, ME)
 73    if not isinstance(data, Series):
 74        raise TypeError("The series argument must be a pandas Series")
 75    series: Series = data
 76    series_index = PeriodIndex(series.index)  # syntactic sugar for type hinting
 77    if series_index.freqstr[:1] not in ("Q", "M", "D"):
 78        raise ValueError("The series index must have a D, M or Q freq")
 79
 80    # rely on line_plot() to validate kwargs
 81    if "plot_from" in kwargs:
 82        print("Warning: the 'plot_from' argument is ignored in postcovid_plot().")
 83        del kwargs["plot_from"]
 84
 85    # --- plot COVID counterfactural
 86    freq = PeriodIndex(series.index).freqstr  # syntactic sugar for type hinting
 87    match freq[0]:
 88        case "Q":
 89            start_regression = Period("2014Q4", freq=freq)
 90            end_regression = Period("2019Q4", freq=freq)
 91        case "M":
 92            start_regression = Period("2015-01", freq=freq)
 93            end_regression = Period("2020-01", freq=freq)
 94        case "D":
 95            start_regression = Period("2015-01-01", freq=freq)
 96            end_regression = Period("2020-01-01", freq=freq)
 97
 98    start_regression = Period(kwargs.pop("start_r", start_regression), freq=freq)
 99    end_regression = Period(kwargs.pop("end_r", end_regression), freq=freq)
100    if start_regression >= end_regression:
101        raise ValueError("Start period must be before end period")
102
103    # --- combine data and projection
104    recent = series[series.index >= start_regression].copy()
105    recent.name = "Series"
106    projection = get_projection(recent, end_regression)
107    projection.name = "Pre-COVID projection"
108    data_set = DataFrame([projection, recent]).T
109
110    # --- activate plot settings
111    kwargs["width"] = kwargs.pop(
112        "width", (get_setting("line_normal"), get_setting("line_wide"))
113    )  # series line is thicker than projection
114    kwargs["style"] = kwargs.pop("style", ("--", "-"))  # dashed regression line
115    kwargs["label_series"] = kwargs.pop("label_series", True)
116    kwargs["annotate"] = kwargs.pop("annotate", (False, True))  # annotate series only
117    kwargs["color"] = kwargs.pop("color", ("darkblue", "#dd0000"))
118
119    return line_plot(
120        data_set,
121        **cast(LineKwargs, kwargs),
122    )

Plots a series with a PeriodIndex.

Arguments

  • data - the series to be plotted (note that this function is designed to work with a single series, not a DataFrame).
  • **kwargs - same as for line_plot() and finalise_plot().

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
class PostcovidKwargs(mgplot.LineKwargs):
26class PostcovidKwargs(LineKwargs):
27    "Keyword arguments for the post-COVID plot."
28
29    start_r: NotRequired[Period]  # start of regression period
30    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]
def run_plot( data: ~DataT, **kwargs: Unpack[RunKwargs]) -> matplotlib.axes._axes.Axes:
101def run_plot(data: DataT, **kwargs: Unpack[RunKwargs]) -> Axes:
102    """Plot a series of percentage rates, highlighting the increasing runs.
103
104    Arguments
105     - data - ordered pandas Series of percentages, with PeriodIndex
106     - **kwargs: RunKwargs
107
108    Return
109     - matplotlib Axes object"""
110
111    # --- check the kwargs
112    report_kwargs(caller="run_plot", **kwargs)
113    validate_kwargs(schema=RunKwargs, caller=ME, **kwargs)
114
115    # --- check the data
116    series = check_clean_timeseries(data, ME)
117    if not isinstance(series, Series):
118        raise TypeError("series must be a pandas Series for run_plot()")
119    series, kwargs_d = constrain_data(series, **kwargs)
120
121    # --- convert PeriodIndex if needed
122    saved_pi = map_periodindex(series)
123    if saved_pi is not None:
124        series = saved_pi[0]
125
126    # --- default arguments - in **kwargs_d
127    kwargs_d["threshold"] = kwargs_d.get("threshold", 0.1)
128    kwargs_d["direction"] = kwargs_d.get("direction", "both")
129    kwargs_d["rounding"] = kwargs_d.get("rounding", 2)
130    kwargs_d["highlight"] = kwargs_d.get(
131        "highlight",
132        (
133            ("gold", "skyblue")
134            if kwargs_d["direction"] == "both"
135            else "gold"
136            if kwargs_d["direction"] == "up"
137            else "skyblue"
138        ),
139    )
140    kwargs_d["color"] = kwargs_d.get("color", "darkblue")
141
142    # --- plot the line
143    kwargs_d["drawstyle"] = kwargs_d.get("drawstyle", "steps-post")
144    lp_kwargs = limit_kwargs(LineKwargs, **kwargs_d)
145    axes = line_plot(series, **lp_kwargs)
146
147    # plot the runs
148    match kwargs["direction"]:
149        case "up":
150            _plot_runs(axes, series, up=True, **kwargs_d)
151        case "down":
152            _plot_runs(axes, series, up=False, **kwargs_d)
153        case "both":
154            _plot_runs(axes, series, up=True, **kwargs_d)
155            _plot_runs(axes, series, up=False, **kwargs_d)
156        case _:
157            raise ValueError(
158                f"Invalid value for direction: {kwargs['direction']}. Expected 'up', 'down', or 'both'."
159            )
160
161    # --- set the labels
162    if saved_pi is not None:
163        set_labels(axes, saved_pi[1], kwargs.get("max_ticks", get_setting("max_ticks")))
164
165    return axes

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

Arguments

  • data - ordered pandas Series of percentages, with PeriodIndex
  • **kwargs: RunKwargs

Return

  • matplotlib Axes object
class RunKwargs(mgplot.LineKwargs):
29class RunKwargs(LineKwargs):
30    """Keyword arguments for the run_plot function."""
31
32    threshold: NotRequired[float]
33    highlight: NotRequired[str | Sequence[str]]
34    direction: NotRequired[str]

Keyword arguments for the run_plot function.

threshold: NotRequired[float]
highlight: NotRequired[str | Sequence[str]]
direction: NotRequired[str]
def revision_plot( data: ~DataT, **kwargs: Unpack[LineKwargs]) -> matplotlib.axes._axes.Axes:
26def revision_plot(data: DataT, **kwargs: Unpack[LineKwargs]) -> Axes:
27    """
28    Plot the revisions to ABS data.
29
30    Arguments
31    data: pd.DataFrame - the data to plot, the DataFrame has a
32        column for each data revision
33    kwargs - additional keyword arguments for the line_plot function.
34    """
35
36    # --- check the kwargs and data
37    report_kwargs(caller=ME, **kwargs)
38    validate_kwargs(schema=LineKwargs, caller=ME, **kwargs)
39    data = check_clean_timeseries(data, ME)
40
41    # --- additional checks
42    if not isinstance(data, DataFrame):
43        print(f"{ME}() requires a DataFrame with columns for each revision, not a Series or any other type.")
44
45    # --- critical defaults
46    kwargs["plot_from"] = kwargs.get("plot_from", -15)
47    kwargs["annotate"] = kwargs.get("annotate", True)
48    kwargs["annotate_color"] = kwargs.get("annotate_color", "black")
49    kwargs["rounding"] = kwargs.get("rounding", 3)
50
51    # --- plot
52    axes = line_plot(data, **kwargs)
53
54    return axes

Plot the revisions to ABS data.

Arguments data: pd.DataFrame - the data to plot, the DataFrame has a column for each data revision kwargs - additional keyword arguments for the line_plot function.

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

Plot annual growth (as a line) and periodic growth (as bars) on the same axes.

Args:

  • data: A pandas DataFrame with two columns:
  • kwargs: GrowthKwargs
    Returns:

  • axes: The matplotlib Axes object.

Raises:

  • TypeError if the annual and periodic arguments are not pandas Series.
  • TypeError if the annual index is not a PeriodIndex.
  • ValueError if the annual and periodic series do not have the same index.
class GrowthKwargs(mgplot.keyword_checking.BaseKwargs):
34class GrowthKwargs(BaseKwargs):
35    """Keyword arguments for the growth_plot function."""
36
37    # --- common options
38    ax: NotRequired[Axes | None]
39    plot_from: NotRequired[int | Period]
40    label_series: NotRequired[bool]
41    max_ticks: NotRequired[int]
42    # --- options passed to the line plot
43    line_width: NotRequired[float | int]
44    line_color: NotRequired[str]
45    line_style: NotRequired[str]
46    annotate_line: NotRequired[bool]
47    line_rounding: NotRequired[bool | int]
48    line_fontsize: NotRequired[str | int | float]
49    line_fontname: NotRequired[str]
50    line_anno_color: NotRequired[str]
51    # --- options passed to the bar plot
52    annotate_bars: NotRequired[bool]
53    bar_fontsize: NotRequired[str | int | float]
54    bar_fontname: NotRequired[str]
55    bar_rounding: NotRequired[int]
56    bar_width: NotRequired[float]
57    bar_color: NotRequired[str]
58    bar_anno_color: NotRequired[str]
59    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]
def series_growth_plot( data: ~DataT, **kwargs: Unpack[SeriesGrowthKwargs]) -> matplotlib.axes._axes.Axes:
220def series_growth_plot(
221    data: DataT,
222    **kwargs: Unpack[SeriesGrowthKwargs],
223) -> Axes:
224    """
225    Plot annual and periodic growth in percentage terms from
226    a pandas Series, and finalise the plot.
227
228    Args:
229    -   data: A pandas Series with an appropriate PeriodIndex.
230    -   kwargs: SeriesGrowthKwargs
231        -   takes much the same kwargs as for growth_plot()
232    """
233
234    # --- check the kwargs
235    me = "series_growth_plot"
236    report_kwargs(caller=me, **kwargs)
237    validate_kwargs(SeriesGrowthKwargs, caller=me, **kwargs)
238
239    # --- sanity checks
240    if not isinstance(data, Series):
241        raise TypeError("The data argument to series_growth_plot() must be a pandas Series")
242
243    # --- calculate growth and plot - add ylabel
244    ylabel: str | None = kwargs.pop("ylabel", None)
245    if ylabel is not None:
246        print(f"Did you intend to specify a value for the 'ylabel' in {me}()?")
247    ylabel = "Growth (%)" if ylabel is None else ylabel
248    growth = calc_growth(data)
249    ax = growth_plot(growth, **cast(GrowthKwargs, kwargs))
250    ax.set_ylabel(ylabel)
251    return ax

Plot annual and periodic growth in percentage terms from a pandas Series, and finalise the plot.

Args:

  • data: A pandas Series with an appropriate PeriodIndex.
  • kwargs: SeriesGrowthKwargs
    • takes much the same kwargs as for growth_plot()
class SeriesGrowthKwargs(mgplot.GrowthKwargs):
62class SeriesGrowthKwargs(GrowthKwargs):
63    """Keyword arguments for the series_growth_plot function."""
64
65    ylabel: NotRequired[str | None]

Keyword arguments for the series_growth_plot function.

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

Calculate annual and periodic growth for a pandas Series, where the index is a PeriodIndex.

Args:

  • series: A pandas Series with an appropriate PeriodIndex.

Returns a two column DataFrame:

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 summary_plot( data: ~DataT, **kwargs: Unpack[SummaryKwargs]) -> matplotlib.axes._axes.Axes:
200def summary_plot(data: DataT, **kwargs: Unpack[SummaryKwargs]) -> Axes:
201    """Plot a summary of historical data for a given DataFrame.
202
203    Args:x
204    - summary: DataFrame containing the summary data. The column names are
205      used as labels for the plot.
206    - kwargs: additional arguments for the plot, including:
207
208    Returns Axes.
209    """
210
211    # --- check the kwargs
212    me = "summary_plot"
213    report_kwargs(caller=me, **kwargs)
214    validate_kwargs(schema=SummaryKwargs, caller=me, **kwargs)
215
216    # --- check the data
217    data = check_clean_timeseries(data, me)
218    if not isinstance(data, DataFrame):
219        raise TypeError("data must be a pandas DataFrame for summary_plot()")
220    df = DataFrame(data)  # syntactic sugar for type hinting
221
222    # --- optional arguments
223    verbose = kwargs.pop("verbose", False)
224    middle = float(kwargs.pop("middle", 0.8))
225    plot_type = kwargs.pop("plot_type", ZSCORES)
226    kwargs["legend"] = kwargs.get(
227        "legend",
228        {
229            # put the legend below the x-axis label
230            "loc": "upper center",
231            "fontsize": "xx-small",
232            "bbox_to_anchor": (0.5, -0.125),
233            "ncol": 4,
234        },
235    )
236
237    # get the data, calculate z-scores and scaled scores based on the start period
238    subset, kwargsd = constrain_data(df, **kwargs)
239    z_scores, z_scaled = _calculate_z(subset, middle, verbose=verbose)
240
241    # plot as required by the plot_types argument
242    adjusted = z_scores if plot_type == ZSCORES else z_scaled
243    ax = _horizontal_bar_plot(subset, adjusted, middle, plot_type, kwargsd)
244    ax.tick_params(axis="y", labelsize="small")
245    make_legend(ax, kwargsd["legend"])
246    ax.set_xlim(kwargsd.get("xlim"))  # provide space for the labels
247
248    return ax

Plot a summary of historical data for a given DataFrame.

Args:x

  • summary: DataFrame containing the summary data. The column names are used as labels for the plot.
  • kwargs: additional arguments for the plot, including:

Returns Axes.

class SummaryKwargs(mgplot.keyword_checking.BaseKwargs):
36class SummaryKwargs(BaseKwargs):
37    """Keyword arguments for the summary_plot function."""
38
39    ax: NotRequired[Axes | None]
40    verbose: NotRequired[bool]
41    middle: NotRequired[float]
42    plot_type: NotRequired[str]
43    plot_from: NotRequired[int | Period | None]
44    legend: NotRequired[dict[str, Any]]

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 | None]
legend: NotRequired[dict[str, Any]]
def multi_start( data: ~DataT, function: Union[Callable, list[Callable]], starts: Iterable[None | pandas._libs.tslibs.period.Period | int], **kwargs) -> None:
200def multi_start(
201    data: DataT,
202    function: Callable | list[Callable],
203    starts: Iterable[None | Period | int],
204    **kwargs,
205) -> None:
206    """
207    Create multiple plots with different starting points.
208    Each plot will start from the specified starting point.
209
210    Parameters
211    - data: Series | DataFrame - The data to be plotted.
212    - function: Callable | list[Callable] - The plotting function
213      to be used.
214    - starts: Iterable[Period | int | None] - The starting points
215      for each plot (None means use the entire data).
216    - **kwargs: Additional keyword arguments to be passed to
217      the plotting function.
218
219    Returns None.
220
221    Raises
222    - ValueError if the starts is not an iterable of None, Period or int.
223
224    Note: kwargs['tag'] is used to create a unique tag for each plot.
225    """
226
227    # --- sanity checks
228    me = "multi_start"
229    report_kwargs(caller=me, **kwargs)
230    if not isinstance(starts, Iterable):
231        raise ValueError("starts must be an iterable of None, Period or int")
232    # data not checked here, assume it is checked by the called
233    # plot function.
234
235    # --- check the function argument
236    original_tag: Final[str] = kwargs.get("tag", "")
237    first, kwargs["function"] = first_unchain(function)
238    if not kwargs["function"]:
239        del kwargs["function"]  # remove the function key if it is empty
240
241    # --- iterate over the starts
242    for i, start in enumerate(starts):
243        kw = kwargs.copy()  # copy to avoid modifying the original kwargs
244        this_tag = f"{original_tag}_{i}"
245        kw["tag"] = this_tag
246        kw["plot_from"] = start  # rely on plotting function to constrain the data
247        first(data, **kw)

Create multiple plots with different starting points. Each plot will start from the specified starting point.

Parameters

  • data: Series | DataFrame - The data to be plotted.
  • function: Callable | list[Callable] - The plotting function to be used.
  • starts: Iterable[Period | int | None] - The starting points for each plot (None means use the entire data).
  • **kwargs: Additional keyword arguments to be passed to the plotting function.

Returns None.

Raises

  • ValueError if the starts is not an iterable of None, Period or int.

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

def multi_column( data: pandas.core.frame.DataFrame, function: Union[Callable, list[Callable]], **kwargs) -> None:
250def multi_column(
251    data: DataFrame,
252    function: Callable | list[Callable],
253    **kwargs,
254) -> None:
255    """
256    Create multiple plots, one for each column in a DataFrame.
257    The plot title will be the column name.
258
259    Parameters
260    - data: DataFrame - The data to be plotted
261    - function: Callable - The plotting function to be used.
262    - **kwargs: Additional keyword arguments to be passed to
263      the plotting function.
264
265    Returns None.
266    """
267
268    # --- sanity checks
269    me = "multi_column"
270    report_kwargs(caller=me, **kwargs)
271    if not isinstance(data, DataFrame):
272        raise TypeError("data must be a pandas DataFrame for multi_column()")
273    # Otherwise, the data is assumed to be checked by the called
274    # plot function, so we do not check it here.
275
276    # --- check the function argument
277    title_stem = kwargs.get("title", "")
278    tag: Final[str] = kwargs.get("tag", "")
279    first, kwargs["function"] = first_unchain(function)
280    if not kwargs["function"]:
281        del kwargs["function"]  # remove the function key if it is empty
282
283    # --- iterate over the columns
284    for i, col in enumerate(data.columns):
285        series = data[[col]]
286        kwargs["title"] = f"{title_stem}{col}" if title_stem else col
287
288        this_tag = f"_{tag}_{i}".replace("__", "_")
289        kwargs["tag"] = this_tag
290
291        first(series, **kwargs)

Create multiple plots, one for each column in a DataFrame. The plot title will be the column name.

Parameters

  • data: DataFrame - The data to be plotted
  • function: Callable - The plotting function to be used.
  • **kwargs: Additional keyword arguments to be passed to the plotting function.

Returns None.

def plot_then_finalise( data: ~DataT, function: Union[Callable, list[Callable]], **kwargs) -> None:
129def plot_then_finalise(
130    data: DataT,
131    function: Callable | list[Callable],
132    **kwargs,
133) -> None:
134    """
135    Chain a plotting function with the finalise_plot() function.
136    This is designed to be the last function in a chain.
137
138    Parameters
139    - data: Series | DataFrame - The data to be plotted.
140    - function: Callable | list[Callable] - The plotting function
141      to be used.
142    - **kwargs: Additional keyword arguments to be passed to
143      the plotting function, and then the finalise_plot() function.
144
145    Returns None.
146    """
147
148    # --- checks
149    me = "plot_then_finalise"
150    report_kwargs(caller=me, **kwargs)
151    # validate once we have established the first function
152
153    # data is not checked here, assume it is checked by the called
154    # plot function.
155
156    first, kwargs["function"] = first_unchain(function)
157    if not kwargs["function"]:
158        del kwargs["function"]  # remove the function key if it is empty
159
160    # --- TO DO: check that the first function is one of the
161    bad_next = (multi_start, multi_column)
162    if first in bad_next:
163        # these functions should not be called by plot_then_finalise()
164        raise ValueError(
165            f"[{', '.join(k.__name__ for k in bad_next)}] should not be called by {me}. "
166            "Call them before calling {me}. "
167        )
168
169    if first in EXPECTED_CALLABLES:
170        expected = EXPECTED_CALLABLES[first]
171        plot_kwargs = limit_kwargs(expected, **kwargs)
172    else:
173        # this is an unexpected Callable, so we will give it a try
174        print(f"Unknown proposed function: {first}; nonetheless, will give it a try.")
175        expected = BaseKwargs
176        plot_kwargs = kwargs.copy()
177
178    # --- validate the original kwargs (could not do before now)
179    kw_types = (
180        # combine the expected kwargs types with the finalise kwargs types
181        dict(cast(dict[str, Any], expected.__annotations__))
182        | dict(cast(dict[str, Any], FinaliseKwargs.__annotations__))
183    )
184    validate_kwargs(schema=kw_types, caller=me, **kwargs)
185
186    # --- call the first function with the data and selected plot kwargs
187    axes = first(data, **plot_kwargs)
188
189    # --- remove potentially overlapping kwargs
190    fp_kwargs = limit_kwargs(FinaliseKwargs, **kwargs)
191    # overlapping = expected.keys() & FinaliseKwargs.keys()
192    # if overlapping:
193    #    for key in overlapping:
194    #        fp_kwargs.pop(key, None)  # remove overlapping keys from kwargs
195
196    # --- finalise the plot
197    finalise_plot(axes, **fp_kwargs)

Chain a plotting function with the finalise_plot() function. This is designed to be the last function in a chain.

Parameters

  • data: Series | DataFrame - The data to be plotted.
  • function: Callable | list[Callable] - The plotting function to be used.
  • **kwargs: Additional keyword arguments to be passed to the plotting function, and then the finalise_plot() function.

Returns None.

def finalise_plot( axes: matplotlib.axes._axes.Axes, **kwargs: Unpack[FinaliseKwargs]) -> None:
244def finalise_plot(axes: Axes, **kwargs: Unpack[FinaliseKwargs]) -> None:
245    """
246    A function to finalise and save plots to the file system. The filename
247    for the saved plot is constructed from the global chart_dir, the plot's title,
248    any specified tag text, and the file_type for the plot.
249
250    Arguments:
251    - axes - matplotlib axes object - required
252    - kwargs: FinaliseKwargs
253
254     Returns:
255        - None
256    """
257
258    # --- check the kwargs
259    me = "finalise_plot"
260    report_kwargs(caller=me, **kwargs)
261    validate_kwargs(schema=FinaliseKwargs, caller=me, **kwargs)
262
263    # --- sanity checks
264    if len(axes.get_children()) < 1:
265        print("Warning: finalise_plot() called with empty axes, which was ignored.")
266        return
267
268    # --- remember axis-limits should we need to restore thems
269    xlim, ylim = axes.get_xlim(), axes.get_ylim()
270
271    # margins
272    axes.margins(0.02)
273    axes.autoscale(tight=False)  # This is problematic ...
274
275    apply_kwargs(axes, **kwargs)
276
277    # tight layout and save the figure
278    fig = axes.figure
279    if "preserve_lims" in kwargs and kwargs["preserve_lims"]:
280        # restore the original limits of the axes
281        axes.set_xlim(xlim)
282        axes.set_ylim(ylim)
283    if not isinstance(fig, mpl.figure.SubFigure):  # mypy
284        fig.tight_layout(pad=1.1)
285    apply_late_kwargs(axes, **kwargs)
286    legend = axes.get_legend()
287    if legend and kwargs.get("remove_legend", False):
288        legend.remove()
289    if not isinstance(fig, mpl.figure.SubFigure):  # mypy
290        save_to_file(fig, **kwargs)
291
292    # show the plot in Jupyter Lab
293    if "show" in kwargs and kwargs["show"]:
294        plt.show()
295
296    # And close
297    closing = True if "dont_close" not in kwargs else not kwargs["dont_close"]
298    if closing:
299        plt.close()

A function to 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.

Arguments:

  • axes - matplotlib axes object - required
  • kwargs: FinaliseKwargs

Returns: - None

class FinaliseKwargs(mgplot.keyword_checking.BaseKwargs):
24class FinaliseKwargs(BaseKwargs):
25    """Keyword arguments for the finalise_plot function."""
26
27    # --- value options
28    title: NotRequired[str | None]
29    xlabel: NotRequired[str | None]
30    ylabel: NotRequired[str | None]
31    xlim: NotRequired[tuple[float, float] | None]
32    ylim: NotRequired[tuple[float, float] | None]
33    xticks: NotRequired[list[float] | None]
34    yticks: NotRequired[list[float] | None]
35    x_scale: NotRequired[str | None]
36    y_scale: NotRequired[str | None]
37    # --- splat options
38    legend: NotRequired[bool | dict[str, Any] | None]
39    axhspan: NotRequired[dict[str, Any]]
40    axvspan: NotRequired[dict[str, Any]]
41    axhline: NotRequired[dict[str, Any]]
42    axvline: NotRequired[dict[str, Any]]
43    # --- options for annotations
44    lfooter: NotRequired[str]
45    rfooter: NotRequired[str]
46    lheader: NotRequired[str]
47    rheader: NotRequired[str]
48    # --- file/save options
49    pre_tag: NotRequired[str]
50    tag: NotRequired[str]
51    chart_dir: NotRequired[str]
52    file_type: NotRequired[str]
53    dpi: NotRequired[int]
54    figsize: NotRequired[tuple[float, float]]
55    show: NotRequired[bool]
56    # --- other options
57    preserve_lims: NotRequired[bool]
58    remove_legend: NotRequired[bool]
59    zero_y: NotRequired[bool]
60    y0: NotRequired[bool]
61    x0: NotRequired[bool]
62    dont_save: NotRequired[bool]
63    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]
x_scale: NotRequired[str | None]
y_scale: 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]
def bar_plot_finalise(data: ~DataT, **kwargs: Unpack[mgplot.finalisers.BPFKwargs]) -> None:
80def bar_plot_finalise(
81    data: DataT,
82    **kwargs: Unpack[BPFKwargs],
83) -> None:
84    """
85    A convenience function to call bar_plot() and finalise_plot().
86    """
87    validate_kwargs(schema=BPFKwargs, caller="bar_plot_finalise", **kwargs)
88    impose_legend(data=data, kwargs=kwargs)
89    plot_then_finalise(
90        data,
91        function=bar_plot,
92        **kwargs,
93    )

A convenience function to call bar_plot() and finalise_plot().

def line_plot_finalise(data: ~DataT, **kwargs: Unpack[mgplot.finalisers.LPFKwargs]) -> None:
64def line_plot_finalise(
65    data: DataT,
66    **kwargs: Unpack[LPFKwargs],
67) -> None:
68    """
69    A convenience function to call line_plot() then finalise_plot().
70    """
71    validate_kwargs(schema=LPFKwargs, caller="line_plot_finalise", **kwargs)
72    impose_legend(data=data, kwargs=kwargs)
73    plot_then_finalise(data, function=line_plot, **kwargs)

A convenience function to call line_plot() then finalise_plot().

def postcovid_plot_finalise(data: ~DataT, **kwargs: Unpack[mgplot.finalisers.PCFKwargs]) -> None:
116def postcovid_plot_finalise(
117    data: DataT,
118    **kwargs: Unpack[PCFKwargs],
119) -> None:
120    """
121    A convenience function to call postcovid_plot() and finalise_plot().
122    """
123    validate_kwargs(schema=PCFKwargs, caller="postcovid_plot_finalise", **kwargs)
124    impose_legend(force=True, kwargs=kwargs)
125    plot_then_finalise(data, function=postcovid_plot, **kwargs)

A convenience function to call postcovid_plot() and finalise_plot().

def growth_plot_finalise(data: ~DataT, **kwargs: Unpack[mgplot.finalisers.GrowthPFKwargs]) -> None:
177def growth_plot_finalise(data: DataT, **kwargs: Unpack[GrowthPFKwargs]) -> None:
178    """
179    A convenience function to call series_growth_plot() and finalise_plot().
180    Use this when you are providing the raw growth data. Don't forget to
181    set the ylabel in kwargs.
182    """
183    validate_kwargs(schema=GrowthPFKwargs, caller="growth_plot_finalise", **kwargs)
184    impose_legend(force=True, kwargs=kwargs)
185    plot_then_finalise(data=data, function=growth_plot, **kwargs)

A convenience function to call series_growth_plot() and finalise_plot(). Use this when you are providing the raw growth data. Don't forget to set the ylabel in kwargs.

def revision_plot_finalise(data: ~DataT, **kwargs: Unpack[mgplot.finalisers.RevPFKwargs]) -> None:
132def revision_plot_finalise(
133    data: DataT,
134    **kwargs: Unpack[RevPFKwargs],
135) -> None:
136    """
137    A convenience function to call revision_plot() and finalise_plot().
138    """
139    validate_kwargs(schema=RevPFKwargs, caller="revision_plot_finalise", **kwargs)
140    impose_legend(force=True, kwargs=kwargs)
141    plot_then_finalise(data=data, function=revision_plot, **kwargs)

A convenience function to call revision_plot() and finalise_plot().

def run_plot_finalise(data: ~DataT, **kwargs: Unpack[mgplot.finalisers.RunPFKwargs]) -> None:
148def run_plot_finalise(
149    data: DataT,
150    **kwargs: Unpack[RunPFKwargs],
151) -> None:
152    """
153    A convenience function to call run_plot() and finalise_plot().
154    """
155    validate_kwargs(schema=RunPFKwargs, caller="run_plot_finalise", **kwargs)
156    impose_legend(force=True, kwargs=kwargs)
157    plot_then_finalise(data=data, function=run_plot, **kwargs)

A convenience function to call run_plot() and finalise_plot().

def seastrend_plot_finalise(data: ~DataT, **kwargs: Unpack[mgplot.finalisers.SFKwargs]) -> None:
100def seastrend_plot_finalise(
101    data: DataT,
102    **kwargs: Unpack[SFKwargs],
103) -> None:
104    """
105    A convenience function to call seas_trend_plot() and finalise_plot().
106    """
107    validate_kwargs(schema=SFKwargs, caller="seastrend_plot_finalise", **kwargs)
108    impose_legend(force=True, kwargs=kwargs)
109    plot_then_finalise(data, function=seastrend_plot, **kwargs)

A convenience function to call seas_trend_plot() and finalise_plot().

def series_growth_plot_finalise(data: ~DataT, **kwargs: Unpack[mgplot.finalisers.SGFPKwargs]) -> None:
164def series_growth_plot_finalise(data: DataT, **kwargs: Unpack[SGFPKwargs]) -> None:
165    """
166    A convenience function to call series_growth_plot() and finalise_plot().
167    """
168    validate_kwargs(schema=SGFPKwargs, caller="series_growth_plot_finalise", **kwargs)
169    impose_legend(force=True, kwargs=kwargs)
170    plot_then_finalise(data=data, function=series_growth_plot, **kwargs)

A convenience function to call series_growth_plot() and finalise_plot().

def summary_plot_finalise(data: ~DataT, **kwargs: Unpack[mgplot.finalisers.SumPFKwargs]) -> None:
192def summary_plot_finalise(
193    data: DataT,
194    **kwargs: Unpack[SumPFKwargs],
195) -> None:
196    """
197    A convenience function to call summary_plot() and finalise_plot().
198    This is more complex than most of the above convienience methods.
199
200    Arguments
201    - data: DataFrame containing the summary data. The index must be a PeriodIndex.
202    - kwargs: additional arguments for the plot
203    """
204
205    # --- standard arguments
206    if not isinstance(data, DataFrame) and isinstance(data.index, PeriodIndex):
207        raise TypeError("Data must be a DataFrame with a PeriodIndex.")
208    validate_kwargs(schema=SumPFKwargs, caller="summary_plot_finalise", **kwargs)
209    kwargs["title"] = kwargs.get("title", f"Summary at {data.index[-1].strftime('%b-%Y')}")
210    kwargs["preserve_lims"] = kwargs.get("preserve_lims", True)
211
212    start: int | Period | None = kwargs.get("plot_from", 0)
213    if start is None:
214        start = data.index[0]
215    if isinstance(start, int):
216        start = data.index[start]
217    kwargs["plot_from"] = start
218    if not isinstance(start, Period):
219        raise TypeError("plot_from must be a Period or convertible to one")
220
221    pre_tag: str = kwargs.get("pre_tag", "")
222    for plot_type in ("zscores", "zscaled"):
223        # some sorting of kwargs for plot production
224        kwargs["plot_type"] = plot_type
225        kwargs["pre_tag"] = pre_tag + plot_type
226
227        if plot_type == "zscores":
228            kwargs["xlabel"] = f"Z-scores for prints since {start.strftime('%b-%Y')}"
229            kwargs["x0"] = True
230        else:
231            kwargs["xlabel"] = f"-1 to 1 scaled z-scores since {start.strftime('%b-%Y')}"
232            kwargs.pop("x0", None)
233
234        plot_then_finalise(
235            data,
236            function=summary_plot,
237            **kwargs,
238        )

A convenience function to call summary_plot() and finalise_plot(). This is more complex than most of the above convienience methods.

Arguments

  • data: DataFrame containing the summary data. The index must be a PeriodIndex.
  • kwargs: additional arguments for the plot