mgplot.finalise_plot
finalise_plot.py: This module provides a function to finalise and save plots to the file system. It is used to publish plots.
1""" 2finalise_plot.py: 3This module provides a function to finalise and save plots to the 4file system. It is used to publish plots. 5""" 6 7# --- imports 8from typing import Any, Final, NotRequired, Unpack 9from collections.abc import Sequence 10import re 11import matplotlib as mpl 12import matplotlib.pyplot as plt 13from matplotlib.pyplot import Axes, Figure 14 15from mgplot.settings import get_setting 16from mgplot.keyword_checking import validate_kwargs, report_kwargs, BaseKwargs 17 18 19# --- constants 20ME: Final[str] = "finalise_plot" 21 22 23class FinaliseKwargs(BaseKwargs): 24 """Keyword arguments for the finalise_plot function.""" 25 26 # --- value options 27 title: NotRequired[str | None] 28 xlabel: NotRequired[str | None] 29 ylabel: NotRequired[str | None] 30 xlim: NotRequired[tuple[float, float] | None] 31 ylim: NotRequired[tuple[float, float] | None] 32 xticks: NotRequired[list[float] | None] 33 yticks: NotRequired[list[float] | None] 34 x_scale: NotRequired[str | None] 35 y_scale: NotRequired[str | None] 36 # --- splat options 37 legend: NotRequired[bool | dict[str, Any] | None] 38 axhspan: NotRequired[dict[str, Any]] 39 axvspan: NotRequired[dict[str, Any]] 40 axhline: NotRequired[dict[str, Any]] 41 axvline: NotRequired[dict[str, Any]] 42 # --- options for annotations 43 lfooter: NotRequired[str] 44 rfooter: NotRequired[str] 45 lheader: NotRequired[str] 46 rheader: NotRequired[str] 47 # --- file/save options 48 pre_tag: NotRequired[str] 49 tag: NotRequired[str] 50 chart_dir: NotRequired[str] 51 file_type: NotRequired[str] 52 dpi: NotRequired[int] 53 figsize: NotRequired[tuple[float, float]] 54 show: NotRequired[bool] 55 # --- other options 56 preserve_lims: NotRequired[bool] 57 remove_legend: NotRequired[bool] 58 zero_y: NotRequired[bool] 59 y0: NotRequired[bool] 60 x0: NotRequired[bool] 61 dont_save: NotRequired[bool] 62 dont_close: NotRequired[bool] 63 64 65value_kwargs = ( 66 "title", 67 "xlabel", 68 "ylabel", 69 "xlim", 70 "ylim", 71 "xticks", 72 "yticks", 73 "x_scale", 74 "y_scale", 75) 76splat_kwargs = ( 77 "legend", 78 "axhspan", 79 "axvspan", 80 "axhline", 81 "axvline", 82) 83annotation_kwargs = ( 84 "lfooter", 85 "rfooter", 86 "lheader", 87 "rheader", 88) 89 90 91# filename limitations - regex used to map the plot title to a filename 92_remove = re.compile(r"[^0-9A-Za-z]") # sensible file names from alphamum title 93_reduce = re.compile(r"[-]+") # eliminate multiple hyphens 94 95 96def make_legend(axes: Axes, legend: None | bool | dict[str, Any]) -> None: 97 """Create a legend for the plot.""" 98 99 if legend is None or legend is False: 100 return 101 102 if legend is True: # use the global default settings 103 legend = get_setting("legend") 104 105 if isinstance(legend, dict): 106 axes.legend(**legend) 107 return 108 109 print(f"Warning: expected dict argument for legend, but got {type(legend)}.") 110 111 112def apply_value_kwargs(axes: Axes, settings: Sequence[str], **kwargs) -> None: 113 """Set matplotlib elements by name using Axes.set().""" 114 115 for setting in settings: 116 value = kwargs.get(setting, None) 117 if value is None and setting not in ("title", "xlabel", "ylabel"): 118 continue 119 if setting == "ylabel" and value is None and axes.get_ylabel(): 120 # already set - probably in series_growth_plot() - so skip 121 continue 122 axes.set(**{setting: value}) 123 124 125def apply_splat_kwargs(axes: Axes, settings: tuple, **kwargs) -> None: 126 """ 127 Set matplotlib elements dynamically using setting_name and splat. 128 This is used for legend, axhspan, axvspan, axhline, and axvline. 129 These can be ignored if not in kwargs, or set to None in kwargs. 130 """ 131 132 for method_name in settings: 133 if method_name in kwargs: 134 if method_name == "legend": 135 # special case for legend 136 make_legend(axes, kwargs[method_name]) 137 continue 138 139 if kwargs[method_name] is None or kwargs[method_name] is False: 140 continue 141 142 if kwargs[method_name] is True: # use the global default settings 143 kwargs[method_name] = get_setting(method_name) 144 145 # splat the kwargs to the method 146 if isinstance(kwargs[method_name], dict): 147 method = getattr(axes, method_name) 148 method(**kwargs[method_name]) 149 else: 150 print( 151 f"Warning expected dict argument for {method_name} but got " 152 + f"{type(kwargs[method_name])}." 153 ) 154 155 156def apply_annotations(axes: Axes, **kwargs) -> None: 157 """Set figure size and apply chart annotations.""" 158 159 fig = axes.figure 160 fig_size = kwargs.get("figsize", get_setting("figsize")) 161 if not isinstance(fig, mpl.figure.SubFigure): 162 fig.set_size_inches(*fig_size) 163 164 annotations = { 165 "rfooter": (0.99, 0.001, "right", "bottom"), 166 "lfooter": (0.01, 0.001, "left", "bottom"), 167 "rheader": (0.99, 0.999, "right", "top"), 168 "lheader": (0.01, 0.999, "left", "top"), 169 } 170 171 for annotation in annotation_kwargs: 172 if annotation in kwargs: 173 x_pos, y_pos, h_align, v_align = annotations[annotation] 174 fig.text( 175 x_pos, 176 y_pos, 177 kwargs[annotation], 178 ha=h_align, 179 va=v_align, 180 fontsize=8, 181 fontstyle="italic", 182 color="#999999", 183 ) 184 185 186def apply_late_kwargs(axes: Axes, **kwargs) -> None: 187 """Apply settings found in kwargs, after plotting the data.""" 188 apply_splat_kwargs(axes, splat_kwargs, **kwargs) 189 190 191def apply_kwargs(axes: Axes, **kwargs) -> None: 192 """Apply settings found in kwargs.""" 193 194 def check_kwargs(name): 195 return name in kwargs and kwargs[name] 196 197 apply_value_kwargs(axes, value_kwargs, **kwargs) 198 apply_annotations(axes, **kwargs) 199 200 if check_kwargs("zero_y"): 201 bottom, top = axes.get_ylim() 202 adj = (top - bottom) * 0.02 203 if bottom > -adj: 204 axes.set_ylim(bottom=-adj) 205 if top < adj: 206 axes.set_ylim(top=adj) 207 208 if check_kwargs("y0"): 209 low, high = axes.get_ylim() 210 if low < 0 < high: 211 axes.axhline(y=0, lw=0.66, c="#555555") 212 213 if check_kwargs("x0"): 214 low, high = axes.get_xlim() 215 if low < 0 < high: 216 axes.axvline(x=0, lw=0.66, c="#555555") 217 218 219def save_to_file(fig: Figure, **kwargs) -> None: 220 """Save the figure to file.""" 221 222 saving = not kwargs.get("dont_save", False) # save by default 223 if saving: 224 chart_dir = kwargs.get("chart_dir", get_setting("chart_dir")) 225 if not chart_dir.endswith("/"): 226 chart_dir += "/" 227 228 title = kwargs.get("title", "") 229 max_title_len = 150 # avoid overly long file names 230 shorter = title if len(title) < max_title_len else title[:max_title_len] 231 pre_tag = kwargs.get("pre_tag", "") 232 tag = kwargs.get("tag", "") 233 file_title = re.sub(_remove, "-", shorter).lower() 234 file_title = re.sub(_reduce, "-", file_title) 235 file_type = kwargs.get("file_type", get_setting("file_type")).lower() 236 dpi = kwargs.get("dpi", get_setting("dpi")) 237 fig.savefig(f"{chart_dir}{pre_tag}{file_title}-{tag}.{file_type}", dpi=dpi) 238 239 240# - public functions for finalise_plot() 241 242 243def finalise_plot(axes: Axes, **kwargs: Unpack[FinaliseKwargs]) -> None: 244 """ 245 A function to finalise and save plots to the file system. The filename 246 for the saved plot is constructed from the global chart_dir, the plot's title, 247 any specified tag text, and the file_type for the plot. 248 249 Arguments: 250 - axes - matplotlib axes object - required 251 - kwargs: FinaliseKwargs 252 253 Returns: 254 - None 255 """ 256 257 # --- check the kwargs 258 me = "finalise_plot" 259 report_kwargs(caller=me, **kwargs) 260 validate_kwargs(schema=FinaliseKwargs, caller=me, **kwargs) 261 262 # --- sanity checks 263 if len(axes.get_children()) < 1: 264 print("Warning: finalise_plot() called with empty axes, which was ignored.") 265 return 266 267 # --- remember axis-limits should we need to restore thems 268 xlim, ylim = axes.get_xlim(), axes.get_ylim() 269 270 # margins 271 axes.margins(0.02) 272 axes.autoscale(tight=False) # This is problematic ... 273 274 apply_kwargs(axes, **kwargs) 275 276 # tight layout and save the figure 277 fig = axes.figure 278 if "preserve_lims" in kwargs and kwargs["preserve_lims"]: 279 # restore the original limits of the axes 280 axes.set_xlim(xlim) 281 axes.set_ylim(ylim) 282 if not isinstance(fig, mpl.figure.SubFigure): # mypy 283 fig.tight_layout(pad=1.1) 284 apply_late_kwargs(axes, **kwargs) 285 legend = axes.get_legend() 286 if legend and kwargs.get("remove_legend", False): 287 legend.remove() 288 if not isinstance(fig, mpl.figure.SubFigure): # mypy 289 save_to_file(fig, **kwargs) 290 291 # show the plot in Jupyter Lab 292 if "show" in kwargs and kwargs["show"]: 293 plt.show() 294 295 # And close 296 closing = True if "dont_close" not in kwargs else not kwargs["dont_close"] 297 if closing: 298 plt.close()
24class FinaliseKwargs(BaseKwargs): 25 """Keyword arguments for the finalise_plot function.""" 26 27 # --- value options 28 title: NotRequired[str | None] 29 xlabel: NotRequired[str | None] 30 ylabel: NotRequired[str | None] 31 xlim: NotRequired[tuple[float, float] | None] 32 ylim: NotRequired[tuple[float, float] | None] 33 xticks: NotRequired[list[float] | None] 34 yticks: NotRequired[list[float] | None] 35 x_scale: NotRequired[str | None] 36 y_scale: NotRequired[str | None] 37 # --- splat options 38 legend: NotRequired[bool | dict[str, Any] | None] 39 axhspan: NotRequired[dict[str, Any]] 40 axvspan: NotRequired[dict[str, Any]] 41 axhline: NotRequired[dict[str, Any]] 42 axvline: NotRequired[dict[str, Any]] 43 # --- options for annotations 44 lfooter: NotRequired[str] 45 rfooter: NotRequired[str] 46 lheader: NotRequired[str] 47 rheader: NotRequired[str] 48 # --- file/save options 49 pre_tag: NotRequired[str] 50 tag: NotRequired[str] 51 chart_dir: NotRequired[str] 52 file_type: NotRequired[str] 53 dpi: NotRequired[int] 54 figsize: NotRequired[tuple[float, float]] 55 show: NotRequired[bool] 56 # --- other options 57 preserve_lims: NotRequired[bool] 58 remove_legend: NotRequired[bool] 59 zero_y: NotRequired[bool] 60 y0: NotRequired[bool] 61 x0: NotRequired[bool] 62 dont_save: NotRequired[bool] 63 dont_close: NotRequired[bool]
Keyword arguments for the finalise_plot function.
97def make_legend(axes: Axes, legend: None | bool | dict[str, Any]) -> None: 98 """Create a legend for the plot.""" 99 100 if legend is None or legend is False: 101 return 102 103 if legend is True: # use the global default settings 104 legend = get_setting("legend") 105 106 if isinstance(legend, dict): 107 axes.legend(**legend) 108 return 109 110 print(f"Warning: expected dict argument for legend, but got {type(legend)}.")
Create a legend for the plot.
113def apply_value_kwargs(axes: Axes, settings: Sequence[str], **kwargs) -> None: 114 """Set matplotlib elements by name using Axes.set().""" 115 116 for setting in settings: 117 value = kwargs.get(setting, None) 118 if value is None and setting not in ("title", "xlabel", "ylabel"): 119 continue 120 if setting == "ylabel" and value is None and axes.get_ylabel(): 121 # already set - probably in series_growth_plot() - so skip 122 continue 123 axes.set(**{setting: value})
Set matplotlib elements by name using Axes.set().
126def apply_splat_kwargs(axes: Axes, settings: tuple, **kwargs) -> None: 127 """ 128 Set matplotlib elements dynamically using setting_name and splat. 129 This is used for legend, axhspan, axvspan, axhline, and axvline. 130 These can be ignored if not in kwargs, or set to None in kwargs. 131 """ 132 133 for method_name in settings: 134 if method_name in kwargs: 135 if method_name == "legend": 136 # special case for legend 137 make_legend(axes, kwargs[method_name]) 138 continue 139 140 if kwargs[method_name] is None or kwargs[method_name] is False: 141 continue 142 143 if kwargs[method_name] is True: # use the global default settings 144 kwargs[method_name] = get_setting(method_name) 145 146 # splat the kwargs to the method 147 if isinstance(kwargs[method_name], dict): 148 method = getattr(axes, method_name) 149 method(**kwargs[method_name]) 150 else: 151 print( 152 f"Warning expected dict argument for {method_name} but got " 153 + f"{type(kwargs[method_name])}." 154 )
Set matplotlib elements dynamically using setting_name and splat. This is used for legend, axhspan, axvspan, axhline, and axvline. These can be ignored if not in kwargs, or set to None in kwargs.
157def apply_annotations(axes: Axes, **kwargs) -> None: 158 """Set figure size and apply chart annotations.""" 159 160 fig = axes.figure 161 fig_size = kwargs.get("figsize", get_setting("figsize")) 162 if not isinstance(fig, mpl.figure.SubFigure): 163 fig.set_size_inches(*fig_size) 164 165 annotations = { 166 "rfooter": (0.99, 0.001, "right", "bottom"), 167 "lfooter": (0.01, 0.001, "left", "bottom"), 168 "rheader": (0.99, 0.999, "right", "top"), 169 "lheader": (0.01, 0.999, "left", "top"), 170 } 171 172 for annotation in annotation_kwargs: 173 if annotation in kwargs: 174 x_pos, y_pos, h_align, v_align = annotations[annotation] 175 fig.text( 176 x_pos, 177 y_pos, 178 kwargs[annotation], 179 ha=h_align, 180 va=v_align, 181 fontsize=8, 182 fontstyle="italic", 183 color="#999999", 184 )
Set figure size and apply chart annotations.
187def apply_late_kwargs(axes: Axes, **kwargs) -> None: 188 """Apply settings found in kwargs, after plotting the data.""" 189 apply_splat_kwargs(axes, splat_kwargs, **kwargs)
Apply settings found in kwargs, after plotting the data.
192def apply_kwargs(axes: Axes, **kwargs) -> None: 193 """Apply settings found in kwargs.""" 194 195 def check_kwargs(name): 196 return name in kwargs and kwargs[name] 197 198 apply_value_kwargs(axes, value_kwargs, **kwargs) 199 apply_annotations(axes, **kwargs) 200 201 if check_kwargs("zero_y"): 202 bottom, top = axes.get_ylim() 203 adj = (top - bottom) * 0.02 204 if bottom > -adj: 205 axes.set_ylim(bottom=-adj) 206 if top < adj: 207 axes.set_ylim(top=adj) 208 209 if check_kwargs("y0"): 210 low, high = axes.get_ylim() 211 if low < 0 < high: 212 axes.axhline(y=0, lw=0.66, c="#555555") 213 214 if check_kwargs("x0"): 215 low, high = axes.get_xlim() 216 if low < 0 < high: 217 axes.axvline(x=0, lw=0.66, c="#555555")
Apply settings found in kwargs.
220def save_to_file(fig: Figure, **kwargs) -> None: 221 """Save the figure to file.""" 222 223 saving = not kwargs.get("dont_save", False) # save by default 224 if saving: 225 chart_dir = kwargs.get("chart_dir", get_setting("chart_dir")) 226 if not chart_dir.endswith("/"): 227 chart_dir += "/" 228 229 title = kwargs.get("title", "") 230 max_title_len = 150 # avoid overly long file names 231 shorter = title if len(title) < max_title_len else title[:max_title_len] 232 pre_tag = kwargs.get("pre_tag", "") 233 tag = kwargs.get("tag", "") 234 file_title = re.sub(_remove, "-", shorter).lower() 235 file_title = re.sub(_reduce, "-", file_title) 236 file_type = kwargs.get("file_type", get_setting("file_type")).lower() 237 dpi = kwargs.get("dpi", get_setting("dpi")) 238 fig.savefig(f"{chart_dir}{pre_tag}{file_title}-{tag}.{file_type}", dpi=dpi)
Save the figure to file.
244def finalise_plot(axes: Axes, **kwargs: Unpack[FinaliseKwargs]) -> None: 245 """ 246 A function to finalise and save plots to the file system. The filename 247 for the saved plot is constructed from the global chart_dir, the plot's title, 248 any specified tag text, and the file_type for the plot. 249 250 Arguments: 251 - axes - matplotlib axes object - required 252 - kwargs: FinaliseKwargs 253 254 Returns: 255 - None 256 """ 257 258 # --- check the kwargs 259 me = "finalise_plot" 260 report_kwargs(caller=me, **kwargs) 261 validate_kwargs(schema=FinaliseKwargs, caller=me, **kwargs) 262 263 # --- sanity checks 264 if len(axes.get_children()) < 1: 265 print("Warning: finalise_plot() called with empty axes, which was ignored.") 266 return 267 268 # --- remember axis-limits should we need to restore thems 269 xlim, ylim = axes.get_xlim(), axes.get_ylim() 270 271 # margins 272 axes.margins(0.02) 273 axes.autoscale(tight=False) # This is problematic ... 274 275 apply_kwargs(axes, **kwargs) 276 277 # tight layout and save the figure 278 fig = axes.figure 279 if "preserve_lims" in kwargs and kwargs["preserve_lims"]: 280 # restore the original limits of the axes 281 axes.set_xlim(xlim) 282 axes.set_ylim(ylim) 283 if not isinstance(fig, mpl.figure.SubFigure): # mypy 284 fig.tight_layout(pad=1.1) 285 apply_late_kwargs(axes, **kwargs) 286 legend = axes.get_legend() 287 if legend and kwargs.get("remove_legend", False): 288 legend.remove() 289 if not isinstance(fig, mpl.figure.SubFigure): # mypy 290 save_to_file(fig, **kwargs) 291 292 # show the plot in Jupyter Lab 293 if "show" in kwargs and kwargs["show"]: 294 plt.show() 295 296 # And close 297 closing = True if "dont_close" not in kwargs else not kwargs["dont_close"] 298 if closing: 299 plt.close()
A function to finalise and save plots to the file system. The filename for the saved plot is constructed from the global chart_dir, the plot's title, any specified tag text, and the file_type for the plot.
Arguments:
- axes - matplotlib axes object - required
- kwargs: FinaliseKwargs
Returns: - None