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, test nor type-checking modules here. 14from mgplot.finalise_plot import finalise_plot, FINALISE_KW_TYPES 15from mgplot.bar_plot import bar_plot, BAR_KW_TYPES 16from mgplot.line_plot import line_plot, LINE_KW_TYPES 17from mgplot.seastrend_plot import seastrend_plot, SEASTREND_KW_TYPES 18from mgplot.postcovid_plot import postcovid_plot, POSTCOVID_KW_TYPES 19from mgplot.revision_plot import revision_plot, REVISION_KW_TYPES 20from mgplot.run_plot import run_plot, RUN_KW_TYPES 21from mgplot.summary_plot import summary_plot, SUMMARY_KW_TYPES 22from mgplot.growth_plot import ( 23 calc_growth, 24 growth_plot, 25 series_growth_plot, 26 SERIES_GROWTH_KW_TYPES, 27 GROWTH_KW_TYPES, 28) 29from mgplot.multi_plot import ( 30 multi_start, 31 multi_column, 32 plot_then_finalise, 33) 34from mgplot.colors import ( 35 get_color, 36 get_party_palette, 37 colorise_list, 38 contrast, 39 abbreviate_state, 40 state_names, 41 state_abbrs, 42) 43from mgplot.settings import ( 44 get_setting, 45 set_setting, 46 set_chart_dir, 47 clear_chart_dir, 48) 49from mgplot.finalisers import ( 50 line_plot_finalise, 51 bar_plot_finalise, 52 seastrend_plot_finalise, 53 postcovid_plot_finalise, 54 revision_plot_finalise, 55 summary_plot_finalise, 56 growth_plot_finalise, 57 series_growth_plot_finalise, 58 run_plot_finalise, 59) 60 61 62# --- version and author 63try: 64 __version__ = importlib.metadata.version(__name__) 65except importlib.metadata.PackageNotFoundError: 66 __version__ = "0.0.0" # Fallback for development mode 67__author__ = "Bryan Palmer" 68 69 70# --- public API 71__all__ = ( 72 "__version__", 73 "__author__", 74 # --- settings 75 "get_setting", 76 "set_setting", 77 "set_chart_dir", 78 "clear_chart_dir", 79 # --- colors 80 "get_color", 81 "get_party_palette", 82 "colorise_list", 83 "contrast", 84 "abbreviate_state", 85 "state_names", 86 "state_abbrs", 87 # --- finalise_plot 88 "finalise_plot", 89 # --- line_plot 90 "line_plot", 91 # --- bar plot 92 "bar_plot", 93 # --- seastrend_plot 94 "seastrend_plot", 95 # --- postcovid_plot 96 "postcovid_plot", 97 # --- revision_plot 98 "revision_plot", 99 # --- run_plot 100 "run_plot", 101 # --- summary_plot 102 "summary_plot", 103 # --- growth_plot 104 "calc_growth", 105 "growth_plot", 106 "series_growth_plot", 107 # --- multi_plot 108 "multi_start", 109 "multi_column", 110 "plot_then_finalise", 111 # --- finaliser functions 112 "line_plot_finalise", 113 "bar_plot_finalise", 114 "seastrend_plot_finalise", 115 "postcovid_plot_finalise", 116 "revision_plot_finalise", 117 "summary_plot_finalise", 118 "growth_plot_finalise", 119 "series_growth_plot_finalise", 120 "run_plot_finalise", 121 # --- typing information 122 "FINALISE_KW_TYPES", 123 "BAR_KW_TYPES", 124 "LINE_KW_TYPES", 125 "SEASTREND_KW_TYPES", 126 "POSTCOVID_KW_TYPES", 127 "REVISION_KW_TYPES", 128 "RUN_KW_TYPES", 129 "SUMMARY_KW_TYPES", 130 "SERIES_GROWTH_KW_TYPES", 131 "GROWTH_KW_TYPES", 132 # --- The rest are internal use only 133) 134# __pdoc__: dict[str, Any] = {"test": False} # hide submodules from documentation
86def get_setting(setting: str) -> Any: 87 """ 88 Get a setting from the global settings. 89 90 Arguments: 91 - setting: str - name of the setting to get. The possible settings are: 92 - file_type: str - the file type to use for saving plots 93 - figsize: tuple[float, float] - the figure size to use for plots 94 - file_dpi: int - the DPI to use for saving plots 95 - line_narrow: float - the line width for narrow lines 96 - line_normal: float - the line width for normal lines 97 - line_wide: float - the line width for wide lines 98 - bar_width: float - the width of bars in bar plots 99 - legend_font_size: float | str - the font size for legends 100 - legend: dict[str, Any] - the legend settings 101 - colors: dict[int, list[str]] - a dictionary of colors for 102 different numbers of lines 103 - chart_dir: str - the directory to save charts in 104 105 Raises: 106 - KeyError: if the setting is not found 107 108 Returns: 109 - value: Any - the value of the setting 110 """ 111 if setting not in _mgplot_defaults: 112 raise KeyError(f"Setting '{setting}' not found in _mgplot_defaults.") 113 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
116def set_setting(setting: str, value: Any) -> None: 117 """ 118 Set a setting in the global settings. 119 Raises KeyError if the setting is not found. 120 121 Arguments: 122 - setting: str - name of the setting to set (see get_setting()) 123 - value: Any - the value to set the setting to 124 """ 125 126 if setting not in _mgplot_defaults: 127 raise KeyError(f"Setting '{setting}' not found in _mgplot_defaults.") 128 _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
147def set_chart_dir(chart_dir: str) -> None: 148 """ 149 A function to set a global chart directory for finalise_plot(), 150 so that it does not need to be included as an argument in each 151 call to finalise_plot(). Create the directory if it does not exist. 152 153 Note: Path.mkdir() may raise an exception if a directory cannot be created. 154 155 Note: This is a wrapper for set_setting() to set the chart_dir setting, and 156 create the directory if it does not exist. 157 158 Arguments: 159 - chart_dir: str - the directory to set as the chart directory 160 """ 161 162 if not chart_dir: 163 chart_dir = "." # avoid the empty string 164 Path(chart_dir).mkdir(parents=True, exist_ok=True) 165 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
131def clear_chart_dir() -> None: 132 """ 133 Remove all graph-image files from the global chart_dir. 134 This is a convenience function to remove all files from the 135 chart_dir directory. It does not remove the directory itself. 136 Note: the function creates the directory if it does not exist. 137 """ 138 139 chart_dir = get_setting("chart_dir") 140 Path(chart_dir).mkdir(parents=True, exist_ok=True) 141 for ext in ("png", "svg", "jpg", "jpeg"): 142 for fs_object in Path(chart_dir).glob(f"*.{ext}"): 143 if fs_object.is_file(): 144 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.
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.
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.
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.
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.
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.
314def finalise_plot(axes: Axes, **kwargs) -> None: 315 """ 316 A function to finalise and save plots to the file system. The filename 317 for the saved plot is constructed from the global chart_dir, the plot's title, 318 any specified tag text, and the file_type for the plot. 319 320 Arguments: 321 - axes - matplotlib axes object - required 322 - kwargs 323 - title: str - plot title, also used to create the save file name 324 - xlabel: str | None - text label for the x-axis 325 - ylabel: str | None - label for the y-axis 326 - pre_tag: str - text before the title in file name 327 - tag: str - text after the title in the file name 328 (useful for ensuring that same titled charts do not over-write) 329 - chart_dir: str - location of the chart directory 330 - file_type: str - specify a file type - eg. 'png' or 'svg' 331 - lfooter: str - text to display on bottom left of plot 332 - rfooter: str - text to display of bottom right of plot 333 - lheader: str - text to display on top left of plot 334 - rheader: str - text to display of top right of plot 335 - figsize: tuple[float, float] - figure size in inches - eg. (8, 4) 336 - show: bool - whether to show the plot or not 337 - zero_y: bool - ensure y=0 is included in the plot. 338 - y0: bool - highlight the y=0 line on the plot (if in scope) 339 - x0: bool - highlights the x=0 line on the plot 340 - dont_save: bool - dont save the plot to the file system 341 - dont_close: bool - dont close the plot 342 - dpi: int - dots per inch for the saved chart 343 - legend: bool | dict - if dict, use as the arguments to pass to axes.legend(), 344 if True pass the global default arguments to axes.legend() 345 - axhspan: dict - arguments to pass to axes.axhspan() 346 - axvspan: dict - arguments to pass to axes.axvspan() 347 - axhline: dict - arguments to pass to axes.axhline() 348 - axvline: dict - arguments to pass to axes.axvline() 349 - ylim: tuple[float, float] - set lower and upper y-axis limits 350 - xlim: tuple[float, float] - set lower and upper x-axis limits 351 - preserve_lims: bool - if True, preserve the original axes limits, 352 lims saved at the start, and restored after the tight layout 353 - remove_legend: bool | None - if True, remove the legend from the plot 354 - report_kwargs: bool - if True, report the kwargs used in this function 355 356 Returns: 357 - None 358 """ 359 360 # --- check the kwargs 361 me = "finalise_plot" 362 report_kwargs(called_from=me, **kwargs) 363 kwargs = validate_kwargs(FINALISE_KW_TYPES, me, **kwargs) 364 365 # --- sanity checks 366 if len(axes.get_children()) < 1: 367 print("Warning: finalise_plot() called with empty axes, which was ignored.") 368 return 369 370 # --- remember axis-limits should we need to restore thems 371 xlim, ylim = axes.get_xlim(), axes.get_ylim() 372 373 # margins 374 axes.margins(0.02) 375 axes.autoscale(tight=False) # This is problematic ... 376 377 _apply_kwargs(axes, **kwargs) 378 379 # tight layout and save the figure 380 fig = axes.figure 381 if not isinstance(fig, mpl.figure.SubFigure): # should never be a SubFigure 382 fig.tight_layout(pad=1.1) 383 if PRESERVE_LIMS in kwargs and kwargs[PRESERVE_LIMS]: 384 # restore the original limits of the axes 385 axes.set_xlim(xlim) 386 axes.set_ylim(ylim) 387 _apply_late_kwargs(axes, **kwargs) 388 legend = axes.get_legend() 389 if legend and kwargs.get(REMOVE_LEGEND, False): 390 legend.remove() 391 _save_to_file(fig, **kwargs) 392 393 # show the plot in Jupyter Lab 394 if SHOW in kwargs and kwargs[SHOW]: 395 plt.show() 396 397 # And close 398 closing = True if DONT_CLOSE not in kwargs else not kwargs[DONT_CLOSE] 399 if closing: 400 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
- title: str - plot title, also used to create the save file name
- xlabel: str | None - text label for the x-axis
- ylabel: str | None - label for the y-axis
- pre_tag: str - text before the title in file name
- tag: str - text after the title in the file name (useful for ensuring that same titled charts do not over-write)
- chart_dir: str - location of the chart directory
- file_type: str - specify a file type - eg. 'png' or 'svg'
- lfooter: str - text to display on bottom left of plot
- rfooter: str - text to display of bottom right of plot
- lheader: str - text to display on top left of plot
- rheader: str - text to display of top right of plot
- figsize: tuple[float, float] - figure size in inches - eg. (8, 4)
- show: bool - whether to show the plot or not
- zero_y: bool - ensure y=0 is included in the plot.
- y0: bool - highlight the y=0 line on the plot (if in scope)
- x0: bool - highlights the x=0 line on the plot
- dont_save: bool - dont save the plot to the file system
- dont_close: bool - dont close the plot
- dpi: int - dots per inch for the saved chart
- legend: bool | dict - if dict, use as the arguments to pass to axes.legend(), if True pass the global default arguments to axes.legend()
- axhspan: dict - arguments to pass to axes.axhspan()
- axvspan: dict - arguments to pass to axes.axvspan()
- axhline: dict - arguments to pass to axes.axhline()
- axvline: dict - arguments to pass to axes.axvline()
- ylim: tuple[float, float] - set lower and upper y-axis limits
- xlim: tuple[float, float] - set lower and upper x-axis limits
- preserve_lims: bool - if True, preserve the original axes limits, lims saved at the start, and restored after the tight layout
- remove_legend: bool | None - if True, remove the legend from the plot
- report_kwargs: bool - if True, report the kwargs used in this function
Returns: - None
147def line_plot(data: DataT, **kwargs) -> Axes: 148 """ 149 Build a single plot from the data passed in. 150 This can be a single- or multiple-line plot. 151 Return the axes object for the build. 152 153 Agruments: 154 - data: DataFrame | Series - data to plot 155 - kwargs: 156 /* chart wide arguments */ 157 - ax: Axes | None - axes to plot on (optional) 158 /* individual line arguments */ 159 - dropna: bool | list[bool] - whether to delete NAs frm the 160 data before plotting [optional] 161 - color: str | list[str] - line colors. 162 - width: float | list[float] - line widths [optional]. 163 - style: str | list[str] - line styles [optional]. 164 - alpha: float | list[float] - line transparencies [optional]. 165 - marker: str | list[str] - line markers [optional]. 166 - marker_size: float | list[float] - line marker sizes [optional]. 167 /* end of line annotation arguments */ 168 - annotate: bool | list[bool] - whether to annotate a series. 169 - rounding: int | bool | list[int | bool] - number of decimal places 170 to round an annotation. If True, a default between 0 and 2 is 171 used. 172 - fontsize: int | str | list[int | str] - font size for the 173 annotation. 174 - fontname: str - font name for the annotation. 175 - rotation: int | float | list[int | float] - rotation of the 176 annotation text. 177 - drawstyle: str | list[str] - matplotlib line draw styles. 178 - annotate_color: str | list[str] | bool | list[bool] - color 179 for the annotation text. If True, the same color as the line. 180 181 Returns: 182 - axes: Axes - the axes object for the plot 183 """ 184 185 # --- check the kwargs 186 me = "line_plot" 187 report_kwargs(called_from=me, **kwargs) 188 kwargs = validate_kwargs(LINE_KW_TYPES, me, **kwargs) 189 190 # --- check the data 191 data = check_clean_timeseries(data, me) 192 df = DataFrame(data) # we are only plotting DataFrames 193 df, kwargs = constrain_data(df, **kwargs) 194 195 # --- some special defaults 196 kwargs[LABEL_SERIES] = ( 197 kwargs.get(LABEL_SERIES, True) 198 if len(df.columns) > 1 199 else kwargs.get(LABEL_SERIES, False) 200 ) 201 202 # --- Let's plot 203 axes, kwargs = get_axes(**kwargs) # get the axes to plot on 204 if df.empty or df.isna().all().all(): 205 # Note: finalise plot should ignore an empty axes object 206 print(f"Warning: No data to plot in {me}().") 207 return axes 208 209 # --- get the arguments for each line we will plot ... 210 item_count = len(df.columns) 211 num_data_points = len(df) 212 swce, kwargs = _get_style_width_color_etc(item_count, num_data_points, **kwargs) 213 214 for i, column in enumerate(df.columns): 215 series = df[column] 216 series = series.dropna() if DROPNA in swce and swce[DROPNA][i] else series 217 if series.empty or series.isna().all(): 218 print(f"Warning: No data to plot for {column} in line_plot().") 219 continue 220 221 series.plot( 222 # Note: pandas will plot PeriodIndex against their ordinal values 223 ls=swce[STYLE][i], 224 lw=swce[WIDTH][i], 225 color=swce[COLOR][i], 226 alpha=swce[ALPHA][i], 227 marker=swce[MARKER][i], 228 ms=swce[MARKERSIZE][i], 229 drawstyle=swce[DRAWSTYLE][i], 230 label=( 231 column 232 if LABEL_SERIES in swce and swce[LABEL_SERIES][i] 233 else f"_{column}_" 234 ), 235 ax=axes, 236 ) 237 238 if swce[ANNOTATE][i] is None or not swce[ANNOTATE][i]: 239 continue 240 241 color = ( 242 swce[COLOR][i] 243 if swce[ANNOTATE_COLOR][i] is True 244 else swce[ANNOTATE_COLOR][i] 245 ) 246 annotate_series( 247 series, 248 axes, 249 color=color, 250 rounding=swce[ROUNDING][i], 251 fontsize=swce[FONTSIZE][i], 252 fontname=swce[FONTNAME][i], 253 rotation=swce[ROTATION][i], 254 ) 255 256 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.
Agruments:
- data: DataFrame | Series - data to plot
- kwargs:
/* chart wide arguments /
- ax: Axes | None - axes to plot on (optional) /
- dropna: bool | list[bool] - whether to delete NAs frm the data before plotting [optional]
- color: str | list[str] - line colors.
- width: float | list[float] - line widths [optional].
- style: str | list[str] - line styles [optional].
- alpha: float | list[float] - line transparencies [optional].
- marker: str | list[str] - line markers [optional].
- marker_size: float | list[float] - line marker sizes [optional]. / end of line annotation arguments */
- annotate: bool | list[bool] - whether to annotate a series.
- rounding: int | bool | list[int | bool] - number of decimal places to round an annotation. If True, a default between 0 and 2 is used.
- fontsize: int | str | list[int | str] - font size for the annotation.
- fontname: str - font name for the annotation.
- rotation: int | float | list[int | float] - rotation of the annotation text.
- drawstyle: str | list[str] - matplotlib line draw styles.
- annotate_color: str | list[str] | bool | list[bool] - color for the annotation text. If True, the same color as the line.
Returns:
- axes: Axes - the axes object for the plot
213def bar_plot( 214 data: DataT, 215 **kwargs, 216) -> Axes: 217 """ 218 Create a bar plot from the given data. Each column in the DataFrame 219 will be stacked on top of each other, with positive values above 220 zero and negative values below zero. 221 222 Parameters 223 - data: Series - The data to plot. Can be a DataFrame or a Series. 224 - **kwargs: dict Additional keyword arguments for customization. 225 # --- options for the entire bar plot 226 ax: Axes - axes to plot on, or None for new axes 227 stacked: bool - if True, the bars will be stacked. If False, they will be grouped. 228 max_ticks: int - maximum number of ticks on the x-axis (for PeriodIndex only) 229 plot_from: int | PeriodIndex - if provided, the plot will start from this index. 230 # --- options for each bar ... 231 color: str | list[str] - the color of the bars (or separate colors for each series 232 label_series: bool | list[bool] - if True, the series will be labeled in the legend 233 width: float | list[float] - the width of the bars 234 # - options for bar annotations 235 annotate: bool - If True them annotate the bars with their values. 236 fontsize: int | float | str - font size of the annotations 237 fontname: str - font name of the annotations 238 rounding: int - number of decimal places to round to 239 annotate_color: str - color of annotations 240 rotation: int | float - rotation of annotations in degrees 241 above: bool - if True, annotations are above the bar, else within the bar 242 243 Note: This function does not assume all data is timeseries with a PeriodIndex, 244 245 Returns 246 - axes: Axes - The axes for the plot. 247 """ 248 249 # --- check the kwargs 250 me = "bar_plot" 251 report_kwargs(called_from=me, **kwargs) 252 kwargs = validate_kwargs(BAR_KW_TYPES, me, **kwargs) 253 254 # --- get the data 255 # no call to check_clean_timeseries here, as bar plots are not 256 # necessarily timeseries data. If the data is a Series, it will be 257 # converted to a DataFrame with a single column. 258 df = DataFrame(data) # really we are only plotting DataFrames 259 df, kwargs = constrain_data(df, **kwargs) 260 item_count = len(df.columns) 261 262 # --- deal with complete PeriodIdex indicies 263 if not is_categorical(df): 264 print( 265 "Warning: bar_plot is not designed for incomplete or non-categorical data indexes." 266 ) 267 saved_pi = map_periodindex(df) 268 if saved_pi is not None: 269 df = saved_pi[0] # extract the reindexed DataFrame from the PeriodIndex 270 271 # --- set up the default arguments 272 chart_defaults: dict[str, Any] = { 273 STACKED: False, 274 MAX_TICKS: 10, 275 LABEL_SERIES: item_count > 1, 276 } 277 chart_args = {k: kwargs.get(k, v) for k, v in chart_defaults.items()} 278 279 bar_defaults: dict[str, Any] = { 280 COLOR: get_color_list(item_count), 281 WIDTH: get_setting("bar_width"), 282 LABEL_SERIES: (item_count > 1), 283 } 284 above = kwargs.get(ABOVE, False) 285 anno_args = { 286 ANNOTATE: kwargs.get(ANNOTATE, False), 287 FONTSIZE: kwargs.get(FONTSIZE, "small"), 288 FONTNAME: kwargs.get(FONTNAME, "Helvetica"), 289 ROTATION: kwargs.get(ROTATION, 0), 290 ROUNDING: kwargs.get(ROUNDING, True), 291 COLOR: kwargs.get(ANNOTATE_COLOR, "black" if above else "white"), 292 ABOVE: above, 293 } 294 bar_args, remaining_kwargs = apply_defaults(item_count, bar_defaults, kwargs) 295 296 # --- plot the data 297 axes, _rkwargs = get_axes(**remaining_kwargs) 298 if chart_args[STACKED]: 299 stacked(axes, df, anno_args, **bar_args) 300 else: 301 grouped(axes, df, anno_args, **bar_args) 302 303 # --- handle complete periodIndex data and label rotation 304 if saved_pi is not None: 305 set_labels(axes, saved_pi[1], chart_args["max_ticks"]) 306 else: 307 plt.xticks(rotation=90) 308 309 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: dict Additional keyword arguments for customization.
--- options for the entire bar plot
ax: Axes - axes to plot on, or None for new axes stacked: bool - if True, the bars will be stacked. If False, they will be grouped. max_ticks: int - maximum number of ticks on the x-axis (for PeriodIndex only) plot_from: int | PeriodIndex - if provided, the plot will start from this index.
--- options for each bar ...
color: str | list[str] - the color of the bars (or separate colors for each series label_series: bool | list[bool] - if True, the series will be labeled in the legend width: float | list[float] - the width of the bars
- options for bar annotations
annotate: bool - If True them annotate the bars with their values. fontsize: int | float | str - font size of the annotations fontname: str - font name of the annotations rounding: int - number of decimal places to round to annotate_color: str - color of annotations rotation: int | float - rotation of annotations in degrees above: bool - if True, annotations are above the bar, else within the bar
Note: This function does not assume all data is timeseries with a PeriodIndex,
Returns
- axes: Axes - The axes for the plot.
28def seastrend_plot(data: DataT, **kwargs) -> Axes: 29 """ 30 Publish a DataFrame, where the first column is seasonally 31 adjusted data, and the second column is trend data. 32 33 Aguments: 34 - data: DataFrame - the data to plot with the first column 35 being the seasonally adjusted data, and the second column 36 being the trend data. 37 The remaining arguments are the same as those passed to 38 line_plot(). 39 40 Returns: 41 - a matplotlib Axes object 42 """ 43 44 # Note: we will rely on the line_plot() function to do most of the work. 45 # including constraining the data to the plot_from keyword argument. 46 47 # --- check the kwargs 48 me = "seastrend_plot" 49 report_kwargs(called_from=me, **kwargs) 50 kwargs = validate_kwargs(SEASTREND_KW_TYPES, me, **kwargs) 51 52 # --- check the data 53 data = check_clean_timeseries(data, me) 54 if len(data.columns) < 2: 55 raise ValueError( 56 "seas_trend_plot() expects a DataFrame data item with at least 2 columns." 57 ) 58 59 # --- defaults if not in kwargs 60 colors = kwargs.pop(COLOR, get_color_list(2)) 61 widths = kwargs.pop(WIDTH, [get_setting("line_normal"), get_setting("line_wide")]) 62 styles = kwargs.pop(STYLE, ["-", "-"]) 63 annotations = kwargs.pop(ANNOTATE, [True, False]) 64 rounding = kwargs.pop(ROUNDING, True) 65 66 # series breaks are common in seas-trend data 67 kwargs[DROPNA] = kwargs.pop(DROPNA, False) 68 69 axes = line_plot( 70 data, 71 color=colors, 72 width=widths, 73 style=styles, 74 annotate=annotations, 75 rounding=rounding, 76 **kwargs, 77 ) 78 79 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
65def postcovid_plot(data: DataT, **kwargs) -> Axes: 66 """ 67 Plots a series with a PeriodIndex. 68 69 Arguments 70 - data - the series to be plotted (note that this function 71 is designed to work with a single series, not a DataFrame). 72 - **kwargs - same as for line_plot() and finalise_plot(). 73 74 Raises: 75 - TypeError if series is not a pandas Series 76 - TypeError if series does not have a PeriodIndex 77 - ValueError if series does not have a D, M or Q frequency 78 - ValueError if regression start is after regression end 79 """ 80 81 # --- check the kwargs 82 me = "postcovid_plot" 83 report_kwargs(called_from=me, **kwargs) 84 kwargs = validate_kwargs(POSTCOVID_KW_TYPES, me, **kwargs) 85 86 # --- check the data 87 data = check_clean_timeseries(data, me) 88 if not isinstance(data, Series): 89 raise TypeError("The series argument must be a pandas Series") 90 series: Series = data 91 series_index = PeriodIndex(series.index) # syntactic sugar for type hinting 92 if series_index.freqstr[:1] not in ("Q", "M", "D"): 93 raise ValueError("The series index must have a D, M or Q freq") 94 # rely on line_plot() to validate kwargs 95 if PLOT_FROM in kwargs: 96 print("Warning: the 'plot_from' argument is ignored in postcovid_plot().") 97 del kwargs[PLOT_FROM] 98 99 # --- plot COVID counterfactural 100 freq = PeriodIndex(series.index).freqstr # syntactic sugar for type hinting 101 match freq[0]: 102 case "Q": 103 start_regression = Period("2014Q4", freq=freq) 104 end_regression = Period("2019Q4", freq=freq) 105 case "M": 106 start_regression = Period("2015-01", freq=freq) 107 end_regression = Period("2020-01", freq=freq) 108 case "D": 109 start_regression = Period("2015-01-01", freq=freq) 110 end_regression = Period("2020-01-01", freq=freq) 111 112 start_regression = Period(kwargs.pop(START_R, start_regression), freq=freq) 113 end_regression = Period(kwargs.pop(END_R, end_regression), freq=freq) 114 if start_regression >= end_regression: 115 raise ValueError("Start period must be before end period") 116 117 # --- combine data and projection 118 recent = series[series.index >= start_regression].copy() 119 recent.name = "Series" 120 projection = get_projection(recent, end_regression) 121 projection.name = "Pre-COVID projection" 122 data_set = DataFrame([projection, recent]).T 123 124 # --- activate plot settings 125 kwargs[WIDTH] = kwargs.pop( 126 WIDTH, (get_setting("line_normal"), get_setting("line_wide")) 127 ) # series line is thicker than projection 128 kwargs[STYLE] = kwargs.pop(STYLE, ("--", "-")) # dashed regression line 129 kwargs[LABEL_SERIES] = kwargs.pop(LABEL_SERIES, True) 130 kwargs[ANNOTATE] = kwargs.pop(ANNOTATE, (False, True)) # annotate series only 131 kwargs[COLOR] = kwargs.pop(COLOR, ("darkblue", "#dd0000")) 132 133 return line_plot( 134 data_set, 135 **kwargs, 136 )
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
34def revision_plot(data: DataT, **kwargs) -> Axes: 35 """ 36 Plot the revisions to ABS data. 37 38 Arguments 39 data: pd.DataFrame - the data to plot, the DataFrame has a 40 column for each data revision 41 kwargs - additional keyword arguments for the line_plot function. 42 """ 43 44 # --- check the kwargs and data 45 me = "revision_plot" 46 report_kwargs(called_from=me, **kwargs) 47 kwargs = validate_kwargs(REVISION_KW_TYPES, me, **kwargs) 48 49 data = check_clean_timeseries(data, me) 50 51 # --- additional checks 52 if not isinstance(data, DataFrame): 53 print( 54 f"{me} requires a DataFrame with columns for each revision, " 55 "not a Series or other type." 56 ) 57 58 # --- critical defaults 59 kwargs[PLOT_FROM] = kwargs.get(PLOT_FROM, -15) 60 kwargs[ANNOTATE] = kwargs.get(ANNOTATE, True) 61 kwargs[ANNOTATE_COLOR] = kwargs.get(ANNOTATE_COLOR, "black") 62 kwargs[ROUNDING] = kwargs.get(ROUNDING, 3) 63 64 # --- plot 65 axes = line_plot(data, **kwargs) 66 67 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.
117def run_plot(data: DataT, **kwargs) -> Axes: 118 """Plot a series of percentage rates, highlighting the increasing runs. 119 120 Arguments 121 - data - ordered pandas Series of percentages, with PeriodIndex 122 - **kwargs 123 - threshold - float - used to ignore micro noise near zero 124 (for example, threshhold=0.01) 125 - round - int - rounding for highlight text 126 - highlight - str or Sequence[str] - color(s) for highlighting the 127 runs, two colors can be specified in a list if direction is "both" 128 - direction - str - whether the highlight is for an upward 129 or downward or both runs. Options are "up", "down" or "both". 130 - in addition the **kwargs for line_plot are accepted. 131 132 Return 133 - matplotlib Axes object""" 134 135 # --- check the kwargs 136 me = "run_plot" 137 report_kwargs(called_from=me, **kwargs) 138 kwargs = validate_kwargs(RUN_KW_TYPES, me, **kwargs) 139 140 # --- check the data 141 series = check_clean_timeseries(data, me) 142 if not isinstance(series, Series): 143 raise TypeError("series must be a pandas Series for run_plot()") 144 series, kwargs = constrain_data(series, **kwargs) 145 146 # --- default arguments - in **kwargs 147 kwargs[THRESHOLD] = kwargs.get(THRESHOLD, 0.1) 148 kwargs[DIRECTION] = kwargs.get(DIRECTION, "both") 149 kwargs[ROUNDING] = kwargs.get(ROUNDING, 2) 150 kwargs[HIGHLIGHT] = kwargs.get( 151 HIGHLIGHT, ("gold", "skyblue") if kwargs[DIRECTION] == "both" else "gold" 152 ) 153 kwargs[COLOR] = kwargs.get(COLOR, "darkblue") 154 155 # --- plot the line 156 kwargs[DRAWSTYLE] = kwargs.get(DRAWSTYLE, "steps-post") 157 lp_kwargs = limit_kwargs(LINE_KW_TYPES, **kwargs) 158 axes = line_plot(series, **lp_kwargs) 159 160 # plot the runs 161 match kwargs[DIRECTION]: 162 case "up": 163 _plot_runs(axes, series, up=True, **kwargs) 164 case "down": 165 _plot_runs(axes, series, up=False, **kwargs) 166 case "both": 167 _plot_runs(axes, series, up=True, **kwargs) 168 _plot_runs(axes, series, up=False, **kwargs) 169 case _: 170 raise ValueError( 171 f"Invalid value for direction: {kwargs[DIRECTION]}. " 172 "Expected 'up', 'down', or 'both'." 173 ) 174 return axes
Plot a series of percentage rates, highlighting the increasing runs.
Arguments
- data - ordered pandas Series of percentages, with PeriodIndex
- *kwargs
- threshold - float - used to ignore micro noise near zero (for example, threshhold=0.01)
- round - int - rounding for highlight text
- highlight - str or Sequence[str] - color(s) for highlighting the runs, two colors can be specified in a list if direction is "both"
- direction - str - whether the highlight is for an upward or downward or both runs. Options are "up", "down" or "both".
- in addition the *
Return
- matplotlib Axes object
201def summary_plot( 202 data: DataT, # summary data 203 **kwargs, 204) -> Axes: 205 """Plot a summary of historical data for a given DataFrame. 206 207 Args: 208 - summary: DataFrame containing the summary data. The column names are 209 used as labels for the plot. 210 - kwargs: additional arguments for the plot, including: 211 - plot_from: int | Period | None 212 - verbose: if True, print the summary data. 213 - middle: proportion of data to highlight (default is 0.8). 214 - plot_types: list of plot types to generate. 215 216 217 Returns Axes. 218 """ 219 220 # --- check the kwargs 221 me = "summary_plot" 222 report_kwargs(called_from=me, **kwargs) 223 kwargs = validate_kwargs(SUMMARY_KW_TYPES, me, **kwargs) 224 225 # --- check the data 226 data = check_clean_timeseries(data, me) 227 if not isinstance(data, DataFrame): 228 raise TypeError("data must be a pandas DataFrame for summary_plot()") 229 df = DataFrame(data) # syntactic sugar for type hinting 230 231 # --- optional arguments 232 verbose = kwargs.pop("verbose", False) 233 middle = float(kwargs.pop("middle", 0.8)) 234 plot_type = kwargs.pop("plot_type", ZSCORES) 235 kwargs["legend"] = kwargs.get( 236 "legend", 237 { 238 # put the legend below the x-axis label 239 "loc": "upper center", 240 "fontsize": "xx-small", 241 "bbox_to_anchor": (0.5, -0.125), 242 "ncol": 4, 243 }, 244 ) 245 246 # get the data, calculate z-scores and scaled scores based on the start period 247 subset, kwargs = constrain_data(df, **kwargs) 248 z_scores, z_scaled = _calculate_z(subset, middle, verbose=verbose) 249 250 # plot as required by the plot_types argument 251 adjusted = z_scores if plot_type == ZSCORES else z_scaled 252 ax = _horizontal_bar_plot(subset, adjusted, middle, plot_type, kwargs) 253 ax.tick_params(axis="y", labelsize="small") 254 make_legend(ax, kwargs["legend"]) 255 ax.set_xlim(kwargs.get("xlim", None)) # provide space for the labels 256 257 return ax
Plot a summary of historical data for a given DataFrame.
Args:
- summary: DataFrame containing the summary data. The column names are used as labels for the plot.
- kwargs: additional arguments for the plot, including:
- plot_from: int | Period | None
- verbose: if True, print the summary data.
- middle: proportion of data to highlight (default is 0.8).
- plot_types: list of plot types to generate.
Returns Axes.
155def calc_growth(series: Series) -> DataFrame: 156 """ 157 Calculate annual and periodic growth for a pandas Series, 158 where the index is a PeriodIndex. 159 160 Args: 161 - series: A pandas Series with an appropriate PeriodIndex. 162 163 Returns a two column DataFrame: 164 165 Raises 166 - TypeError if the series is not a pandas Series. 167 - TypeError if the series index is not a PeriodIndex. 168 - ValueError if the series is empty. 169 - ValueError if the series index does not have a frequency of Q, M, or D. 170 - ValueError if the series index has duplicates. 171 """ 172 173 # --- sanity checks 174 if not isinstance(series, Series): 175 raise TypeError("The series argument must be a pandas Series") 176 if not isinstance(series.index, PeriodIndex): 177 raise TypeError("The series index must be a pandas PeriodIndex") 178 if series.empty: 179 raise ValueError("The series argument must not be empty") 180 if series.index.freqstr[0] not in ("Q", "M", "D"): 181 raise ValueError("The series index must have a frequency of Q, M, or D") 182 if series.index.has_duplicates: 183 raise ValueError("The series index must not have duplicate values") 184 185 # --- ensure the index is complete and the date is sorted 186 complete = period_range(start=series.index.min(), end=series.index.max()) 187 series = series.reindex(complete, fill_value=nan) 188 series = series.sort_index(ascending=True) 189 190 # --- calculate annual and periodic growth 191 ppy = {"Q": 4, "M": 12, "D": 365}[PeriodIndex(series.index).freqstr[:1]] 192 annual = series.pct_change(periods=ppy) * 100 193 periodic = series.pct_change(periods=1) * 100 194 periodic_name = {4: "Quarterly", 12: "Monthly", 365: "Daily"}[ppy] + " Growth" 195 return DataFrame( 196 { 197 "Annual Growth": annual, 198 periodic_name: periodic, 199 } 200 )
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.
203def growth_plot( 204 data: DataT, 205 **kwargs, 206) -> Axes: 207 """ 208 Plot annual growth (as a line) and periodic growth (as bars) 209 on the same axes. 210 211 Args: 212 - data: A pandas DataFrame with two columns: 213 - kwargs: 214 # --- common options 215 ax: Axes | None -- the matplotlib Axes to plot on, or None to create a new one. 216 plot_from: Period | int | None -- the period to start plotting from 217 label_series: bool -- whether to label the series in the legend. 218 max_ticks: int -- maximum number of ticks on the x-axis 219 # --- options passed to the line plot 220 line_width: float | int -- the width of the line 221 line_color: str -- the color of the line 222 line_style: str -- the style of the line 223 annotate_line: None | bool -- whether to annotate the end of the line 224 line_rounding: bool | int -- rounding for line annotation 225 line_fontsize: str | int | float -- fontsize for the line annotation 226 line_fontname: str -- font name for the line annotation 227 line_anno_color: str | bool | None -- color for the line annotation 228 # --- options passed to the bar plot 229 bar_width: float, 230 bar_color: str, 231 annotate_bars: None | bool -- whether to annotate the bars 232 above: bool -- whether to place the bar annotations above the bars 233 bar_fontsize: str | int | float -- fontsize for the bar annotations 234 bar_fontname: str -- font name for the bar annotations 235 bar_rounding: bool | int -- rounding for bar annotation 236 bar_anno_color: str | None -- color for the bar annotation 237 bar_rotation: int | float -- rotation for the bar annotation 238 } 239 Returns: 240 - axes: The matplotlib Axes object. 241 242 Raises: 243 - TypeError if the annual and periodic arguments are not pandas Series. 244 - TypeError if the annual index is not a PeriodIndex. 245 - ValueError if the annual and periodic series do not have the same index. 246 """ 247 248 # --- check the kwargs 249 me = "growth_plot" 250 report_kwargs(called_from=me, **kwargs) 251 kwargs = validate_kwargs(GROWTH_KW_TYPES, me, **kwargs) 252 253 # --- data checks 254 data = check_clean_timeseries(data, me) 255 if len(data.columns) != 2: 256 raise TypeError("The data argument must be a pandas DataFrame with two columns") 257 data, kwargs = constrain_data(data, **kwargs) 258 259 # --- get the series of interest ... 260 annual = data[data.columns[0]] 261 periodic = data[data.columns[1]] 262 263 # --- series names 264 annual.name = "Annual Growth" 265 periodic.name = {"M": "Monthly", "Q": "Quarterly", "D": "Daily"}[ 266 PeriodIndex(periodic.index).freqstr[:1] 267 ] + " Growth" 268 269 # --- convert PeriodIndex periodic growth data to integer indexed data. 270 saved_pi = map_periodindex(periodic) 271 if saved_pi is not None: 272 periodic = saved_pi[0] # extract the reindexed DataFrame 273 274 # --- simple bar chart for the periodic growth 275 if BAR_ANNO_COLOR not in kwargs or kwargs[BAR_ANNO_COLOR] is None: 276 kwargs[BAR_ANNO_COLOR] = "black" if kwargs.get(ABOVE, False) else "white" 277 selected = package_kwargs(to_bar_plot, **kwargs) 278 axes = bar_plot(periodic, **selected) 279 280 # --- and now the annual growth as a line 281 selected = package_kwargs(to_line_plot, **kwargs) 282 line_plot(annual, ax=axes, **selected) 283 284 # --- fix the x-axis labels 285 if saved_pi is not None: 286 set_labels(axes, saved_pi[1], kwargs.get("max_ticks", 10)) 287 288 # --- and done ... 289 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:
# --- common options
ax: Axes | None -- the matplotlib Axes to plot on, or None to create a new one.
plot_from: Period | int | None -- the period to start plotting from
label_series: bool -- whether to label the series in the legend.
max_ticks: int -- maximum number of ticks on the x-axis
# --- options passed to the line plot
line_width: float | int -- the width of the line
line_color: str -- the color of the line
line_style: str -- the style of the line
annotate_line: None | bool -- whether to annotate the end of the line
line_rounding: bool | int -- rounding for line annotation
line_fontsize: str | int | float -- fontsize for the line annotation
line_fontname: str -- font name for the line annotation
line_anno_color: str | bool | None -- color for the line annotation
# --- options passed to the bar plot
bar_width: float,
bar_color: str,
annotate_bars: None | bool -- whether to annotate the bars
above: bool -- whether to place the bar annotations above the bars
bar_fontsize: str | int | float -- fontsize for the bar annotations
bar_fontname: str -- font name for the bar annotations
bar_rounding: bool | int -- rounding for bar annotation
bar_anno_color: str | None -- color for the bar annotation
bar_rotation: int | float -- rotation for the bar annotation
} 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.
292def series_growth_plot( 293 data: DataT, 294 **kwargs, 295) -> Axes: 296 """ 297 Plot annual and periodic growth in percentage terms from 298 a pandas Series, and finalise the plot. 299 300 Args: 301 - data: A pandas Series with an appropriate PeriodIndex. 302 - kwargs: 303 - takes the same kwargs as for growth_plot() 304 """ 305 306 # --- check the kwargs 307 me = "series_growth_plot" 308 report_kwargs(called_from=me, **kwargs) 309 kwargs = validate_kwargs(SERIES_GROWTH_KW_TYPES, me, **kwargs) 310 311 # --- sanity checks 312 if not isinstance(data, Series): 313 raise TypeError( 314 "The data argument to series_growth_plot() must be a pandas Series" 315 ) 316 317 # --- calculate growth and plot - add ylabel 318 ylabel: str | None = kwargs.pop("ylabel", None) 319 if ylabel is not None: 320 print(f"Did you intend to specify a value for the 'ylabel' in {me}()?") 321 ylabel = "Growth (%)" if ylabel is None else ylabel 322 growth = calc_growth(data) 323 ax = growth_plot(growth, **kwargs) 324 ax.set_ylabel(ylabel) 325 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:
- takes the same kwargs as for growth_plot()
187def multi_start( 188 data: DataT, 189 function: Callable | list[Callable], 190 starts: Iterable[None | Period | int], 191 **kwargs, 192) -> None: 193 """ 194 Create multiple plots with different starting points. 195 Each plot will start from the specified starting point. 196 197 Parameters 198 - data: Series | DataFrame - The data to be plotted. 199 - function: Callable | list[Callable] - The plotting function 200 to be used. 201 - starts: Iterable[Period | int | None] - The starting points 202 for each plot (None means use the entire data). 203 - **kwargs: Additional keyword arguments to be passed to 204 the plotting function. 205 206 Returns None. 207 208 Raises 209 - ValueError if the starts is not an iterable of None, Period or int. 210 211 Note: kwargs['tag'] is used to create a unique tag for each plot. 212 """ 213 214 # --- sanity checks 215 me = "multi_start" 216 report_kwargs(called_from=me, **kwargs) 217 if not isinstance(starts, Iterable): 218 raise ValueError("starts must be an iterable of None, Period or int") 219 # data not checked here, assume it is checked by the called 220 # plot function. 221 222 # --- check the function argument 223 original_tag: Final[str] = kwargs.get("tag", "") 224 first, kwargs["function"] = first_unchain(function) 225 if not kwargs["function"]: 226 del kwargs["function"] # remove the function key if it is empty 227 228 # --- iterate over the starts 229 for i, start in enumerate(starts): 230 kw = kwargs.copy() # copy to avoid modifying the original kwargs 231 this_tag = f"{original_tag}_{i}" 232 kw["tag"] = this_tag 233 kw["plot_from"] = start # rely on plotting function to constrain the data 234 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.
237def multi_column( 238 data: DataFrame, 239 function: Callable | list[Callable], 240 **kwargs, 241) -> None: 242 """ 243 Create multiple plots, one for each column in a DataFrame. 244 The plot title will be the column name. 245 246 Parameters 247 - data: DataFrame - The data to be plotted 248 - function: Callable - The plotting function to be used. 249 - **kwargs: Additional keyword arguments to be passed to 250 the plotting function. 251 252 Returns None. 253 """ 254 255 # --- sanity checks 256 me = "multi_column" 257 report_kwargs(called_from=me, **kwargs) 258 if not isinstance(data, DataFrame): 259 raise TypeError("data must be a pandas DataFrame for multi_column()") 260 # Otherwise, the data is assumed to be checked by the called 261 # plot function, so we do not check it here. 262 263 # --- check the function argument 264 title_stem = kwargs.get("title", "") 265 tag: Final[str] = kwargs.get("tag", "") 266 first, kwargs["function"] = first_unchain(function) 267 if not kwargs["function"]: 268 del kwargs["function"] # remove the function key if it is empty 269 270 # --- iterate over the columns 271 for i, col in enumerate(data.columns): 272 273 series = data[[col]] 274 kwargs["title"] = f"{title_stem}{col}" if title_stem else col 275 276 this_tag = f"_{tag}_{i}".replace("__", "_") 277 kwargs["tag"] = this_tag 278 279 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.
122def plot_then_finalise( 123 data: DataT, 124 function: Callable | list[Callable], 125 **kwargs, 126) -> None: 127 """ 128 Chain a plotting function with the finalise_plot() function. 129 This is designed to be the last function in a chain. 130 131 Parameters 132 - data: Series | DataFrame - The data to be plotted. 133 - function: Callable | list[Callable] - The plotting function 134 to be used. 135 - **kwargs: Additional keyword arguments to be passed to 136 the plotting function, and then the finalise_plot() function. 137 138 Returns None. 139 """ 140 141 # --- checks 142 me = "plot_then_finalise" 143 report_kwargs(called_from=me, **kwargs) 144 # validate once we have established the first function 145 146 # data is not checked here, assume it is checked by the called 147 # plot function. 148 149 first, kwargs["function"] = first_unchain(function) 150 if not kwargs["function"]: 151 del kwargs["function"] # remove the function key if it is empty 152 153 bad_next = (multi_start, multi_column) 154 if first in bad_next: 155 # these functions should not be called by plot_then_finalise() 156 raise ValueError( 157 f"[{', '.join(k.__name__ for k in bad_next)}] should not be called by {me}. " 158 "Call them before calling {me}. " 159 ) 160 161 if first in EXPECTED_CALLABLES: 162 expected = EXPECTED_CALLABLES[first] 163 plot_kwargs = limit_kwargs(expected, **kwargs) 164 else: 165 # this is an unexpected Callable, so we will give it a try 166 print(f"Unknown proposed function: {first}; nonetheless, will give it a try.") 167 expected = {} 168 plot_kwargs = kwargs.copy() 169 170 # --- validate the original kwargs (could not do before now) 171 kwargs = validate_kwargs(expected | FINALISE_KW_TYPES, me, **kwargs) 172 173 # --- call the first function with the data and selected plot kwargs 174 axes = first(data, **plot_kwargs) 175 176 # --- remove potentially overlapping kwargs 177 fp_kwargs = limit_kwargs(FINALISE_KW_TYPES, **kwargs) 178 overlapping = expected.keys() & FINALISE_KW_TYPES.keys() 179 if overlapping: 180 for key in overlapping: 181 fp_kwargs.pop(key, None) # remove overlapping keys from kwargs 182 183 # --- finalise the plot 184 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.
69def line_plot_finalise( 70 data: DataT, 71 **kwargs, 72) -> None: 73 """ 74 A convenience function to call line_plot() then finalise_plot(). 75 """ 76 impose_legend(data=data, kwargs=kwargs) 77 plot_then_finalise(data, function=line_plot, **kwargs)
A convenience function to call line_plot() then finalise_plot().
80def bar_plot_finalise( 81 data: DataT, 82 **kwargs, 83) -> None: 84 """ 85 A convenience function to call bar_plot() and finalise_plot(). 86 """ 87 impose_legend(data=data, kwargs=kwargs) 88 plot_then_finalise( 89 data, 90 function=bar_plot, 91 **kwargs, 92 )
A convenience function to call bar_plot() and finalise_plot().
95def seastrend_plot_finalise( 96 data: DataT, 97 **kwargs, 98) -> None: 99 """ 100 A convenience function to call seas_trend_plot() and finalise_plot(). 101 """ 102 impose_legend(force=True, kwargs=kwargs) 103 plot_then_finalise(data, function=seastrend_plot, **kwargs)
A convenience function to call seas_trend_plot() and finalise_plot().
106def postcovid_plot_finalise( 107 data: DataT, 108 **kwargs, 109) -> None: 110 """ 111 A convenience function to call postcovid_plot() and finalise_plot(). 112 """ 113 impose_legend(force=True, kwargs=kwargs) 114 plot_then_finalise(data, function=postcovid_plot, **kwargs)
A convenience function to call postcovid_plot() and finalise_plot().
117def revision_plot_finalise( 118 data: DataT, 119 **kwargs, 120) -> None: 121 """ 122 A convenience function to call revision_plot() and finalise_plot(). 123 """ 124 impose_legend(force=True, kwargs=kwargs) 125 plot_then_finalise(data=data, function=revision_plot, **kwargs)
A convenience function to call revision_plot() and finalise_plot().
156def summary_plot_finalise( 157 data: DataT, 158 **kwargs, 159) -> None: 160 """ 161 A convenience function to call summary_plot() and finalise_plot(). 162 This is more complex than most convienience methods. 163 164 Arguments 165 - data: DataFrame containing the summary data. The index must be a PeriodIndex. 166 - kwargs: additional arguments for the plot, including: 167 - plot_from: int | Period | None (None means plot from 1995-01-01) 168 - verbose: if True, print the summary data. 169 - middle: proportion of data to highlight (default is 0.8). 170 - plot_type: list of plot types to generate (either "zscores" or "zscaled") 171 defaults to "zscores". 172 """ 173 174 # --- standard arguments 175 kwargs[TITLE] = kwargs.get(TITLE, f"Summary at {data.index[-1]}") 176 kwargs[PRESERVE_LIMS] = kwargs.get(PRESERVE_LIMS, True) 177 178 start: None | int | Period = kwargs.get(PLOT_FROM, None) 179 if start is None: 180 start = data.index[0] 181 if isinstance(start, int): 182 start = data.index[start] 183 kwargs[PLOT_FROM] = start 184 185 for plot_type in (ZSCORES, ZSCALED): 186 # some sorting of kwargs for plot production 187 kwargs[PLOT_TYPE] = plot_type 188 kwargs[PRE_TAG] = plot_type # necessary because the title is the same 189 190 if plot_type == "zscores": 191 kwargs[XLABEL] = f"Z-scores for prints since {start}" 192 kwargs[X0] = True 193 else: 194 kwargs[XLABEL] = f"-1 to 1 scaled z-scores since {start}" 195 kwargs.pop(X0, None) 196 197 plot_then_finalise( 198 data, 199 function=summary_plot, 200 **kwargs, 201 )
A convenience function to call summary_plot() and finalise_plot(). This is more complex than most convienience methods.
Arguments
- data: DataFrame containing the summary data. The index must be a PeriodIndex.
- kwargs: additional arguments for the plot, including:
- plot_from: int | Period | None (None means plot from 1995-01-01)
- verbose: if True, print the summary data.
- middle: proportion of data to highlight (default is 0.8).
- plot_type: list of plot types to generate (either "zscores" or "zscaled") defaults to "zscores".
146def growth_plot_finalise(data: DataT, **kwargs) -> None: 147 """ 148 A convenience function to call series_growth_plot() and finalise_plot(). 149 Use this when you are providing the raw growth data. Don't forget to 150 set the ylabel in kwargs. 151 """ 152 impose_legend(force=True, kwargs=kwargs) 153 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.
138def series_growth_plot_finalise(data: DataT, **kwargs) -> None: 139 """ 140 A convenience function to call series_growth_plot() and finalise_plot(). 141 """ 142 impose_legend(force=True, kwargs=kwargs) 143 plot_then_finalise(data=data, function=series_growth_plot, **kwargs)
A convenience function to call series_growth_plot() and finalise_plot().
128def run_plot_finalise( 129 data: DataT, 130 **kwargs, 131) -> None: 132 """ 133 A convenience function to call run_plot() and finalise_plot(). 134 """ 135 plot_then_finalise(data=data, function=run_plot, **kwargs)
A convenience function to call run_plot() and finalise_plot().