mgplot 0.1.1__tar.gz → 0.1.3__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 (29) hide show
  1. {mgplot-0.1.1/src/mgplot.egg-info → mgplot-0.1.3}/PKG-INFO +4 -3
  2. {mgplot-0.1.1 → mgplot-0.1.3}/README.md +2 -2
  3. {mgplot-0.1.1 → mgplot-0.1.3}/pyproject.toml +3 -2
  4. {mgplot-0.1.1 → mgplot-0.1.3}/src/mgplot/__init__.py +3 -9
  5. {mgplot-0.1.1 → mgplot-0.1.3}/src/mgplot/bar_plot.py +13 -4
  6. {mgplot-0.1.1 → mgplot-0.1.3}/src/mgplot/finalise_plot.py +34 -4
  7. {mgplot-0.1.1 → mgplot-0.1.3}/src/mgplot/finalisers.py +13 -39
  8. {mgplot-0.1.1 → mgplot-0.1.3}/src/mgplot/growth_plot.py +30 -12
  9. {mgplot-0.1.1 → mgplot-0.1.3}/src/mgplot/line_plot.py +17 -15
  10. {mgplot-0.1.1 → mgplot-0.1.3}/src/mgplot/multi_plot.py +36 -15
  11. {mgplot-0.1.1 → mgplot-0.1.3}/src/mgplot/postcovid_plot.py +24 -15
  12. {mgplot-0.1.1 → mgplot-0.1.3}/src/mgplot/revision_plot.py +7 -5
  13. {mgplot-0.1.1 → mgplot-0.1.3}/src/mgplot/run_plot.py +21 -12
  14. {mgplot-0.1.1 → mgplot-0.1.3}/src/mgplot/seastrend_plot.py +8 -5
  15. {mgplot-0.1.1 → mgplot-0.1.3}/src/mgplot/summary_plot.py +21 -6
  16. {mgplot-0.1.1 → mgplot-0.1.3}/src/mgplot/utilities.py +7 -4
  17. {mgplot-0.1.1 → mgplot-0.1.3/src/mgplot.egg-info}/PKG-INFO +4 -3
  18. {mgplot-0.1.1 → mgplot-0.1.3}/src/mgplot.egg-info/requires.txt +1 -0
  19. {mgplot-0.1.1 → mgplot-0.1.3}/LICENSE +0 -0
  20. {mgplot-0.1.1 → mgplot-0.1.3}/setup.cfg +0 -0
  21. {mgplot-0.1.1 → mgplot-0.1.3}/src/mgplot/colors.py +0 -0
  22. {mgplot-0.1.1 → mgplot-0.1.3}/src/mgplot/date_utils.py +0 -0
  23. {mgplot-0.1.1 → mgplot-0.1.3}/src/mgplot/kw_type_checking.py +0 -0
  24. {mgplot-0.1.1 → mgplot-0.1.3}/src/mgplot/py.typed +0 -0
  25. {mgplot-0.1.1 → mgplot-0.1.3}/src/mgplot/settings.py +0 -0
  26. {mgplot-0.1.1 → mgplot-0.1.3}/src/mgplot/test.py +0 -0
  27. {mgplot-0.1.1 → mgplot-0.1.3}/src/mgplot.egg-info/SOURCES.txt +0 -0
  28. {mgplot-0.1.1 → mgplot-0.1.3}/src/mgplot.egg-info/dependency_links.txt +0 -0
  29. {mgplot-0.1.1 → mgplot-0.1.3}/src/mgplot.egg-info/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mgplot
3
- Version: 0.1.1
4
- Summary: mgplot is a frontend for matplotlib
3
+ Version: 0.1.3
4
+ Summary: mgplot is a time-series/PeriodIndex frontend for matplotlib
5
5
  Project-URL: Homepage, https://github.com/bpalmer4/mgplot
6
6
  Requires-Python: >=3.11
7
7
  Description-Content-Type: text/markdown
@@ -18,6 +18,7 @@ Requires-Dist: pdoc
18
18
  Requires-Dist: pylint
19
19
  Requires-Dist: ruff
20
20
  Requires-Dist: pandas-stubs
21
+ Requires-Dist: numpy-typing
21
22
  Requires-Dist: types-tabulate
22
23
  Provides-Extra: build
23
24
  Requires-Dist: setuptools; extra == "build"
@@ -64,7 +65,7 @@ The remaining arrguments are passed as keyword arguments:
64
65
  - summary_plot() -- plots the latest data in a summary-format against
65
66
  the range of previous data.
66
67
 
67
- Once a plot has been generated and am Axes object is available. The
68
+ Once a plot has been generated and an Axes object is available. The
68
69
  plot can be finalised or published, with appropriate titles and
69
70
  axis labels using
70
71
  - finalise_plot()
@@ -38,7 +38,7 @@ The remaining arrguments are passed as keyword arguments:
38
38
  - summary_plot() -- plots the latest data in a summary-format against
39
39
  the range of previous data.
40
40
 
41
- Once a plot has been generated and am Axes object is available. The
41
+ Once a plot has been generated and an Axes object is available. The
42
42
  plot can be finalised or published, with appropriate titles and
43
43
  axis labels using
44
44
  - finalise_plot()
@@ -69,4 +69,4 @@ function:
69
69
 
70
70
  For more details, see the documentation folder.
71
71
 
72
- ---
72
+ ---
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "mgplot"
3
- version = "0.1.1"
4
- description = "mgplot is a frontend for matplotlib"
3
+ version = "0.1.3"
4
+ description = "mgplot is a time-series/PeriodIndex frontend for matplotlib"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
7
7
 
@@ -25,6 +25,7 @@ dependencies = [
25
25
 
26
26
  # - typing
27
27
  "pandas-stubs",
28
+ "numpy-typing",
28
29
  "types-tabulate",
29
30
  ]
30
31
 
@@ -8,7 +8,7 @@ with timeseries data that is indexed with a PeriodIndex.
8
8
 
9
9
  # --- version and author
10
10
  # NOTE: update version number here (below) and in pyproject.toml
11
- __version__ = "0.1.0"
11
+ __version__ = "0.1.3"
12
12
  __author__ = "Bryan Palmer"
13
13
 
14
14
 
@@ -27,7 +27,8 @@ from mgplot.growth_plot import (
27
27
  calc_growth,
28
28
  raw_growth_plot,
29
29
  series_growth_plot,
30
- GROWTH_KW_TYPES,
30
+ SERIES_GROWTH_KW_TYPES,
31
+ RAW_GROWTH_KW_TYPES,
31
32
  )
32
33
  from mgplot.multi_plot import (
33
34
  multi_start,
@@ -62,13 +63,7 @@ from mgplot.finalisers import (
62
63
  )
63
64
 
64
65
 
65
- # --- version and author
66
- __version__ = "0.0.1"
67
- __author__ = "Bryan Palmer"
68
-
69
-
70
66
  # --- public API
71
- SERIES_GROWTH_KW_TYPES = RAW_GROWTH_KW_TYPES = GROWTH_KW_TYPES
72
67
  __all__ = (
73
68
  "__version__",
74
69
  "__author__",
@@ -129,7 +124,6 @@ __all__ = (
129
124
  "REVISION_KW_TYPES",
130
125
  "RUN_KW_TYPES",
131
126
  "SUMMARY_KW_TYPES",
132
- "GROWTH_KW_TYPES",
133
127
  "SERIES_GROWTH_KW_TYPES",
134
128
  "RAW_GROWTH_KW_TYPES",
135
129
  # --- The rest are internal use only
@@ -16,7 +16,12 @@ from matplotlib.pyplot import Axes
16
16
 
17
17
  from mgplot.settings import DataT, get_setting
18
18
  from mgplot.utilities import apply_defaults, get_color_list, get_axes, constrain_data
19
- from mgplot.kw_type_checking import validate_kwargs, validate_expected, ExpectedTypeDict
19
+ from mgplot.kw_type_checking import (
20
+ ExpectedTypeDict,
21
+ validate_expected,
22
+ report_kwargs,
23
+ validate_kwargs,
24
+ )
20
25
  from mgplot.date_utils import set_labels
21
26
 
22
27
 
@@ -61,11 +66,15 @@ def bar_plot(
61
66
  - axes: Axes - The axes for the plot.
62
67
  """
63
68
 
64
- # --- validate the kwargs
65
- validate_kwargs(BAR_KW_TYPES, "bar_plot", **kwargs)
66
- # note data may not be time-series or have a period index.
69
+ # --- check the kwargs
70
+ me = "bar_plot"
71
+ report_kwargs(called_from=me, **kwargs)
72
+ validate_kwargs(BAR_KW_TYPES, me, **kwargs)
67
73
 
68
74
  # --- get the data
75
+ # no call to check_clean_timeseries here, as bar plots are not
76
+ # necessarily timeseries data. If the data is a Series, it will be
77
+ # converted to a DataFrame with a single column.
69
78
  df = DataFrame(data) # really we are only plotting DataFrames
70
79
  df, kwargs = constrain_data(df, **kwargs)
71
80
  item_count = len(df.columns)
@@ -5,7 +5,7 @@ file system. It is used to publish plots.
5
5
  """
6
6
 
7
7
  # --- imports
8
- from typing import Final
8
+ from typing import Final, Any
9
9
  import re
10
10
  import matplotlib as mpl
11
11
  import matplotlib.pyplot as plt
@@ -121,6 +121,22 @@ _internal_consistency_kwargs()
121
121
  # - private utility functions for finalise_plot()
122
122
 
123
123
 
124
+ def make_legend(axes: Axes, legend: None | bool | dict[str, Any]) -> None:
125
+ """Create a legend for the plot."""
126
+
127
+ if legend is None or legend is False:
128
+ return
129
+
130
+ if legend is True: # use the global default settings
131
+ legend = get_setting("legend")
132
+
133
+ if isinstance(legend, dict):
134
+ axes.legend(**legend)
135
+ return
136
+
137
+ print(f"Warning: expected dict argument for legend, but got {type(legend)}.")
138
+
139
+
124
140
  def _apply_value_kwargs(axes: Axes, settings: tuple, **kwargs) -> None:
125
141
  """Set matplotlib elements by name using Axes.set()."""
126
142
 
@@ -128,6 +144,9 @@ def _apply_value_kwargs(axes: Axes, settings: tuple, **kwargs) -> None:
128
144
  value = kwargs.get(setting, None)
129
145
  if value is None and setting not in _value_must_kwargs:
130
146
  continue
147
+ if setting == "ylabel" and value is None and axes.get_ylabel():
148
+ # already set - probably in series_growth_plot() - so skip
149
+ continue
131
150
  axes.set(**{setting: value})
132
151
 
133
152
 
@@ -141,6 +160,11 @@ def _apply_splat_kwargs(axes: Axes, settings: tuple, **kwargs) -> None:
141
160
  for method_name in settings:
142
161
  if method_name in kwargs:
143
162
 
163
+ if method_name == "legend":
164
+ # special case for legend
165
+ make_legend(axes, kwargs[method_name])
166
+ continue
167
+
144
168
  if kwargs[method_name] is None or kwargs[method_name] is False:
145
169
  continue
146
170
 
@@ -153,7 +177,7 @@ def _apply_splat_kwargs(axes: Axes, settings: tuple, **kwargs) -> None:
153
177
  method(**kwargs[method_name])
154
178
  else:
155
179
  print(
156
- f"Warning expected dict argument: {method_name} but got "
180
+ f"Warning expected dict argument for {method_name} but got "
157
181
  + f"{type(kwargs[method_name])}."
158
182
  )
159
183
 
@@ -297,9 +321,15 @@ def finalise_plot(axes: Axes, **kwargs) -> None:
297
321
  - None
298
322
  """
299
323
 
324
+ # --- check the kwargs
325
+ me = "finalise_plot"
326
+ report_kwargs(called_from=me, **kwargs)
327
+ validate_kwargs(FINALISE_KW_TYPES, me, **kwargs)
328
+
300
329
  # --- sanity checks
301
- report_kwargs(called_from="finalise_plot", **kwargs)
302
- validate_kwargs(FINALISE_KW_TYPES, "finalise_plot", **kwargs)
330
+ if len(axes.get_children()) < 1:
331
+ print("Warning: finalise_plot() called with empty axes, which was ignored.")
332
+ return
303
333
 
304
334
  # --- remember should we need to restore the axes limits
305
335
  xlim, ylim = axes.get_xlim(), axes.get_ylim()
@@ -149,8 +149,6 @@ def series_growth_plot_finalise(data: DataT, **kwargs) -> None:
149
149
  the growth series.
150
150
  """
151
151
 
152
- kwargs["ylabel"] = kwargs.get("ylabel", "Per cent Growth")
153
- kwargs["xlabel"] = kwargs.get("xlabel", None)
154
152
  plot_then_finalise(
155
153
  data=data,
156
154
  function=series_growth_plot,
@@ -165,8 +163,6 @@ def raw_growth_plot_finalise(data: DataT, **kwargs) -> None:
165
163
  set the ylabel in kwargs.
166
164
  """
167
165
 
168
- kwargs["ylabel"] = kwargs.get("ylabel", "Growth Units unspecified")
169
- kwargs["xlabel"] = kwargs.get("xlabel", None)
170
166
  plot_then_finalise(
171
167
  data=data,
172
168
  function=raw_growth_plot,
@@ -192,46 +188,24 @@ def summary_plot_finalise(
192
188
  defaults to "zscores".
193
189
  """
194
190
 
195
- # --- sanity checks
196
- if not isinstance(data.index, PeriodIndex):
197
- raise ValueError("data must have a PeriodIndex")
198
-
199
191
  # --- standard arguments
200
- kwargs["legend"] = kwargs.get(
201
- "legend",
202
- {
203
- # put the legend below the x-axis label
204
- "loc": "upper center",
205
- "fontsize": "xx-small",
206
- "bbox_to_anchor": (0.5, -0.125),
207
- "ncol": 4,
208
- },
209
- )
210
- start = kwargs.get("plot_from", None)
192
+ kwargs["title"] = kwargs.get("title", f"Summary at {data.index[-1]}")
193
+ kwargs["preserve_lims"] = kwargs.get(
194
+ "preserve_lims", True
195
+ ) # preserve the x-axis limits
196
+
197
+ start: None | int | Period = kwargs.get("plot_from", None)
198
+ if start is None:
199
+ start = data.index[0]
200
+ if isinstance(start, int):
201
+ start = data.index[start]
202
+ kwargs["plot_from"] = start
211
203
 
212
204
  for plot_type in (ZSCORES, ZSCALED):
213
205
  # some sorting of kwargs for plot production
214
206
  kwargs["plot_type"] = plot_type
215
- kwargs["title"] = kwargs.get("title", f"Summary at {data.index[-1]}")
216
- kwargs["pre_tag"] = plot_type # necessary because the title is same
217
- kwargs["preserve_lims"] = kwargs.get(
218
- "preserve_lims", True
219
- ) # preserve the x-axis limits
220
-
221
- # get the start date for the plot
222
- set_default = start is None
223
- if isinstance(start, int):
224
- start = data.index[start]
225
- if set_default:
226
- freq = data.index.freqstr[0]
227
- if freq not in ("D", "M", "Q"):
228
- raise ValueError(f"Unknown frequency {freq} for data index")
229
- start = Period("1995-01-01", freq=data.index.freqstr)
230
- kwargs["plot_from"] = start
231
-
232
- if plot_type not in (ZSCORES, ZSCALED):
233
- print(f"Unknown plot type {plot_type}, defaulting to {ZSCORES}")
234
- plot_type = ZSCORES
207
+ kwargs["pre_tag"] = plot_type # necessary because the title is the same
208
+
235
209
  if plot_type == "zscores":
236
210
  kwargs["xlabel"] = f"Z-scores for prints since {start}"
237
211
  kwargs["x0"] = True
@@ -14,6 +14,7 @@ from matplotlib.pyplot import Axes
14
14
  import matplotlib.patheffects as pe
15
15
  from tabulate import tabulate
16
16
 
17
+ from mgplot.finalise_plot import make_legend
17
18
  from mgplot.test import prepare_for_test
18
19
  from mgplot.settings import get_setting, DataT
19
20
  from mgplot.date_utils import set_labels
@@ -30,7 +31,7 @@ from mgplot.kw_type_checking import (
30
31
  ANNUAL = "annual"
31
32
  PERIODIC = "periodic"
32
33
 
33
- GROWTH_KW_TYPES: Final[ExpectedTypeDict] = {
34
+ RAW_GROWTH_KW_TYPES: Final[ExpectedTypeDict] = {
34
35
  "line_width": (float, int),
35
36
  "line_color": str,
36
37
  "line_style": str,
@@ -41,9 +42,13 @@ GROWTH_KW_TYPES: Final[ExpectedTypeDict] = {
41
42
  "annotation_rounding": int,
42
43
  "plot_from": (type(None), Period, int),
43
44
  "max_ticks": int,
45
+ "legend": (type(None), bool, dict, (str, object)),
44
46
  }
45
- validate_expected(GROWTH_KW_TYPES, "growth_plot")
46
- # --- alieses for intuitive compatibility
47
+ validate_expected(RAW_GROWTH_KW_TYPES, "growth_plot")
48
+ SERIES_GROWTH_KW_TYPES: Final[ExpectedTypeDict] = {
49
+ "ylabel": (str, type(None)),
50
+ } | RAW_GROWTH_KW_TYPES
51
+ validate_expected(SERIES_GROWTH_KW_TYPES, "growth_plot")
47
52
 
48
53
 
49
54
  # --- functions
@@ -183,10 +188,13 @@ def raw_growth_plot(
183
188
  - ValueError if the annual and periodic series do not have the same index.
184
189
  """
185
190
 
186
- # --- sanity checks
187
- report_kwargs(called_from="raw_growth_plot", **kwargs)
188
- validate_kwargs(GROWTH_KW_TYPES, "raw_growth_plot", **kwargs)
189
- data = check_clean_timeseries(data)
191
+ # --- check the kwargs
192
+ me = "raw_growth_plot"
193
+ report_kwargs(called_from=me, **kwargs)
194
+ validate_kwargs(RAW_GROWTH_KW_TYPES, me, **kwargs)
195
+
196
+ # --- data checks
197
+ data = check_clean_timeseries(data, me)
190
198
  if len(data.columns) != 2:
191
199
  raise TypeError("The data argument must be a pandas DataFrame with two columns")
192
200
 
@@ -228,7 +236,10 @@ def raw_growth_plot(
228
236
  linestyle=kwargs.get("line_style", "-"),
229
237
  )
230
238
  _annotations(annual, periodic, axes, **kwargs)
231
- axes.set_ylabel("Per cent Growth")
239
+
240
+ # --- expose the legend by default
241
+ legend = kwargs.get("legend", True)
242
+ make_legend(axes, legend)
232
243
 
233
244
  # --- fix the x-axis labels
234
245
  set_labels(axes, save_index, kwargs.get("max_ticks", 10))
@@ -251,18 +262,25 @@ def series_growth_plot(
251
262
  - takes the same kwargs as for growth_plot()
252
263
  """
253
264
 
265
+ # --- check the kwargs
266
+ me = "series_growth_plot"
267
+ report_kwargs(called_from=me, **kwargs)
268
+ validate_kwargs(SERIES_GROWTH_KW_TYPES, me, **kwargs)
269
+
254
270
  # --- sanity checks
255
- report_kwargs(called_from="series_growth_plot", **kwargs)
256
- data = check_clean_timeseries(data)
257
- # we will validate kwargs in raw_growth_plot()
258
271
  if not isinstance(data, Series):
259
272
  raise TypeError(
260
273
  "The data argument to series_growth_plot() must be a pandas Series"
261
274
  )
262
275
 
263
- # --- calculate growth and plot
276
+ # --- calculate growth and plot - add ylabel
277
+ ylabel: str | None = kwargs.pop("ylabel", None)
278
+ if ylabel is not None:
279
+ print(f"Did you intend to specify a value for the 'ylabel' in {me}()?")
280
+ ylabel = "Growth (%)" if ylabel is None else ylabel
264
281
  growth = calc_growth(data)
265
282
  ax = raw_growth_plot(growth, **kwargs)
283
+ ax.set_ylabel(ylabel)
266
284
  return ax
267
285
 
268
286
 
@@ -10,6 +10,7 @@ import matplotlib.pyplot as plt
10
10
  from pandas import DataFrame, Period
11
11
 
12
12
  from mgplot.settings import DataT, get_setting
13
+ from mgplot.finalise_plot import make_legend
13
14
  from mgplot.kw_type_checking import (
14
15
  report_kwargs,
15
16
  validate_kwargs,
@@ -119,25 +120,28 @@ def line_plot(data: DataT, **kwargs) -> plt.Axes:
119
120
  - axes: plt.Axes - the axes object for the plot
120
121
  """
121
122
 
122
- # sanity checks
123
- report_kwargs(called_from="line_plot", **kwargs)
124
- data = check_clean_timeseries(data)
125
- validate_kwargs(LINE_KW_TYPES, called_from="line_plot", **kwargs)
123
+ # --- check the kwargs
124
+ me = "line_plot"
125
+ report_kwargs(called_from=me, **kwargs)
126
+ validate_kwargs(LINE_KW_TYPES, me, **kwargs)
126
127
 
127
- # the data to be plotted:
128
+ # --- check the data
129
+ data = check_clean_timeseries(data, me)
128
130
  df = DataFrame(data) # really we are only plotting DataFrames
129
131
  df, kwargs = constrain_data(df, **kwargs)
130
- if df.empty:
132
+
133
+ # --- Let's plot
134
+ axes, kwargs = get_axes(**kwargs) # get the axes to plot on
135
+ if df.empty or df.isna().all().all():
136
+ # Note: finalise plot will ignore an empty axes object
131
137
  print("Warning: No data to plot.")
138
+ return axes
132
139
 
133
- # get the arguments for each line we will plot ...
140
+ # --- get the arguments for each line we will plot ...
134
141
  item_count = len(df.columns)
135
142
  num_data_points = len(df)
136
143
  swce, kwargs = _get_style_width_color_etc(item_count, num_data_points, **kwargs)
137
144
 
138
- # Let's plot
139
- axes, kwargs = get_axes(**kwargs) # get the axes to plot on
140
-
141
145
  for i, column in enumerate(df.columns):
142
146
  series = df[column]
143
147
  series = series.dropna() if DROPNA in swce and swce[DROPNA][i] else series
@@ -169,10 +173,8 @@ def line_plot(data: DataT, **kwargs) -> plt.Axes:
169
173
  # add a legend if requested
170
174
  if len(df.columns) > 1:
171
175
  kwargs[LEGEND] = kwargs.get(LEGEND, get_setting("legend"))
172
- if LEGEND in kwargs and kwargs[LEGEND] is not None:
173
- legend = kwargs[LEGEND]
174
- if isinstance(legend, bool):
175
- legend = get_setting("legend")
176
- axes.legend(**legend)
176
+
177
+ if LEGEND in kwargs:
178
+ make_legend(axes, kwargs[LEGEND])
177
179
 
178
180
  return axes
@@ -45,11 +45,11 @@ from mgplot.kw_type_checking import (
45
45
  limit_kwargs,
46
46
  ExpectedTypeDict,
47
47
  report_kwargs,
48
+ validate_kwargs,
48
49
  )
49
50
  from mgplot.finalise_plot import finalise_plot, FINALISE_KW_TYPES
50
51
  from mgplot.settings import DataT
51
52
  from mgplot.test import prepare_for_test
52
- from mgplot.utilities import check_clean_timeseries
53
53
 
54
54
  from mgplot.line_plot import line_plot, LINE_KW_TYPES
55
55
  from mgplot.bar_plot import bar_plot, BAR_KW_TYPES
@@ -58,8 +58,12 @@ from mgplot.postcovid_plot import postcovid_plot, POSTCOVID_KW_TYPES
58
58
  from mgplot.revision_plot import revision_plot, REVISION_KW_TYPES
59
59
  from mgplot.run_plot import run_plot, RUN_KW_TYPES
60
60
  from mgplot.summary_plot import summary_plot, SUMMARY_KW_TYPES
61
- from mgplot.growth_plot import series_growth_plot, raw_growth_plot, GROWTH_KW_TYPES
62
-
61
+ from mgplot.growth_plot import (
62
+ series_growth_plot,
63
+ raw_growth_plot,
64
+ RAW_GROWTH_KW_TYPES,
65
+ SERIES_GROWTH_KW_TYPES,
66
+ )
63
67
 
64
68
  # --- constants
65
69
  EXPECTED_CALLABLES: Final[dict[Callable, ExpectedTypeDict]] = {
@@ -73,8 +77,8 @@ EXPECTED_CALLABLES: Final[dict[Callable, ExpectedTypeDict]] = {
73
77
  revision_plot: REVISION_KW_TYPES,
74
78
  run_plot: RUN_KW_TYPES,
75
79
  summary_plot: SUMMARY_KW_TYPES,
76
- series_growth_plot: GROWTH_KW_TYPES,
77
- raw_growth_plot: GROWTH_KW_TYPES,
80
+ series_growth_plot: SERIES_GROWTH_KW_TYPES,
81
+ raw_growth_plot: RAW_GROWTH_KW_TYPES,
78
82
  }
79
83
 
80
84
 
@@ -139,9 +143,13 @@ def plot_then_finalise(
139
143
  Returns None.
140
144
  """
141
145
 
142
- # --- sanity checks
143
- report_kwargs(called_from="plot_then_finalise", **kwargs)
144
- data = check_clean_timeseries(data)
146
+ # --- checks
147
+ me = "plot_then_finalise"
148
+ report_kwargs(called_from=me, **kwargs)
149
+ # validate once we have established the first function
150
+
151
+ # data is not checked here, assume it is checked by the called
152
+ # plot function.
145
153
 
146
154
  # --- check the function argument
147
155
  first, kwargs_ = first_unchain(function, **kwargs)
@@ -152,14 +160,21 @@ def plot_then_finalise(
152
160
  else:
153
161
  # this is an unexpected Callable, so we will give it a try
154
162
  print(f"Unknown proposed function: {first}; nonetheless, will give it a try.")
163
+ expected = {}
155
164
  plot_kwargs = kwargs_
165
+ validate_kwargs(expected | FINALISE_KW_TYPES, me, **plot_kwargs)
156
166
 
157
167
  # --- call the first function with the data and kwargs
158
-
159
168
  axes = first(data, **plot_kwargs)
160
169
 
161
- # --- finalise the plot
170
+ # --- remove potentially overlapping kwargs
162
171
  fp_kwargs = limit_kwargs(FINALISE_KW_TYPES, **kwargs)
172
+ overlapping = expected.keys() & FINALISE_KW_TYPES.keys()
173
+ if overlapping:
174
+ for key in overlapping:
175
+ fp_kwargs.pop(key, None) # remove overlapping keys from kwargs
176
+
177
+ # --- finalise the plot
163
178
  finalise_plot(axes, **fp_kwargs)
164
179
 
165
180
 
@@ -191,10 +206,12 @@ def multi_start(
191
206
  """
192
207
 
193
208
  # --- sanity checks
194
- report_kwargs(called_from="multi_start", **kwargs)
195
- data = check_clean_timeseries(data)
209
+ me = "multi_start"
210
+ report_kwargs(called_from=me, **kwargs)
196
211
  if not isinstance(starts, Iterable):
197
212
  raise ValueError("starts must be an iterable of None, Period or int")
213
+ # data not checked here, assume it is checked by the called
214
+ # plot function.
198
215
 
199
216
  # --- check the function argument
200
217
  original_tag: Final[str] = kwargs.get("tag", "")
@@ -219,7 +236,7 @@ def multi_column(
219
236
  The plot title will be the column name.
220
237
 
221
238
  Parameters
222
- - data: DataFrame - The data to be plotted.
239
+ - data: DataFrame - The data to be plotted
223
240
  - function: Callable - The plotting function to be used.
224
241
  - **kwargs: Additional keyword arguments to be passed to
225
242
  the plotting function.
@@ -228,8 +245,12 @@ def multi_column(
228
245
  """
229
246
 
230
247
  # --- sanity checks
231
- report_kwargs(called_from="multi_column", **kwargs)
232
- data = check_clean_timeseries(data)
248
+ me = "multi_column"
249
+ report_kwargs(called_from=me, **kwargs)
250
+ if not isinstance(data, DataFrame):
251
+ raise TypeError("data must be a pandas DataFrame for multi_column()")
252
+ # Otherwise, the data is assumed to be checked by the called
253
+ # plot function, so we do not check it here.
233
254
 
234
255
  # --- check the function argument
235
256
  title_stem = kwargs.get("title", "")
@@ -4,29 +4,31 @@ Plot the pre-COVID trajectory against the current trend.
4
4
  """
5
5
 
6
6
  # --- imports
7
- from collections.abc import Sequence
8
7
  from pandas import DataFrame, Series, Period, PeriodIndex
9
8
  from matplotlib.pyplot import Axes
10
9
  from numpy import arange, polyfit
11
10
 
12
11
  from mgplot.settings import DataT, get_setting
13
- from mgplot.line_plot import line_plot
12
+ from mgplot.line_plot import line_plot, LINE_KW_TYPES
14
13
  from mgplot.utilities import check_clean_timeseries
15
- from mgplot.kw_type_checking import report_kwargs, ExpectedTypeDict, validate_expected
14
+ from mgplot.kw_type_checking import (
15
+ ExpectedTypeDict,
16
+ validate_kwargs,
17
+ validate_expected,
18
+ report_kwargs,
19
+ )
16
20
 
17
21
 
18
22
  # --- constants
19
- WIDTH = "width"
20
- STYLE = "style"
21
23
  START_R = "start_r"
22
24
  END_R = "end_r"
25
+ WIDTH = "width"
26
+ STYLE = "style"
23
27
 
24
28
  POSTCOVID_KW_TYPES: ExpectedTypeDict = {
25
- WIDTH: (Sequence, (int, float), int, float),
26
- STYLE: (Sequence, (str,), str),
27
29
  START_R: Period,
28
30
  END_R: Period,
29
- }
31
+ } | LINE_KW_TYPES
30
32
  validate_expected(POSTCOVID_KW_TYPES, "postcovid_plot")
31
33
 
32
34
 
@@ -64,9 +66,13 @@ def postcovid_plot(data: DataT, **kwargs) -> Axes:
64
66
  - ValueError if regression start is after regression end
65
67
  """
66
68
 
67
- # --- sanity checks
68
- report_kwargs(called_from="postcovid_plot", **kwargs)
69
- data = check_clean_timeseries(data)
69
+ # --- check the kwargs
70
+ me = "postcovid_plot"
71
+ report_kwargs(called_from=me, **kwargs)
72
+ validate_kwargs(POSTCOVID_KW_TYPES, me, **kwargs)
73
+
74
+ # --- check the data
75
+ data = check_clean_timeseries(data, me)
70
76
  if not isinstance(data, Series):
71
77
  raise TypeError("The series argument must be a pandas Series")
72
78
  series: Series = data
@@ -103,11 +109,14 @@ def postcovid_plot(data: DataT, **kwargs) -> Axes:
103
109
  projection.name = "Pre-COVID projection"
104
110
  data_set = DataFrame([projection, recent]).T
105
111
 
112
+ # --- activate plot settings
106
113
  kwargs[WIDTH] = kwargs.pop(
107
- WIDTH, [get_setting("line_normal"), get_setting("line_wide")]
108
- )
109
- kwargs[STYLE] = kwargs.pop(STYLE, ["--", "-"])
110
- kwargs["legend"] = kwargs.pop("legend", True)
114
+ WIDTH, (get_setting("line_normal"), get_setting("line_wide"))
115
+ ) # series line is thicker than projection
116
+ kwargs[STYLE] = kwargs.pop(STYLE, ("--", "-")) # dashed regression line
117
+ kwargs["legend"] = kwargs.pop("legend", True) # show legend by default
118
+ kwargs["annotate"] = kwargs.pop("annotate", (False, True)) # annotate series only
119
+ kwargs["color"] = kwargs.pop("color", ("darkblue", "#dd0000"))
111
120
 
112
121
  return line_plot(
113
122
  data_set,
@@ -40,13 +40,15 @@ def revision_plot(data: DataT, **kwargs) -> Axes:
40
40
  apply int rounding.
41
41
  """
42
42
 
43
- # --- sanity checks
44
- data = check_clean_timeseries(data)
45
- report_kwargs(called_from="revision_plot", **kwargs)
46
- validate_kwargs(REVISION_KW_TYPES, "revision_plot", **kwargs)
43
+ # --- check the kwargs and data
44
+ me = "revision_plot"
45
+ report_kwargs(called_from=me, **kwargs)
46
+ validate_kwargs(REVISION_KW_TYPES, me, **kwargs)
47
+
48
+ data = check_clean_timeseries(data, me)
47
49
 
48
50
  # --- critical defaults
49
- kwargs["plot_from"] = kwargs.get("plot_from", -18)
51
+ kwargs["plot_from"] = kwargs.get("plot_from", -19)
50
52
 
51
53
  # --- plot
52
54
  axes = line_plot(data, **kwargs)
@@ -6,7 +6,7 @@ the 'runs' in a series.
6
6
 
7
7
  # --- imports
8
8
  from collections.abc import Sequence
9
- from pandas import Series, concat
9
+ from pandas import Series, concat, period_range
10
10
  from matplotlib.pyplot import Axes
11
11
  from matplotlib import patheffects as pe
12
12
 
@@ -105,17 +105,17 @@ def _plot_runs(
105
105
  ),
106
106
  va=vert_align,
107
107
  ha="left",
108
- fontsize="small",
108
+ fontsize="x-small",
109
109
  rotation=90,
110
110
  )
111
111
  text.set_path_effects([pe.withStroke(linewidth=5, foreground="w")])
112
112
 
113
113
 
114
- def run_plot(series: DataT, **kwargs) -> Axes:
114
+ def run_plot(data: DataT, **kwargs) -> Axes:
115
115
  """Plot a series of percentage rates, highlighting the increasing runs.
116
116
 
117
117
  Arguments
118
- - series - ordered pandas Series of percentages, with PeriodIndex
118
+ - data - ordered pandas Series of percentages, with PeriodIndex
119
119
  - **kwargs
120
120
  - threshold - float - used to ignore micro noise near zero
121
121
  (for example, threshhold=0.01)
@@ -129,17 +129,17 @@ def run_plot(series: DataT, **kwargs) -> Axes:
129
129
  Return
130
130
  - matplotlib Axes object"""
131
131
 
132
- # --- sanity checks
133
- series = check_clean_timeseries(series)
132
+ # --- check the kwargs
133
+ me = "run_plot"
134
+ report_kwargs(called_from=me, **kwargs)
135
+ validate_kwargs(RUN_KW_TYPES, me, **kwargs)
136
+
137
+ # --- check the data
138
+ series = check_clean_timeseries(data, me)
134
139
  if not isinstance(series, Series):
135
140
  raise TypeError("series must be a pandas Series for run_plot()")
136
141
  series, kwargs = constrain_data(series, **kwargs)
137
142
 
138
- # --- check the kwargs
139
- report_kwargs(called_from="run_plot", **kwargs)
140
- expected = RUN_KW_TYPES
141
- validate_kwargs(expected, "run_plot", **kwargs)
142
-
143
143
  # --- default arguments - in **kwargs
144
144
  kwargs[THRESHOLD] = kwargs.get(THRESHOLD, 0.1)
145
145
  kwargs[ROUND] = kwargs.get(ROUND, 2)
@@ -162,7 +162,7 @@ def run_plot(series: DataT, **kwargs) -> Axes:
162
162
 
163
163
  # plot the line
164
164
  kwargs["drawstyle"] = kwargs.get("drawstyle", "steps-post")
165
- lp_kwargs = limit_kwargs(RUN_KW_TYPES, **kwargs)
165
+ lp_kwargs = limit_kwargs(LINE_KW_TYPES, **kwargs)
166
166
  axes = line_plot(series, **lp_kwargs)
167
167
 
168
168
  # plot the runs
@@ -180,3 +180,12 @@ def run_plot(series: DataT, **kwargs) -> Axes:
180
180
  "Expected 'up', 'down', or 'both'."
181
181
  )
182
182
  return axes
183
+
184
+
185
+ # test ---
186
+ if __name__ == "__main__":
187
+ N_PERIODS = 25
188
+ periods = period_range(start="2020Q1", periods=N_PERIODS, freq="Q")
189
+ dataset = Series([1] * N_PERIODS, index=periods).cumsum()
190
+
191
+ ax = run_plot(data=dataset, junk="should generate a warning")
@@ -9,7 +9,7 @@ from matplotlib.pyplot import Axes
9
9
  from mgplot.settings import DataT
10
10
  from mgplot.line_plot import line_plot, LINE_KW_TYPES
11
11
  from mgplot.utilities import get_color_list, get_setting, check_clean_timeseries
12
- from mgplot.kw_type_checking import report_kwargs
12
+ from mgplot.kw_type_checking import report_kwargs, validate_kwargs
13
13
 
14
14
 
15
15
  # --- constants
@@ -44,14 +44,17 @@ def seastrend_plot(data: DataT, **kwargs) -> Axes:
44
44
  # Note: we will rely on the line_plot() function to do most of the work.
45
45
  # including constraining the data to the plot_from keyword argument.
46
46
 
47
- # --- sanity checks
48
- report_kwargs(called_from="seastrend_plot", **kwargs)
49
- data = check_clean_timeseries(data)
47
+ # --- check the kwargs
48
+ me = "seastrend_plot"
49
+ report_kwargs(called_from=me, **kwargs)
50
+ validate_kwargs(SEASTREND_KW_TYPES, me, **kwargs)
51
+
52
+ # --- check the data
53
+ data = check_clean_timeseries(data, me)
50
54
  if len(data.columns) < 2:
51
55
  raise ValueError(
52
56
  "seas_trend_plot() expects a DataFrame data item with at least 2 columns."
53
57
  )
54
- # let line_plot() handle validate_kwargs()
55
58
 
56
59
  # --- defaults if not in kwargs
57
60
  colors = kwargs.pop(COLOR, get_color_list(2))
@@ -17,6 +17,7 @@ from pandas import DataFrame, Period
17
17
 
18
18
  # local imports
19
19
  from mgplot.settings import DataT
20
+ from mgplot.finalise_plot import make_legend
20
21
  from mgplot.utilities import constrain_data, check_clean_timeseries
21
22
  from mgplot.kw_type_checking import (
22
23
  report_kwargs,
@@ -35,6 +36,7 @@ SUMMARY_KW_TYPES: ExpectedTypeDict = {
35
36
  "middle": float,
36
37
  "plot_type": str,
37
38
  "plot_from": (int, Period, type(None)),
39
+ "legend": (type(None), bool, dict, (str, object)),
38
40
  }
39
41
  validate_expected(SUMMARY_KW_TYPES, "summary_plot")
40
42
 
@@ -213,20 +215,31 @@ def summary_plot(
213
215
  Returns Axes.
214
216
  """
215
217
 
216
- # --- sanity checks
217
- data = check_clean_timeseries(data)
218
+ # --- check the kwargs
219
+ me = "summary_plot"
220
+ report_kwargs(called_from=me, **kwargs)
221
+ validate_kwargs(SUMMARY_KW_TYPES, me, **kwargs)
222
+
223
+ # --- check the data
224
+ data = check_clean_timeseries(data, me)
218
225
  if not isinstance(data, DataFrame):
219
226
  raise TypeError("data must be a pandas DataFrame for summary_plot()")
220
227
  df = DataFrame(data) # syntactic sugar for type hinting
221
228
 
222
- # --- check the arguments
223
- report_kwargs("summary_plot", **kwargs)
224
- validate_kwargs(SUMMARY_KW_TYPES, "summary_plot", **kwargs)
225
-
226
229
  # --- optional arguments
227
230
  verbose = kwargs.pop("verbose", False)
228
231
  middle = float(kwargs.pop("middle", 0.8))
229
232
  plot_type = kwargs.pop("plot_type", ZSCORES)
233
+ kwargs["legend"] = kwargs.get(
234
+ "legend",
235
+ {
236
+ # put the legend below the x-axis label
237
+ "loc": "upper center",
238
+ "fontsize": "xx-small",
239
+ "bbox_to_anchor": (0.5, -0.125),
240
+ "ncol": 4,
241
+ },
242
+ )
230
243
 
231
244
  # get the data, calculate z-scores and scaled scores based on the start period
232
245
  subset, kwargs = constrain_data(df, **kwargs)
@@ -236,5 +249,7 @@ def summary_plot(
236
249
  adjusted = z_scores if plot_type == ZSCORES else z_scaled
237
250
  ax = _horizontal_bar_plot(subset, adjusted, middle, plot_type, kwargs)
238
251
  ax.tick_params(axis="y", labelsize="small")
252
+ make_legend(ax, kwargs["legend"])
239
253
  ax.set_xlim(kwargs.get("xlim", None)) # provide space for the labels
254
+
240
255
  return ax
@@ -25,7 +25,7 @@ from mgplot.settings import DataT
25
25
 
26
26
 
27
27
  # --- functions
28
- def check_clean_timeseries(data: DataT) -> DataT:
28
+ def check_clean_timeseries(data: DataT, called_by: str) -> DataT:
29
29
  """
30
30
  Check timeseries data for the following:
31
31
  - That the data is a Series or DataFrame.
@@ -71,7 +71,10 @@ def check_clean_timeseries(data: DataT) -> DataT:
71
71
  missing = complete.difference(data_index)
72
72
  if not missing.empty:
73
73
  plural = "s" if len(missing) > 1 else ""
74
- print(f"Warning: {len(missing)} period{plural} missing from data index. ")
74
+ print(
75
+ f"Warning: {len(missing)} period{plural} missing from data index. "
76
+ + f"Found by {called_by}."
77
+ )
75
78
 
76
79
  # --- return the final data
77
80
  return data
@@ -79,7 +82,7 @@ def check_clean_timeseries(data: DataT) -> DataT:
79
82
 
80
83
  def constrain_data(data: DataT, **kwargs) -> tuple[DataT, dict[str, Any]]:
81
84
  """
82
- Constrain the data to start after a certain point.
85
+ Constrain the data to start after a certain point - kwargs["plot_from"].
83
86
 
84
87
  Args:
85
88
  data: the data to be constrained
@@ -234,7 +237,7 @@ if __name__ == "__main__":
234
237
  my_list = [np.nan, np.nan, 1.12345, 2.12345, 3.12345, 4.12345, 5.12345]
235
238
  _ = Series(my_list, period_range(start="2023-01", periods=len(my_list), freq="M"))
236
239
  _ = _.drop(index=[_.index[3]])
237
- clean = check_clean_timeseries(_)
240
+ clean = check_clean_timeseries(_, "test")
238
241
  print(f"Cleaned data:\n{clean}")
239
242
 
240
243
  # --- test annotate_series()
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mgplot
3
- Version: 0.1.1
4
- Summary: mgplot is a frontend for matplotlib
3
+ Version: 0.1.3
4
+ Summary: mgplot is a time-series/PeriodIndex frontend for matplotlib
5
5
  Project-URL: Homepage, https://github.com/bpalmer4/mgplot
6
6
  Requires-Python: >=3.11
7
7
  Description-Content-Type: text/markdown
@@ -18,6 +18,7 @@ Requires-Dist: pdoc
18
18
  Requires-Dist: pylint
19
19
  Requires-Dist: ruff
20
20
  Requires-Dist: pandas-stubs
21
+ Requires-Dist: numpy-typing
21
22
  Requires-Dist: types-tabulate
22
23
  Provides-Extra: build
23
24
  Requires-Dist: setuptools; extra == "build"
@@ -64,7 +65,7 @@ The remaining arrguments are passed as keyword arguments:
64
65
  - summary_plot() -- plots the latest data in a summary-format against
65
66
  the range of previous data.
66
67
 
67
- Once a plot has been generated and am Axes object is available. The
68
+ Once a plot has been generated and an Axes object is available. The
68
69
  plot can be finalised or published, with appropriate titles and
69
70
  axis labels using
70
71
  - finalise_plot()
@@ -10,6 +10,7 @@ pdoc
10
10
  pylint
11
11
  ruff
12
12
  pandas-stubs
13
+ numpy-typing
13
14
  types-tabulate
14
15
 
15
16
  [build]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes