mgplot 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mgplot/date_utils.py ADDED
@@ -0,0 +1,324 @@
1
+ """
2
+ date_utils.py
3
+ This module contains functions to work with date-like
4
+ (i.e. not time-like) PeriodIndex frequencies in Pandas.
5
+ It is used to label the x-axis of a plot with the
6
+ appropriate date-like labels.
7
+ """
8
+
9
+ import calendar
10
+ from enum import Enum
11
+ from pandas import Period, PeriodIndex, period_range
12
+ from matplotlib.pyplot import Axes
13
+
14
+
15
+ class DateLike(Enum):
16
+ """Recognised date-like PeriodIndex frequencies"""
17
+
18
+ YEARS = 1
19
+ QUARTERS = 2
20
+ MONTHS = 3
21
+ DAYS = 4
22
+ BAD = 5
23
+
24
+
25
+ frequencies = {
26
+ # freq: [Periods from smaller to larger]
27
+ "D": [DateLike.DAYS, DateLike.MONTHS, DateLike.YEARS],
28
+ "M": [DateLike.MONTHS, DateLike.YEARS],
29
+ "Q": [DateLike.QUARTERS, DateLike.YEARS],
30
+ "Y": [DateLike.YEARS],
31
+ }
32
+
33
+ r_freqs = {v[0]: k for k, v in frequencies.items()}
34
+
35
+ intervals = {
36
+ DateLike.YEARS: [1, 2, 4, 5, 10, 20, 40, 50, 100, 200, 400, 500, 1000],
37
+ DateLike.QUARTERS: [1, 2],
38
+ DateLike.MONTHS: [1, 2, 3, 4, 6],
39
+ DateLike.DAYS: [1, 2, 4, 7, 14],
40
+ }
41
+
42
+
43
+ def get_count(p: PeriodIndex, max_ticks: int) -> tuple[int, DateLike, int]:
44
+ """
45
+ Work out the label frequency and interval for a date-like
46
+ PeriodIndex.
47
+
48
+ Parameters
49
+ - p: PeriodIndex - the PeriodIndex
50
+ - max_ticks - the maximum number of ticks [suggestive]
51
+
52
+ Returns a tuple:
53
+ - the roughly anticipated number of ticks to highlight: int
54
+ - the type of ticks to highlight (eg. days/months/quarters/years): str
55
+ - the tick interval (ie. number of days/months/quarters/years): int
56
+ """
57
+
58
+ # --- sanity checks
59
+ error = (0, DateLike.BAD, 0)
60
+ if p.empty:
61
+ return error
62
+ freq: str = p.freqstr[0].upper()
63
+ if freq not in frequencies:
64
+ print("Unrecognised date-like PeriodIndex frequency {freq}")
65
+ return error
66
+
67
+ # --- calculate
68
+ for test_freq in frequencies[freq]:
69
+ r_freq = r_freqs[test_freq]
70
+ for interval in intervals[test_freq]:
71
+ count = (
72
+ p.max().asfreq(r_freq, how="end").ordinal
73
+ - p.min().asfreq(r_freq, how="end").ordinal
74
+ + 1
75
+ ) // interval
76
+ if count <= max_ticks:
77
+ return count, test_freq, interval
78
+ return error
79
+
80
+
81
+ def day_labeller(labels: dict[Period, str]) -> dict[Period, str]:
82
+ """Label the selected days."""
83
+
84
+ def add_month(label: str, month: str) -> str:
85
+ return f"{label}\n{month}"
86
+
87
+ def add_year(label: str, year: str) -> str:
88
+ label = label.replace("\n", " ") if len(label) > 2 else f"{label} {month}"
89
+ label = f"{label}\n{year}"
90
+ return label
91
+
92
+ if not labels:
93
+ return labels
94
+
95
+ start = min(labels.keys())
96
+ month_previous: str = calendar.month_abbr[start.month]
97
+ year_previous: str = str(start.year)
98
+ final_year = True
99
+
100
+ for period in sorted(labels.keys()):
101
+ label = str(period.day)
102
+ month = calendar.month_abbr[period.month]
103
+ year = str(period.year)
104
+
105
+ if month_previous != month:
106
+ label = add_month(label, month)
107
+
108
+ if year_previous != year:
109
+ final_year = False
110
+ label = add_year(label, year)
111
+
112
+ labels[period] = label
113
+
114
+ if final_year:
115
+ final_period = max(labels.keys())
116
+ labels[final_period] = add_year(label, year)
117
+
118
+ return labels
119
+
120
+
121
+ def month_locator(p: PeriodIndex, interval: int) -> dict[Period, str]:
122
+ """Select the months to label."""
123
+
124
+ subset = PeriodIndex([c for c in p if c.day == 1]) if p.freqstr[0] == "D" else p
125
+
126
+ start = 0
127
+ if interval > 1:
128
+ mod_months = [(c.month - 1) % interval for c in subset]
129
+ start = 0 if 0 not in mod_months else mod_months.index(0)
130
+ return {k: "" for k in subset[start::interval]}
131
+
132
+
133
+ def month_labeller(labels: dict[Period, str]) -> dict[Period, str]:
134
+ """Label the selected months."""
135
+
136
+ if not labels:
137
+ return labels
138
+
139
+ start = min(labels.keys())
140
+ year_previous: str = str(start.year)
141
+ final_year = True
142
+
143
+ for period in sorted(labels.keys()):
144
+ label = calendar.month_abbr[period.month]
145
+ year = str(period.year)
146
+
147
+ if year_previous != year or period.month == 1:
148
+ label = f"{label}\n{year}"
149
+ year_previous = year
150
+ final_year = False
151
+
152
+ labels[period] = label
153
+
154
+ if final_year:
155
+ final_period = max(labels.keys())
156
+ label = labels[final_period]
157
+ year = str(final_period.year)
158
+ label = f"{label}\n{year}"
159
+ labels[final_period] = label
160
+
161
+ return labels
162
+
163
+
164
+ def qtr_locator(p: PeriodIndex, interval: int) -> dict[Period, str]:
165
+ """Select the quarters to label."""
166
+
167
+ start = 0
168
+ if interval > 1:
169
+ mod_qtrs = [(c.quarter - 1) % interval for c in p]
170
+ start = 0 if 0 not in mod_qtrs else mod_qtrs.index(0)
171
+ return {k: "" for k in p[start::interval]}
172
+
173
+
174
+ def qtr_labeller(labels: dict[Period, str]) -> dict[Period, str]:
175
+ """Label the selected quarters."""
176
+
177
+ if not labels:
178
+ return labels
179
+
180
+ final_year = True
181
+ for period in sorted(labels.keys()):
182
+ quarter = period.quarter
183
+ label = f"Q{quarter}"
184
+ if quarter == 1:
185
+ final_year = False
186
+ label = f"{label}\n{period.year}"
187
+ labels[period] = label
188
+
189
+ if final_year:
190
+ final_period = max(labels.keys())
191
+ label = labels[final_period]
192
+ year = str(final_period.year)
193
+ label = f"{label}\n{year}"
194
+ labels[final_period] = label
195
+
196
+ return labels
197
+
198
+
199
+ def year_locator(p: PeriodIndex, interval: int) -> dict[Period, str]:
200
+ """Select the years to label."""
201
+
202
+ match p.freqstr[0]:
203
+ case "D":
204
+ subset = PeriodIndex([c for c in p if c.month == 1 and c.day == 1])
205
+ case "M":
206
+ subset = PeriodIndex([c for c in p if c.month == 1])
207
+ case "Q":
208
+ subset = PeriodIndex([c for c in p if c.quarter == 1])
209
+ case _:
210
+ subset = p
211
+
212
+ start = 0
213
+ if interval > 1:
214
+ mod_years = [(c.year) % interval for c in subset]
215
+ start = 0 if 0 not in mod_years else mod_years.index(0)
216
+ return {k: "" for k in subset[start::interval]}
217
+
218
+
219
+ def year_labeller(labels: dict[Period, str]) -> dict[Period, str]:
220
+ """Label the selected years."""
221
+
222
+ if not labels:
223
+ return labels
224
+
225
+ for period in sorted(labels.keys()):
226
+ label = str(period.year)
227
+ labels[period] = label
228
+ return labels
229
+
230
+
231
+ def make_labels(p: PeriodIndex, max_ticks: int) -> dict[Period, str]:
232
+ """
233
+ Provide a dictionary of labels for the date-like PeriodIndex.
234
+
235
+ Parameters
236
+ - p: PeriodIndex - the PeriodIndex
237
+ - max_ticks - the maximum number of ticks [suggestive]
238
+
239
+ Returns a dictionary:
240
+ - keys are the Periods to label
241
+ - values are the labels to apply
242
+ """
243
+
244
+ labels: dict[Period, str] = {}
245
+ max_ticks = max(max_ticks, 4)
246
+ count, date_like, interval = get_count(p, max_ticks)
247
+ if date_like == DateLike.BAD:
248
+ return labels
249
+
250
+ target_freq = r_freqs[date_like]
251
+ complete = period_range(start=p.min(), end=p.max(), freq=p.freqstr)
252
+
253
+ match target_freq:
254
+ case "D":
255
+ start = 0 if interval == 2 and count % 2 else interval // 2
256
+ labels = {k: "" for k in complete[start::interval]}
257
+ labels = day_labeller(labels)
258
+
259
+ case "M":
260
+ labels = month_locator(complete, interval)
261
+ labels = month_labeller(labels)
262
+
263
+ case "Q":
264
+ labels = qtr_locator(complete, interval)
265
+ labels = qtr_labeller(labels)
266
+
267
+ case "Y":
268
+ labels = year_locator(complete, interval)
269
+ labels = year_labeller(labels)
270
+
271
+ return labels
272
+
273
+
274
+ def make_ilabels(p: PeriodIndex, max_ticks: int) -> tuple[list[int], list[str]]:
275
+ """
276
+ From a PeriodIndex, create a list of integer ticks and ticklabels
277
+
278
+ Parameters
279
+ - p: PeriodIndex - the PeriodIndex
280
+ - max_ticks - the maximum number of ticks [suggestive]
281
+
282
+ Returns a tuple:
283
+ - list of integer ticks
284
+ - list of tick label strings
285
+ """
286
+
287
+ labels = make_labels(p, max_ticks)
288
+ base = p.min().ordinal
289
+ ticks = [x.ordinal - base for x in sorted(labels.keys())]
290
+ ticklabels = [labels[x] for x in sorted(labels.keys())]
291
+
292
+ return ticks, ticklabels
293
+
294
+
295
+ def set_labels(axes: Axes, p: PeriodIndex, max_ticks: int = 10) -> None:
296
+ """
297
+ Set the x-axis labels for a date-like PeriodIndex.
298
+
299
+ Parameters
300
+ - axes: Axes - the axes to set the labels on
301
+ - p: PeriodIndex - the PeriodIndex
302
+ - max_ticks: int - the maximum number of ticks [suggestive]
303
+ """
304
+
305
+ ticks, ticklabels = make_ilabels(p, max_ticks)
306
+ axes.set_xticks(ticks)
307
+ axes.set_xticklabels(ticklabels, rotation=0, ha="center")
308
+
309
+
310
+ # --- test ---
311
+ if __name__ == "__main__":
312
+
313
+ tests = [
314
+ PeriodIndex(["2020-01-01", "2020-01-02", "2020-01-03", "2020-01-04"], freq="D"),
315
+ period_range(start="2020-01-01", end="2020-01-15", freq="D"),
316
+ period_range(start="2020-02-01", end="2022-07-15", freq="D"),
317
+ period_range(start="2020-Q2", end="2022-Q4", freq="Q"),
318
+ period_range(start="2000-Q2", end="2022-Q4", freq="Q"),
319
+ period_range(start="1950-01-01", end="2026-12-15", freq="D"),
320
+ ]
321
+ for index, test in enumerate(tests):
322
+ print(f"Test {index + 1}")
323
+ print("Labels:", make_labels(test, 10), "\n")
324
+ print("========")
@@ -0,0 +1,335 @@
1
+ """
2
+ finalise_plot.py:
3
+ This module provides a function to finalise and save plots to the
4
+ file system. It is used to publish plots.
5
+ """
6
+
7
+ # --- imports
8
+ # from typing import Any
9
+ import re
10
+ import matplotlib as mpl
11
+ import matplotlib.pyplot as plt
12
+ from matplotlib.pyplot import Axes, Figure
13
+ import matplotlib.dates as mdates
14
+
15
+ from mgplot.settings import get_setting
16
+ from mgplot.kw_type_checking import (
17
+ report_kwargs,
18
+ validate_expected,
19
+ ExpectedTypeDict,
20
+ validate_kwargs,
21
+ )
22
+
23
+
24
+ # --- constants
25
+ # filename limitations - regex used to map the plot title to a filename
26
+ _remove = re.compile(r"[^0-9A-Za-z]") # sensible file names from alphamum title
27
+ _reduce = re.compile(r"[-]+") # eliminate multiple hyphens
28
+
29
+ # map of the acceptable kwargs for finalise_plot()
30
+ # make sure "legend" is last in the _splat_kwargs tuple ...
31
+ _splat_kwargs = ("axhspan", "axvspan", "axhline", "axvline", "legend")
32
+ _value_must_kwargs = ("title", "xlabel", "ylabel")
33
+ _value_may_kwargs = ("ylim", "xlim", "yscale", "xscale")
34
+ _value_kwargs = _value_must_kwargs + _value_may_kwargs
35
+ _annotation_kwargs = ("lfooter", "rfooter", "lheader", "rheader")
36
+
37
+ _file_kwargs = ("pre_tag", "tag", "chart_dir", "file_type", "dpi")
38
+ _fig_kwargs = ("figsize", "show", "preserve_lims", "remove_legend")
39
+ _oth_kwargs = (
40
+ "zero_y",
41
+ "y0",
42
+ "x0",
43
+ "dont_save",
44
+ "dont_close",
45
+ "concise_dates",
46
+ )
47
+ _ACCEPTABLE_KWARGS = frozenset(
48
+ _value_kwargs
49
+ + _splat_kwargs
50
+ + _file_kwargs
51
+ + _annotation_kwargs
52
+ + _fig_kwargs
53
+ + _oth_kwargs
54
+ )
55
+
56
+ FINALISE_KW_TYPES: ExpectedTypeDict = {
57
+ # - value kwargs
58
+ "title": (str, type(None)),
59
+ "xlabel": (str, type(None)),
60
+ "ylabel": (str, type(None)),
61
+ "ylim": (tuple, (float, int), type(None)),
62
+ "xlim": (tuple, (float, int), type(None)),
63
+ "yscale": (str, type(None)),
64
+ "xscale": (str, type(None)),
65
+ # - splat kwargs
66
+ "legend": (dict, (str, (int, float, str)), bool, type(None)),
67
+ "axhspan": (dict, (str, (int, float, str)), type(None)),
68
+ "axvspan": (dict, (str, (int, float, str)), type(None)),
69
+ "axhline": (dict, (str, (int, float, str)), type(None)),
70
+ "axvline": (dict, (str, (int, float, str)), type(None)),
71
+ # - file kwargs
72
+ "pre_tag": str,
73
+ "tag": str,
74
+ "chart_dir": str,
75
+ "file_type": str,
76
+ "dpi": int,
77
+ # - fig kwargs
78
+ "remove_legend": (type(None), bool),
79
+ "preserve_lims": (type(None), bool),
80
+ "figsize": (tuple, (float, int)),
81
+ "show": bool,
82
+ # - annotation kwargs
83
+ "lfooter": str,
84
+ "rfooter": str,
85
+ "lheader": str,
86
+ "rheader": str,
87
+ # - Other kwargs
88
+ "zero_y": bool,
89
+ "y0": bool,
90
+ "x0": bool,
91
+ "dont_save": bool,
92
+ "dont_close": bool,
93
+ "concise_dates": bool,
94
+ }
95
+ validate_expected(FINALISE_KW_TYPES, "finalise_plot")
96
+
97
+
98
+ def _internal_consistency_kwargs():
99
+ """Quick check to ensure that the kwargs checkers are consistent."""
100
+
101
+ bad = False
102
+ for k in FINALISE_KW_TYPES:
103
+ if k not in _ACCEPTABLE_KWARGS:
104
+ bad = True
105
+ print(f"Key {k} in FINALISE_KW_TYPES but not _ACCEPTABLE_KWARGS")
106
+
107
+ for k in _ACCEPTABLE_KWARGS:
108
+ if k not in FINALISE_KW_TYPES:
109
+ bad = True
110
+ print(f"Key {k} in _ACCEPTABLE_KWARGS but not FINALISE_KW_TYPES")
111
+
112
+ if bad:
113
+ raise RuntimeError(
114
+ "Internal error: _ACCEPTABLE_KWARGS and FINALISE_KW_TYPES are inconsistent."
115
+ )
116
+
117
+
118
+ _internal_consistency_kwargs()
119
+
120
+
121
+ # - private utility functions for finalise_plot()
122
+
123
+
124
+ def _apply_value_kwargs(axes: Axes, settings: tuple, **kwargs) -> None:
125
+ """Set matplotlib elements by name using Axes.set()."""
126
+
127
+ for setting in settings:
128
+ value = kwargs.get(setting, None)
129
+ if value is None and setting not in _value_must_kwargs:
130
+ continue
131
+ axes.set(**{setting: value})
132
+
133
+
134
+ def _apply_splat_kwargs(axes: Axes, settings: tuple, **kwargs) -> None:
135
+ """
136
+ Set matplotlib elements dynamically using setting_name and splat.
137
+ This is used for legend, axhspan, axvspan, axhline, and axvline.
138
+ These can be ignored if not in kwargs, or set to None in kwargs.
139
+ """
140
+
141
+ for method_name in settings:
142
+ if method_name in kwargs:
143
+
144
+ if kwargs[method_name] is None or kwargs[method_name] is False:
145
+ continue
146
+
147
+ if kwargs[method_name] is True: # use the global default settings
148
+ kwargs[method_name] = get_setting(method_name)
149
+
150
+ # splat the kwargs to the method
151
+ if isinstance(kwargs[method_name], dict):
152
+ method = getattr(axes, method_name)
153
+ method(**kwargs[method_name])
154
+ else:
155
+ print(
156
+ f"Warning expected dict argument: {method_name} but got "
157
+ + f"{type(kwargs[method_name])}."
158
+ )
159
+
160
+
161
+ def _apply_annotations(axes: Axes, **kwargs) -> None:
162
+ """Set figure size and apply chart annotations."""
163
+
164
+ fig = axes.figure
165
+ fig_size = get_setting("figsize") if "figsize" not in kwargs else kwargs["figsize"]
166
+ if not isinstance(fig, mpl.figure.SubFigure):
167
+ fig.set_size_inches(*fig_size)
168
+
169
+ annotations = {
170
+ "rfooter": (0.99, 0.001, "right", "bottom"),
171
+ "lfooter": (0.01, 0.001, "left", "bottom"),
172
+ "rheader": (0.99, 0.999, "right", "top"),
173
+ "lheader": (0.01, 0.999, "left", "top"),
174
+ }
175
+
176
+ for annotation in _annotation_kwargs:
177
+ if annotation in kwargs:
178
+ x_pos, y_pos, h_align, v_align = annotations[annotation]
179
+ fig.text(
180
+ x_pos,
181
+ y_pos,
182
+ kwargs[annotation],
183
+ ha=h_align,
184
+ va=v_align,
185
+ fontsize=8,
186
+ fontstyle="italic",
187
+ color="#999999",
188
+ )
189
+
190
+
191
+ def _apply_late_kwargs(axes: Axes, **kwargs) -> None:
192
+ """Apply settings found in kwargs, after plotting the data."""
193
+ _apply_splat_kwargs(axes, _splat_kwargs, **kwargs)
194
+
195
+
196
+ def _apply_kwargs(axes: Axes, **kwargs) -> None:
197
+ """Apply settings found in kwargs."""
198
+
199
+ def check_kwargs(name):
200
+ return name in kwargs and kwargs[name]
201
+
202
+ _apply_value_kwargs(axes, _value_kwargs, **kwargs)
203
+ _apply_annotations(axes, **kwargs)
204
+
205
+ if check_kwargs("zero_y"):
206
+ bottom, top = axes.get_ylim()
207
+ adj = (top - bottom) * 0.02
208
+ if bottom > -adj:
209
+ axes.set_ylim(bottom=-adj)
210
+ if top < adj:
211
+ axes.set_ylim(top=adj)
212
+
213
+ if check_kwargs("y0"):
214
+ low, high = axes.get_ylim()
215
+ if low < 0 < high:
216
+ axes.axhline(y=0, lw=0.66, c="#555555")
217
+
218
+ if check_kwargs("x0"):
219
+ low, high = axes.get_xlim()
220
+ if low < 0 < high:
221
+ axes.axvline(x=0, lw=0.66, c="#555555")
222
+
223
+ if check_kwargs("concise_dates"):
224
+ locator = mdates.AutoDateLocator()
225
+ formatter = mdates.ConciseDateFormatter(locator)
226
+ axes.xaxis.set_major_locator(locator)
227
+ axes.xaxis.set_major_formatter(formatter)
228
+
229
+
230
+ def _save_to_file(fig: Figure, **kwargs) -> None:
231
+ """Save the figure to file."""
232
+
233
+ saving = not kwargs.get("dont_save", False) # save by default
234
+ if saving:
235
+ chart_dir = kwargs.get("chart_dir", get_setting("chart_dir"))
236
+ if not chart_dir.endswith("/"):
237
+ chart_dir += "/"
238
+
239
+ title = "" if "title" not in kwargs else kwargs["title"]
240
+ max_title_len = 150 # avoid overly long file names
241
+ shorter = title if len(title) < max_title_len else title[:max_title_len]
242
+ pre_tag = kwargs.get("pre_tag", "")
243
+ tag = kwargs.get("tag", "")
244
+ file_title = re.sub(_remove, "-", shorter).lower()
245
+ file_title = re.sub(_reduce, "-", file_title)
246
+ file_type = kwargs.get("file_type", get_setting("file_type")).lower()
247
+ dpi = kwargs.get("dpi", get_setting("file_dpi"))
248
+ fig.savefig(f"{chart_dir}{pre_tag}{file_title}-{tag}.{file_type}", dpi=dpi)
249
+
250
+
251
+ # - public functions for finalise_plot()
252
+
253
+
254
+ def finalise_plot(axes: Axes, **kwargs) -> None:
255
+ """
256
+ A function to finalise and save plots to the file system. The filename
257
+ for the saved plot is constructed from the global chart_dir, the plot's title,
258
+ any specified tag text, and the file_type for the plot.
259
+
260
+ Arguments:
261
+ - axes - matplotlib axes object - required
262
+ - kwargs
263
+ - title: str - plot title, also used to create the save file name
264
+ - xlabel: str | None - text label for the x-axis
265
+ - ylabel: str | None - label for the y-axis
266
+ - pre_tag: str - text before the title in file name
267
+ - tag: str - text after the title in the file name
268
+ (useful for ensuring that same titled charts do not over-write)
269
+ - chart_dir: str - location of the chart directory
270
+ - file_type: str - specify a file type - eg. 'png' or 'svg'
271
+ - lfooter: str - text to display on bottom left of plot
272
+ - rfooter: str - text to display of bottom right of plot
273
+ - lheader: str - text to display on top left of plot
274
+ - rheader: str - text to display of top right of plot
275
+ - figsize: tuple[float, float] - figure size in inches - eg. (8, 4)
276
+ - show: bool - whether to show the plot or not
277
+ - zero_y: bool - ensure y=0 is included in the plot.
278
+ - y0: bool - highlight the y=0 line on the plot (if in scope)
279
+ - x0: bool - highlights the x=0 line on the plot
280
+ - dont_save: bool - dont save the plot to the file system
281
+ - dont_close: bool - dont close the plot
282
+ - dpi: int - dots per inch for the saved chart
283
+ - legend: bool | dict - if dict, use as the arguments to pass to axes.legend(),
284
+ if True pass the global default arguments to axes.legend()
285
+ - axhspan: dict - arguments to pass to axes.axhspan()
286
+ - axvspan: dict - arguments to pass to axes.axvspan()
287
+ - axhline: dict - arguments to pass to axes.axhline()
288
+ - axvline: dict - arguments to pass to axes.axvline()
289
+ - ylim: tuple[float, float] - set lower and upper y-axis limits
290
+ - xlim: tuple[float, float] - set lower and upper x-axis limits
291
+ - preserve_lims: bool - if True, preserve the original axes limits,
292
+ lims saved at the start, and restored after the tight layout
293
+ - remove_legend: bool | None - if True, remove the legend from the plot
294
+ - report_kwargs: bool - if True, report the kwargs used in this function
295
+
296
+ Returns:
297
+ - None
298
+ """
299
+
300
+ # --- sanity checks
301
+ report_kwargs(called_from="finalise_plot", **kwargs)
302
+ validate_kwargs(FINALISE_KW_TYPES, "finalise_plot", **kwargs)
303
+
304
+ # --- remember should we need to restore the axes limits
305
+ xlim, ylim = axes.get_xlim(), axes.get_ylim()
306
+
307
+ # margins
308
+ # axes.use_sticky_margins = False ### CHECK THIS
309
+ axes.margins(0.02)
310
+ axes.autoscale(tight=False) # This is problematic ...
311
+
312
+ _apply_kwargs(axes, **kwargs)
313
+
314
+ # tight layout and save the figure
315
+ fig = axes.figure
316
+ if not isinstance(fig, mpl.figure.SubFigure): # should never be a SubFigure
317
+ fig.tight_layout(pad=1.1)
318
+ if "preserve_lims" in kwargs and kwargs["preserve_lims"]:
319
+ # restore the original limits of the axes
320
+ axes.set_xlim(xlim)
321
+ axes.set_ylim(ylim)
322
+ _apply_late_kwargs(axes, **kwargs)
323
+ legend = axes.get_legend()
324
+ if legend and kwargs.get("remove_legend", False):
325
+ legend.remove()
326
+ _save_to_file(fig, **kwargs)
327
+
328
+ # show the plot in Jupyter Lab
329
+ if "show" in kwargs and kwargs["show"]:
330
+ plt.show()
331
+
332
+ # And close
333
+ closing = True if "dont_close" not in kwargs else not kwargs["dont_close"]
334
+ if closing:
335
+ plt.close()