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.
Files changed (53) hide show
  1. {mgplot-0.2.20 → mgplot-0.2.21}/CHANGELOG.md +11 -0
  2. {mgplot-0.2.20 → mgplot-0.2.21}/PKG-INFO +1 -1
  3. {mgplot-0.2.20 → mgplot-0.2.21}/docs/mgplot.html +1 -1
  4. {mgplot-0.2.20 → mgplot-0.2.21}/pyproject.toml +1 -1
  5. {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/finalise_plot.py +44 -31
  6. mgplot-0.2.21/test/test_splat_sequences.py +118 -0
  7. {mgplot-0.2.20 → mgplot-0.2.21}/uv.lock +227 -215
  8. {mgplot-0.2.20 → mgplot-0.2.21}/.gitignore +0 -0
  9. {mgplot-0.2.20 → mgplot-0.2.21}/.pylintrc +0 -0
  10. {mgplot-0.2.20 → mgplot-0.2.21}/LICENSE +0 -0
  11. {mgplot-0.2.20 → mgplot-0.2.21}/README.md +0 -0
  12. {mgplot-0.2.20 → mgplot-0.2.21}/build-all.sh +0 -0
  13. {mgplot-0.2.20 → mgplot-0.2.21}/build-docs.sh +0 -0
  14. {mgplot-0.2.20 → mgplot-0.2.21}/docs/index.html +0 -0
  15. {mgplot-0.2.20 → mgplot-0.2.21}/docs/mgplot/bar_plot.html +0 -0
  16. {mgplot-0.2.20 → mgplot-0.2.21}/docs/mgplot/fill_between_plot.html +0 -0
  17. {mgplot-0.2.20 → mgplot-0.2.21}/docs/mgplot/finalise_plot.html +0 -0
  18. {mgplot-0.2.20 → mgplot-0.2.21}/docs/mgplot/growth_plot.html +0 -0
  19. {mgplot-0.2.20 → mgplot-0.2.21}/docs/mgplot/line_plot.html +0 -0
  20. {mgplot-0.2.20 → mgplot-0.2.21}/docs/mgplot/postcovid_plot.html +0 -0
  21. {mgplot-0.2.20 → mgplot-0.2.21}/docs/mgplot/revision_plot.html +0 -0
  22. {mgplot-0.2.20 → mgplot-0.2.21}/docs/mgplot/run_plot.html +0 -0
  23. {mgplot-0.2.20 → mgplot-0.2.21}/docs/mgplot/seastrend_plot.html +0 -0
  24. {mgplot-0.2.20 → mgplot-0.2.21}/docs/mgplot/summary_plot.html +0 -0
  25. {mgplot-0.2.20 → mgplot-0.2.21}/docs/search.js +0 -0
  26. {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/__init__.py +0 -0
  27. {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/axis_utils.py +0 -0
  28. {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/bar_plot.py +0 -0
  29. {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/colors.py +0 -0
  30. {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/fill_between_plot.py +0 -0
  31. {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/finalisers.py +0 -0
  32. {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/growth_plot.py +0 -0
  33. {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/keyword_checking.py +0 -0
  34. {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/line_plot.py +0 -0
  35. {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/lint-all.sh +0 -0
  36. {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/multi_plot.py +0 -0
  37. {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/postcovid_plot.py +0 -0
  38. {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/py.typed +0 -0
  39. {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/revision_plot.py +0 -0
  40. {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/run_plot.py +0 -0
  41. {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/seastrend_plot.py +0 -0
  42. {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/settings.py +0 -0
  43. {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/summary_plot.py +0 -0
  44. {mgplot-0.2.20 → mgplot-0.2.21}/src/mgplot/utilities.py +0 -0
  45. {mgplot-0.2.20 → mgplot-0.2.21}/test/test-executed.ipynb +0 -0
  46. {mgplot-0.2.20 → mgplot-0.2.21}/test/test.ipynb +0 -0
  47. {mgplot-0.2.20 → mgplot-0.2.21}/test/test_bar_string_index.py +0 -0
  48. {mgplot-0.2.20 → mgplot-0.2.21}/test/test_multi_series_ticks.py +0 -0
  49. {mgplot-0.2.20 → mgplot-0.2.21}/test/test_zorder.py +0 -0
  50. {mgplot-0.2.20 → mgplot-0.2.21}/test/zz-test-data/ocr_rba.csv +0 -0
  51. {mgplot-0.2.20 → mgplot-0.2.21}/test/zz-test-data/revisions.csv +0 -0
  52. {mgplot-0.2.20 → mgplot-0.2.21}/test/zz-test-data/summary.csv +0 -0
  53. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mgplot
3
- Version: 0.2.20
3
+ Version: 0.2.21
4
4
  Summary: mgplot is a time-series/PeriodIndex frontend for matplotlib
5
5
  Project-URL: Repository, https://github.com/bpalmer4/mgplot
6
6
  Project-URL: Homepage, https://github.com/bpalmer4/mgplot
@@ -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">&#39;0.2.19&#39;</span>
2343
+ <span class="default_value">&#39;0.2.20&#39;</span>
2344
2344
 
2345
2345
 
2346
2346
  </div>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mgplot"
3
- version = "0.2.20"
3
+ version = "0.2.21"
4
4
  description = "mgplot is a time-series/PeriodIndex frontend for matplotlib"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -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 if filename else "untitled"
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
- if method_name == "legend":
202
- # special case for legend
203
- legend_value = kwargs.get(method_name)
204
- if isinstance(legend_value, (bool, dict, type(None))):
205
- make_legend(axes, legend=legend_value)
206
- else:
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
- f"Warning expected dict argument for {method_name} but got {type(value)}.",
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 if suptitle else title
309
- file_title = sanitize_filename(name_title if name_title else DEFAULT_FILE_TITLE_NAME)
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!")