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
211def bar_plot( 212 data: DataT, 213 **kwargs, 214) -> Axes: 215 """ 216 Create a bar plot from the given data. Each column in the DataFrame 217 will be stacked on top of each other, with positive values above 218 zero and negative values below zero. 219 220 Parameters 221 - data: Series - The data to plot. Can be a DataFrame or a Series. 222 - **kwargs: dict Additional keyword arguments for customization. 223 # --- options for the entire bar plot 224 ax: Axes - axes to plot on, or None for new axes 225 stacked: bool - if True, the bars will be stacked. If False, they will be grouped. 226 max_ticks: int - maximum number of ticks on the x-axis (for PeriodIndex only) 227 plot_from: int | PeriodIndex - if provided, the plot will start from this index. 228 # --- options for each bar ... 229 color: str | list[str] - the color of the bars (or separate colors for each series 230 label_series: bool | list[bool] - if True, the series will be labeled in the legend 231 width: float | list[float] - the width of the bars 232 # - options for bar annotations 233 annotate: bool - If True them annotate the bars with their values. 234 fontsize: int | float | str - font size of the annotations 235 fontname: str - font name of the annotations 236 rounding: int - number of decimal places to round to 237 annotate_color: str - color of annotations 238 rotation: int | float - rotation of annotations in degrees 239 above: bool - if True, annotations are above the bar, else within the bar 240 241 Note: This function does not assume all data is timeseries with a PeriodIndex, 242 243 Returns 244 - axes: Axes - The axes for the plot. 245 """ 246 247 # --- check the kwargs 248 me = "bar_plot" 249 report_kwargs(called_from=me, **kwargs) 250 kwargs = validate_kwargs(BAR_KW_TYPES, me, **kwargs) 251 252 # --- get the data 253 # no call to check_clean_timeseries here, as bar plots are not 254 # necessarily timeseries data. If the data is a Series, it will be 255 # converted to a DataFrame with a single column. 256 df = DataFrame(data) # really we are only plotting DataFrames 257 df, kwargs = constrain_data(df, **kwargs) 258 item_count = len(df.columns) 259 260 # --- deal with complete PeriodIdex indicies 261 if not is_categorical(df): 262 print( 263 "Warning: bar_plot is not designed for incomplete or non-categorical data indexes." 264 ) 265 saved_pi = map_periodindex(df) 266 if saved_pi is not None: 267 df = saved_pi[0] # extract the reindexed DataFrame from the PeriodIndex 268 269 # --- set up the default arguments 270 chart_defaults: dict[str, Any] = { 271 STACKED: False, 272 MAX_TICKS: 10, 273 LABEL_SERIES: item_count > 1, 274 } 275 chart_args = {k: kwargs.get(k, v) for k, v in chart_defaults.items()} 276 277 bar_defaults: dict[str, Any] = { 278 COLOR: get_color_list(item_count), 279 WIDTH: get_setting("bar_width"), 280 LABEL_SERIES: (item_count > 1), 281 } 282 above = kwargs.get(ABOVE, False) 283 anno_args = { 284 ANNOTATE: kwargs.get(ANNOTATE, False), 285 FONTSIZE: kwargs.get(FONTSIZE, "small"), 286 FONTNAME: kwargs.get(FONTNAME, "Helvetica"), 287 ROTATION: kwargs.get(ROTATION, 0), 288 ROUNDING: kwargs.get(ROUNDING, True), 289 COLOR: kwargs.get(ANNOTATE_COLOR, "black" if above else "white"), 290 ABOVE: above, 291 } 292 bar_args, remaining_kwargs = apply_defaults(item_count, bar_defaults, kwargs) 293 294 # --- plot the data 295 axes, _rkwargs = get_axes(**remaining_kwargs) 296 if chart_args[STACKED]: 297 stacked(axes, df, anno_args, **bar_args) 298 else: 299 grouped(axes, df, anno_args, **bar_args) 300 301 # --- handle complete periodIndex data and label rotation 302 rotate_labels = True 303 if saved_pi is not None: 304 set_labels(axes, saved_pi[1], chart_args["max_ticks"]) 305 rotate_labels = False 306 307 if rotate_labels: 308 plt.xticks(rotation=90) 309 310 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] = ( 151 kwargs.get(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
207def summary_plot( 208 data: DataT, # summary data 209 **kwargs, 210) -> Axes: 211 """Plot a summary of historical data for a given DataFrame. 212 213 Args: 214 - summary: DataFrame containing the summary data. The column names are 215 used as labels for the plot. 216 - kwargs: additional arguments for the plot, including: 217 - plot_from: int | Period | None 218 - verbose: if True, print the summary data. 219 - middle: proportion of data to highlight (default is 0.8). 220 - plot_types: list of plot types to generate. 221 222 223 Returns Axes. 224 """ 225 226 # --- check the kwargs 227 me = "summary_plot" 228 report_kwargs(called_from=me, **kwargs) 229 kwargs = validate_kwargs(SUMMARY_KW_TYPES, me, **kwargs) 230 231 # --- check the data 232 data = check_clean_timeseries(data, me) 233 if not isinstance(data, DataFrame): 234 raise TypeError("data must be a pandas DataFrame for summary_plot()") 235 df = DataFrame(data) # syntactic sugar for type hinting 236 237 # --- optional arguments 238 verbose = kwargs.pop("verbose", False) 239 middle = float(kwargs.pop("middle", 0.8)) 240 plot_type = kwargs.pop("plot_type", ZSCORES) 241 kwargs["legend"] = kwargs.get( 242 "legend", 243 { 244 # put the legend below the x-axis label 245 "loc": "upper center", 246 "fontsize": "xx-small", 247 "bbox_to_anchor": (0.5, -0.125), 248 "ncol": 4, 249 }, 250 ) 251 252 # get the data, calculate z-scores and scaled scores based on the start period 253 subset, kwargs = constrain_data(df, **kwargs) 254 z_scores, z_scaled = _calculate_z(subset, middle, verbose=verbose) 255 256 # plot as required by the plot_types argument 257 adjusted = z_scores if plot_type == ZSCORES else z_scaled 258 ax = _horizontal_bar_plot(subset, adjusted, middle, plot_type, kwargs) 259 ax.tick_params(axis="y", labelsize="small") 260 make_legend(ax, kwargs["legend"]) 261 ax.set_xlim(kwargs.get("xlim", None)) # provide space for the labels 262 263 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.
146def calc_growth(series: Series) -> DataFrame: 147 """ 148 Calculate annual and periodic growth for a pandas Series, 149 where the index is a PeriodIndex. 150 151 Args: 152 - series: A pandas Series with an appropriate PeriodIndex. 153 154 Returns a two column DataFrame: 155 156 Raises 157 - TypeError if the series is not a pandas Series. 158 - TypeError if the series index is not a PeriodIndex. 159 - ValueError if the series is empty. 160 - ValueError if the series index does not have a frequency of Q, M, or D. 161 - ValueError if the series index has duplicates. 162 """ 163 164 # --- sanity checks 165 if not isinstance(series, Series): 166 raise TypeError("The series argument must be a pandas Series") 167 if not isinstance(series.index, PeriodIndex): 168 raise TypeError("The series index must be a pandas PeriodIndex") 169 if series.empty: 170 raise ValueError("The series argument must not be empty") 171 if series.index.freqstr[0] not in ("Q", "M", "D"): 172 raise ValueError("The series index must have a frequency of Q, M, or D") 173 if series.index.has_duplicates: 174 raise ValueError("The series index must not have duplicate values") 175 176 # --- ensure the index is complete and the date is sorted 177 complete = period_range(start=series.index.min(), end=series.index.max()) 178 series = series.reindex(complete, fill_value=nan) 179 series = series.sort_index(ascending=True) 180 181 # --- calculate annual and periodic growth 182 ppy = {"Q": 4, "M": 12, "D": 365}[PeriodIndex(series.index).freqstr[:1]] 183 annual = series.pct_change(periods=ppy) * 100 184 periodic = series.pct_change(periods=1) * 100 185 periodic_name = {4: "Quarterly", 12: "Monthly", 365: "Daily"}[ppy] + " Growth" 186 return DataFrame( 187 { 188 "Annual Growth": annual, 189 periodic_name: periodic, 190 } 191 )
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.
214def growth_plot( 215 data: DataT, 216 **kwargs, 217) -> Axes: 218 """ 219 Plot annual growth (as a line) and periodic growth (as bars) 220 on the same axes. 221 222 Args: 223 - data: A pandas DataFrame with two columns: 224 - kwargs: 225 - line_width: The width of the line (default is 2). 226 - line_color: The color of the line (default is "darkblue"). 227 - line_style: The style of the line (default is "-"). 228 - annotate_line: None | bool | int | str - fontsize to annotate 229 the line (default is "small", which means the line is annotated with 230 small text). 231 - rounding: None | bool | int - the number of decimal places to round 232 the line (default is 0). 233 - bar_width: The width of the bars (default is 0.8). 234 - bar_color: The color of the bars (default is "indianred"). 235 - annotate_bar: None | int | str - fontsize to annotate the bars 236 (default is "small", which means the bars are annotated with 237 small text). 238 - bar_rounding: The number of decimal places to round the 239 annotations to (default is 1). 240 - plot_from: None | Period | int -- if: 241 - None: the entire series is plotted 242 - Period: the plot starts from this period 243 - int: the plot starts from this +/- index position 244 - max_ticks: The maximum number of ticks to show on the x-axis 245 (default is 10). 246 247 Returns: 248 - axes: The matplotlib Axes object. 249 250 Raises: 251 - TypeError if the annual and periodic arguments are not pandas Series. 252 - TypeError if the annual index is not a PeriodIndex. 253 - ValueError if the annual and periodic series do not have the same index. 254 """ 255 256 # --- check the kwargs 257 me = "growth_plot" 258 report_kwargs(called_from=me, **kwargs) 259 kwargs = validate_kwargs(GROWTH_KW_TYPES, me, **kwargs) 260 261 # --- data checks 262 data = check_clean_timeseries(data, me) 263 if len(data.columns) != 2: 264 raise TypeError("The data argument must be a pandas DataFrame with two columns") 265 data, kwargs = constrain_data(data, **kwargs) 266 267 # --- get the series of interest ... 268 annual = data[data.columns[0]] 269 periodic = data[data.columns[1]] 270 271 # --- series names 272 annual.name = "Annual Growth" 273 periodic.name = {"M": "Monthly", "Q": "Quarterly", "D": "Daily"}[ 274 PeriodIndex(periodic.index).freqstr[:1] 275 ] + " Growth" 276 277 # --- convert PeriodIndex periodic growth data to integer indexed data. 278 saved_pi = map_periodindex(periodic) 279 if saved_pi is not None: 280 periodic = saved_pi[0] # extract the reindexed DataFrame 281 282 # --- simple bar chart for the periodic growth 283 if BAR_ANNO_COLOR not in kwargs or kwargs[BAR_ANNO_COLOR] is None: 284 kwargs[BAR_ANNO_COLOR] = "black" if kwargs.get(ABOVE, False) else "white" 285 selected = package_kwargs(to_bar_plot, **kwargs) 286 axes = bar_plot(periodic, **selected) 287 288 # --- and now the annual growth as a line 289 selected = package_kwargs(to_line_plot, **kwargs) 290 line_plot(annual, ax=axes, **selected) 291 292 # --- fix the x-axis labels 293 if saved_pi is not None: 294 set_labels(axes, saved_pi[1], kwargs.get("max_ticks", 10)) 295 296 # --- and done ... 297 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:
- line_width: The width of the line (default is 2).
- line_color: The color of the line (default is "darkblue").
- line_style: The style of the line (default is "-").
- annotate_line: None | bool | int | str - fontsize to annotate the line (default is "small", which means the line is annotated with small text).
- rounding: None | bool | int - the number of decimal places to round the line (default is 0).
- bar_width: The width of the bars (default is 0.8).
- bar_color: The color of the bars (default is "indianred").
- annotate_bar: None | int | str - fontsize to annotate the bars (default is "small", which means the bars are annotated with small text).
- bar_rounding: The number of decimal places to round the annotations to (default is 1).
- plot_from: None | Period | int -- if:
- None: the entire series is plotted
- Period: the plot starts from this period
- int: the plot starts from this +/- index position
- max_ticks: The maximum number of ticks to show on the x-axis (default is 10).
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.
300def series_growth_plot( 301 data: DataT, 302 **kwargs, 303) -> Axes: 304 """ 305 Plot annual and periodic growth in percentage terms from 306 a pandas Series, and finalise the plot. 307 308 Args: 309 - data: A pandas Series with an appropriate PeriodIndex. 310 - kwargs: 311 - takes the same kwargs as for growth_plot() 312 """ 313 314 # --- check the kwargs 315 me = "series_growth_plot" 316 report_kwargs(called_from=me, **kwargs) 317 kwargs = validate_kwargs(SERIES_GROWTH_KW_TYPES, me, **kwargs) 318 319 # --- sanity checks 320 if not isinstance(data, Series): 321 raise TypeError( 322 "The data argument to series_growth_plot() must be a pandas Series" 323 ) 324 325 # --- calculate growth and plot - add ylabel 326 ylabel: str | None = kwargs.pop("ylabel", None) 327 if ylabel is not None: 328 print(f"Did you intend to specify a value for the 'ylabel' in {me}()?") 329 ylabel = "Growth (%)" if ylabel is None else ylabel 330 growth = calc_growth(data) 331 ax = growth_plot(growth, **kwargs) 332 ax.set_ylabel(ylabel) 333 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.
72def line_plot_finalise( 73 data: DataT, 74 **kwargs, 75) -> None: 76 """ 77 A convenience function to call line_plot() then finalise_plot(). 78 """ 79 impose_legend(data=data, kwargs=kwargs) 80 plot_then_finalise(data, function=line_plot, **kwargs)
A convenience function to call line_plot() then finalise_plot().
84def bar_plot_finalise( 85 data: DataT, 86 **kwargs, 87) -> None: 88 """ 89 A convenience function to call bar_plot() and finalise_plot(). 90 """ 91 impose_legend(data=data, kwargs=kwargs) 92 plot_then_finalise( 93 data, 94 function=bar_plot, 95 **kwargs, 96 )
A convenience function to call bar_plot() and finalise_plot().
99def seastrend_plot_finalise( 100 data: DataT, 101 **kwargs, 102) -> None: 103 """ 104 A convenience function to call seas_trend_plot() and finalise_plot(). 105 """ 106 impose_legend(force=True, kwargs=kwargs) 107 plot_then_finalise(data, function=seastrend_plot, **kwargs)
A convenience function to call seas_trend_plot() and finalise_plot().
110def postcovid_plot_finalise( 111 data: DataT, 112 **kwargs, 113) -> None: 114 """ 115 A convenience function to call postcovid_plot() and finalise_plot(). 116 """ 117 impose_legend(force=True, kwargs=kwargs) 118 plot_then_finalise(data, function=postcovid_plot, **kwargs)
A convenience function to call postcovid_plot() and finalise_plot().
121def revision_plot_finalise( 122 data: DataT, 123 **kwargs, 124) -> None: 125 """ 126 A convenience function to call revision_plot() and finalise_plot(). 127 """ 128 impose_legend(force=True, kwargs=kwargs) 129 plot_then_finalise(data=data, function=revision_plot, **kwargs)
A convenience function to call revision_plot() and finalise_plot().
160def summary_plot_finalise( 161 data: DataT, 162 **kwargs, 163) -> None: 164 """ 165 A convenience function to call summary_plot() and finalise_plot(). 166 This is more complex than most convienience methods. 167 168 Arguments 169 - data: DataFrame containing the summary data. The index must be a PeriodIndex. 170 - kwargs: additional arguments for the plot, including: 171 - plot_from: int | Period | None (None means plot from 1995-01-01) 172 - verbose: if True, print the summary data. 173 - middle: proportion of data to highlight (default is 0.8). 174 - plot_type: list of plot types to generate (either "zscores" or "zscaled") 175 defaults to "zscores". 176 """ 177 178 # --- standard arguments 179 kwargs[TITLE] = kwargs.get(TITLE, f"Summary at {data.index[-1]}") 180 kwargs[PRESERVE_LIMS] = kwargs.get(PRESERVE_LIMS, True) 181 182 start: None | int | Period = kwargs.get(PLOT_FROM, None) 183 if start is None: 184 start = data.index[0] 185 if isinstance(start, int): 186 start = data.index[start] 187 kwargs[PLOT_FROM] = start 188 189 for plot_type in (ZSCORES, ZSCALED): 190 # some sorting of kwargs for plot production 191 kwargs[PLOT_TYPE] = plot_type 192 kwargs[PRE_TAG] = plot_type # necessary because the title is the same 193 194 if plot_type == "zscores": 195 kwargs[XLABEL] = f"Z-scores for prints since {start}" 196 kwargs[X0] = True 197 else: 198 kwargs[XLABEL] = f"-1 to 1 scaled z-scores since {start}" 199 kwargs.pop(X0, None) 200 201 plot_then_finalise( 202 data, 203 function=summary_plot, 204 **kwargs, 205 )
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".
150def growth_plot_finalise(data: DataT, **kwargs) -> None: 151 """ 152 A convenience function to call series_growth_plot() and finalise_plot(). 153 Use this when you are providing the raw growth data. Don't forget to 154 set the ylabel in kwargs. 155 """ 156 impose_legend(force=True, kwargs=kwargs) 157 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.
142def series_growth_plot_finalise(data: DataT, **kwargs) -> None: 143 """ 144 A convenience function to call series_growth_plot() and finalise_plot(). 145 """ 146 impose_legend(force=True, kwargs=kwargs) 147 plot_then_finalise(data=data, function=series_growth_plot, **kwargs)
A convenience function to call series_growth_plot() and finalise_plot().
132def run_plot_finalise( 133 data: DataT, 134 **kwargs, 135) -> None: 136 """ 137 A convenience function to call run_plot() and finalise_plot(). 138 """ 139 plot_then_finalise(data=data, function=run_plot, **kwargs)
A convenience function to call run_plot() and finalise_plot().