mgplot 0.2.20__tar.gz → 0.2.21__tar.gz
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-0.2.20 → mgplot-0.2.21}/CHANGELOG.md +11 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/PKG-INFO +1 -1
- {mgplot-0.2.20 → mgplot-0.2.21}/docs/mgplot.html +1 -1
- {mgplot-0.2.20 → mgplot-0.2.21}/pyproject.toml +1 -1
- {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/finalise_plot.py +44 -31
- mgplot-0.2.21/test/test_splat_sequences.py +118 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/uv.lock +227 -215
- {mgplot-0.2.20 → mgplot-0.2.21}/.gitignore +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/.pylintrc +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/LICENSE +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/README.md +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/build-all.sh +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/build-docs.sh +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/docs/index.html +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/docs/mgplot/bar_plot.html +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/docs/mgplot/fill_between_plot.html +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/docs/mgplot/finalise_plot.html +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/docs/mgplot/growth_plot.html +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/docs/mgplot/line_plot.html +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/docs/mgplot/postcovid_plot.html +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/docs/mgplot/revision_plot.html +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/docs/mgplot/run_plot.html +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/docs/mgplot/seastrend_plot.html +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/docs/mgplot/summary_plot.html +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/docs/search.js +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/__init__.py +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/axis_utils.py +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/bar_plot.py +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/colors.py +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/fill_between_plot.py +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/finalisers.py +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/growth_plot.py +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/keyword_checking.py +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/line_plot.py +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/lint-all.sh +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/multi_plot.py +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/postcovid_plot.py +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/py.typed +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/revision_plot.py +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/run_plot.py +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/seastrend_plot.py +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/settings.py +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/summary_plot.py +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/utilities.py +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/test/test-executed.ipynb +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/test/test.ipynb +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/test/test_bar_string_index.py +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/test/test_multi_series_ticks.py +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/test/test_zorder.py +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/test/zz-test-data/ocr_rba.csv +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/test/zz-test-data/revisions.csv +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/test/zz-test-data/summary.csv +0 -0
- {mgplot-0.2.20 → mgplot-0.2.21}/uv-upgrade.sh +0 -0
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
Version 0.2.21 - released 17-Mar-2026 (Canberra, Australia)
|
|
2
|
+
|
|
3
|
+
* enhancement
|
|
4
|
+
- axhline, axvline, axhspan, axvspan in finalise_plot() now accept
|
|
5
|
+
a sequence of dicts to draw multiple lines/spans in a single call,
|
|
6
|
+
in addition to the existing single dict usage
|
|
7
|
+
- added test/test_splat_sequences.py
|
|
8
|
+
- fixed FURB110 ruff warnings (ternary if replaced with or operator)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
1
12
|
Version 0.2.20 - released 25-Feb-2026 (Canberra, Australia)
|
|
2
13
|
|
|
3
14
|
* bug fix
|
|
@@ -2340,7 +2340,7 @@ for color management and finalising plots with consistent styling.</p>
|
|
|
2340
2340
|
<section id="__version__">
|
|
2341
2341
|
<div class="attr variable">
|
|
2342
2342
|
<span class="name">__version__</span> =
|
|
2343
|
-
<span class="default_value">'0.2.
|
|
2343
|
+
<span class="default_value">'0.2.20'</span>
|
|
2344
2344
|
|
|
2345
2345
|
|
|
2346
2346
|
</div>
|
|
@@ -43,10 +43,10 @@ class FinaliseKwargs(BaseKwargs):
|
|
|
43
43
|
yscale: NotRequired[str | None]
|
|
44
44
|
# --- splat options
|
|
45
45
|
legend: NotRequired[bool | dict[str, Any] | None]
|
|
46
|
-
axhspan: NotRequired[dict[str, Any] | None]
|
|
47
|
-
axvspan: NotRequired[dict[str, Any] | None]
|
|
48
|
-
axhline: NotRequired[dict[str, Any] | None]
|
|
49
|
-
axvline: NotRequired[dict[str, Any] | None]
|
|
46
|
+
axhspan: NotRequired[dict[str, Any] | Sequence[dict[str, Any]] | None]
|
|
47
|
+
axvspan: NotRequired[dict[str, Any] | Sequence[dict[str, Any]] | None]
|
|
48
|
+
axhline: NotRequired[dict[str, Any] | Sequence[dict[str, Any]] | None]
|
|
49
|
+
axvline: NotRequired[dict[str, Any] | Sequence[dict[str, Any]] | None]
|
|
50
50
|
# --- options for annotations
|
|
51
51
|
lfooter: NotRequired[str]
|
|
52
52
|
rfooter: NotRequired[str]
|
|
@@ -135,7 +135,7 @@ def sanitize_filename(filename: str, max_length: int = MAX_FILENAME_LENGTH) -> s
|
|
|
135
135
|
filename = filename[:max_length].rstrip("-")
|
|
136
136
|
|
|
137
137
|
# Ensure we have a valid filename
|
|
138
|
-
return filename
|
|
138
|
+
return filename or "untitled"
|
|
139
139
|
|
|
140
140
|
|
|
141
141
|
def make_legend(axes: Axes, *, legend: None | bool | dict[str, Any]) -> None:
|
|
@@ -194,34 +194,47 @@ def apply_value_kwargs(axes: Axes, value_kwargs_: Sequence[str], **kwargs: Unpac
|
|
|
194
194
|
axes.set(**{setting: value})
|
|
195
195
|
|
|
196
196
|
|
|
197
|
+
_SplatValue = bool | dict[str, Any] | Sequence[dict[str, Any]] | None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _apply_splat(axes: Axes, method_name: str, value: _SplatValue) -> None:
|
|
201
|
+
"""Apply a single splat kwarg, which may be a dict or sequence of dicts."""
|
|
202
|
+
if value is None or value is False:
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
if value is True: # use the global default settings
|
|
206
|
+
value = get_setting(method_name)
|
|
207
|
+
|
|
208
|
+
# normalise to a list of dicts
|
|
209
|
+
if isinstance(value, dict):
|
|
210
|
+
value = [value]
|
|
211
|
+
|
|
212
|
+
if isinstance(value, Sequence):
|
|
213
|
+
method = getattr(axes, method_name)
|
|
214
|
+
for item in value:
|
|
215
|
+
if isinstance(item, dict):
|
|
216
|
+
method(**item)
|
|
217
|
+
else:
|
|
218
|
+
print(f"Warning: expected dict in {method_name} sequence, but got {type(item)}.")
|
|
219
|
+
else:
|
|
220
|
+
print(f"Warning: expected dict or sequence of dicts for {method_name}, but got {type(value)}.")
|
|
221
|
+
|
|
222
|
+
|
|
197
223
|
def apply_splat_kwargs(axes: Axes, settings: tuple, **kwargs: Unpack[FinaliseKwargs]) -> None:
|
|
198
224
|
"""Set matplotlib elements dynamically using setting_name and splat."""
|
|
199
225
|
for method_name in settings:
|
|
200
|
-
if method_name in kwargs:
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
print(f"Warning: expected bool, dict, or None for legend, but got {type(legend_value)}.")
|
|
208
|
-
continue
|
|
209
|
-
|
|
210
|
-
value = kwargs.get(method_name)
|
|
211
|
-
if value is None or value is False:
|
|
212
|
-
continue
|
|
213
|
-
|
|
214
|
-
if value is True: # use the global default settings
|
|
215
|
-
value = get_setting(method_name)
|
|
216
|
-
|
|
217
|
-
# splat the kwargs to the method
|
|
218
|
-
if isinstance(value, dict):
|
|
219
|
-
method = getattr(axes, method_name)
|
|
220
|
-
method(**value)
|
|
226
|
+
if method_name not in kwargs:
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
if method_name == "legend":
|
|
230
|
+
legend_value = kwargs.get(method_name)
|
|
231
|
+
if isinstance(legend_value, (bool, dict, type(None))):
|
|
232
|
+
make_legend(axes, legend=legend_value)
|
|
221
233
|
else:
|
|
222
|
-
print(
|
|
223
|
-
|
|
224
|
-
|
|
234
|
+
print(f"Warning: expected bool, dict, or None for legend, but got {type(legend_value)}.")
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
_apply_splat(axes, method_name, kwargs.get(method_name))
|
|
225
238
|
|
|
226
239
|
|
|
227
240
|
def apply_annotations(axes: Axes, **kwargs: Unpack[FinaliseKwargs]) -> None:
|
|
@@ -305,8 +318,8 @@ def save_to_file(fig: Figure, **kwargs: Unpack[FinaliseKwargs]) -> None:
|
|
|
305
318
|
title = kwargs.get("title", "")
|
|
306
319
|
pre_tag = kwargs.get("pre_tag", "")
|
|
307
320
|
tag = kwargs.get("tag", "")
|
|
308
|
-
name_title = suptitle
|
|
309
|
-
file_title = sanitize_filename(name_title
|
|
321
|
+
name_title = suptitle or title
|
|
322
|
+
file_title = sanitize_filename(name_title or DEFAULT_FILE_TITLE_NAME)
|
|
310
323
|
file_type = kwargs.get("file_type", get_setting("file_type")).lower()
|
|
311
324
|
dpi = kwargs.get("dpi", get_setting("dpi"))
|
|
312
325
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Test that axhline, axvline, axhspan, axvspan accept single dicts and sequences.
|
|
2
|
+
|
|
3
|
+
Run with: uv run python test/test_splat_sequences.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from mgplot import line_plot
|
|
10
|
+
from mgplot.finalise_plot import finalise_plot
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _make_series() -> pd.Series:
|
|
14
|
+
return pd.Series(range(1, 11), index=pd.period_range("2020-01", periods=10, freq="M"))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_axhline_single_dict() -> None:
|
|
18
|
+
"""A single dict should draw one horizontal line."""
|
|
19
|
+
ax = line_plot(_make_series())
|
|
20
|
+
finalise_plot(ax, axhline={"y": 5, "color": "red"}, dont_save=True, dont_close=True)
|
|
21
|
+
|
|
22
|
+
hlines = [l for l in ax.get_lines() if len(l.get_xdata()) == 2 and l.get_ydata()[0] == 5]
|
|
23
|
+
assert len(hlines) == 1, f"Expected 1 hline at y=5, found {len(hlines)}"
|
|
24
|
+
|
|
25
|
+
plt.close()
|
|
26
|
+
print("PASS: axhline single dict")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_axhline_sequence() -> None:
|
|
30
|
+
"""A sequence of dicts should draw multiple horizontal lines."""
|
|
31
|
+
ax = line_plot(_make_series())
|
|
32
|
+
finalise_plot(
|
|
33
|
+
ax,
|
|
34
|
+
axhline=[{"y": 3, "color": "red"}, {"y": 7, "color": "blue"}],
|
|
35
|
+
dont_save=True,
|
|
36
|
+
dont_close=True,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
ys = {l.get_ydata()[0] for l in ax.get_lines() if len(l.get_xdata()) == 2}
|
|
40
|
+
assert 3 in ys, "Expected hline at y=3"
|
|
41
|
+
assert 7 in ys, "Expected hline at y=7"
|
|
42
|
+
|
|
43
|
+
plt.close()
|
|
44
|
+
print("PASS: axhline sequence")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_axvline_sequence() -> None:
|
|
48
|
+
"""A sequence of dicts should draw multiple vertical lines."""
|
|
49
|
+
ax = line_plot(_make_series())
|
|
50
|
+
finalise_plot(
|
|
51
|
+
ax,
|
|
52
|
+
axvline=[{"x": 2, "color": "red"}, {"x": 8, "color": "blue"}],
|
|
53
|
+
dont_save=True,
|
|
54
|
+
dont_close=True,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
xs = {l.get_xdata()[0] for l in ax.get_lines() if len(l.get_ydata()) == 2}
|
|
58
|
+
assert 2 in xs, "Expected vline at x=2"
|
|
59
|
+
assert 8 in xs, "Expected vline at x=8"
|
|
60
|
+
|
|
61
|
+
plt.close()
|
|
62
|
+
print("PASS: axvline sequence")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_axhspan_single_dict() -> None:
|
|
66
|
+
"""A single dict should draw one horizontal span."""
|
|
67
|
+
ax = line_plot(_make_series())
|
|
68
|
+
n_patches_before = len(ax.patches)
|
|
69
|
+
finalise_plot(ax, axhspan={"ymin": 2, "ymax": 4, "alpha": 0.3}, dont_save=True, dont_close=True)
|
|
70
|
+
|
|
71
|
+
assert len(ax.patches) == n_patches_before + 1, "Expected 1 new patch from axhspan"
|
|
72
|
+
|
|
73
|
+
plt.close()
|
|
74
|
+
print("PASS: axhspan single dict")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_axhspan_sequence() -> None:
|
|
78
|
+
"""A sequence of dicts should draw multiple horizontal spans."""
|
|
79
|
+
ax = line_plot(_make_series())
|
|
80
|
+
n_patches_before = len(ax.patches)
|
|
81
|
+
finalise_plot(
|
|
82
|
+
ax,
|
|
83
|
+
axhspan=[{"ymin": 1, "ymax": 3, "alpha": 0.2}, {"ymin": 6, "ymax": 8, "alpha": 0.2}],
|
|
84
|
+
dont_save=True,
|
|
85
|
+
dont_close=True,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
assert len(ax.patches) == n_patches_before + 2, "Expected 2 new patches from axhspan sequence"
|
|
89
|
+
|
|
90
|
+
plt.close()
|
|
91
|
+
print("PASS: axhspan sequence")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_axvspan_sequence() -> None:
|
|
95
|
+
"""A sequence of dicts should draw multiple vertical spans."""
|
|
96
|
+
ax = line_plot(_make_series())
|
|
97
|
+
n_patches_before = len(ax.patches)
|
|
98
|
+
finalise_plot(
|
|
99
|
+
ax,
|
|
100
|
+
axvspan=[{"xmin": 1, "xmax": 3}, {"xmin": 6, "xmax": 8}],
|
|
101
|
+
dont_save=True,
|
|
102
|
+
dont_close=True,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
assert len(ax.patches) == n_patches_before + 2, "Expected 2 new patches from axvspan sequence"
|
|
106
|
+
|
|
107
|
+
plt.close()
|
|
108
|
+
print("PASS: axvspan sequence")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
test_axhline_single_dict()
|
|
113
|
+
test_axhline_sequence()
|
|
114
|
+
test_axvline_sequence()
|
|
115
|
+
test_axhspan_single_dict()
|
|
116
|
+
test_axhspan_sequence()
|
|
117
|
+
test_axvspan_sequence()
|
|
118
|
+
print("\nAll splat sequence tests passed!")
|