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/utilities.py ADDED
@@ -0,0 +1,254 @@
1
+ """
2
+ utilities.py:
3
+ Utiltiy functions used by more than one mgplot module.
4
+ These are not intended to be used directly by the user.
5
+
6
+ Functions:
7
+ - check_clean_timeseries()
8
+ - constrain_data()
9
+ - apply_defaults()
10
+ - get_color_list()
11
+ - get_axes()
12
+ - annotate_series()
13
+ """
14
+
15
+ # --- imports
16
+ import math
17
+ from typing import Any
18
+ from pandas import Series, DataFrame, Period, PeriodIndex, period_range
19
+ import numpy as np
20
+ from matplotlib import cm
21
+ from matplotlib.pyplot import Axes, subplots
22
+
23
+ from mgplot.settings import get_setting
24
+ from mgplot.settings import DataT
25
+
26
+
27
+ # --- functions
28
+ def check_clean_timeseries(data: DataT) -> DataT:
29
+ """
30
+ Check timeseries data for the following:
31
+ - That the data is a Series or DataFrame.
32
+ - That the index is a PeriodIndex
33
+ - That the index is unique and monotonic increasing
34
+
35
+ Remove any leading NAN rows or columns from the data.
36
+
37
+ Return the cleaned data.
38
+
39
+ Args:
40
+ - data: the data to be cleaned
41
+
42
+ Returns:
43
+ - The data with leading NaN values removed.
44
+
45
+ Raises TypeError/Value if problems found
46
+ """
47
+
48
+ # --- initial checks
49
+ if not isinstance(data, (Series, DataFrame)):
50
+ raise TypeError("Data must be a pandas Series or DataFrame.")
51
+ if not isinstance(data.index, PeriodIndex):
52
+ raise TypeError("Data index must be a PeriodIndex.")
53
+ if not data.index.is_unique:
54
+ raise ValueError("Data index must be unique.")
55
+ if not data.index.is_monotonic_increasing:
56
+ raise ValueError("Data index must be monotonic increasing.")
57
+
58
+ # --- remove any leading NaNs
59
+ start = data.first_valid_index()
60
+ if start is None:
61
+ return data # no valid index, return original data
62
+ if not isinstance(start, Period): # syntactic sugar for type hinting
63
+ raise TypeError("First valid index must be a Period.")
64
+ data = data.loc[data.index >= start]
65
+
66
+ # --- report and missing periods (ie. potentially incomplete data)
67
+ data_index = PeriodIndex(data.index) # syntactic sugar for type hinting
68
+ complete = period_range(
69
+ start=data_index.min(), end=data_index.max(), freq=data_index.freq
70
+ )
71
+ missing = complete.difference(data_index)
72
+ if not missing.empty:
73
+ plural = "s" if len(missing) > 1 else ""
74
+ print(f"Warning: {len(missing)} period{plural} missing from data index. ")
75
+
76
+ # --- return the final data
77
+ return data
78
+
79
+
80
+ def constrain_data(data: DataT, **kwargs) -> tuple[DataT, dict[str, Any]]:
81
+ """
82
+ Constrain the data to start after a certain point.
83
+
84
+ Args:
85
+ data: the data to be constrained
86
+ kwargs: keyword arguments - uses "plot_from" in kwargs to constrain the data
87
+
88
+ Assume:
89
+ - that mgplot.utilitiesd.check_clean_timeseries() has already been applied
90
+ - that the data is a Series or DataFrame with a PeriodIndex
91
+ - that the index is unique and monotonic increasing
92
+
93
+ Returns:
94
+ A tuple of the constrained data and the modified kwargs.
95
+ """
96
+
97
+ plot_from = kwargs.pop("plot_from", 0)
98
+ if isinstance(plot_from, Period) and isinstance(data.index, PeriodIndex):
99
+ data = data.loc[data.index >= plot_from]
100
+ elif isinstance(plot_from, int):
101
+ data = data.iloc[plot_from:]
102
+ elif plot_from is None:
103
+ pass
104
+ else:
105
+ print(f"Warning: {plot_from=} either not a valid type or not applicable. ")
106
+ return data, kwargs
107
+
108
+
109
+ def apply_defaults(
110
+ length: int, defaults: dict[str, Any], kwargs_d: dict[str, Any]
111
+ ) -> tuple[dict[str, Any], dict[str, list[Any] | tuple[Any]]]:
112
+ """
113
+ Get arguments from kwargs_d, and apply a default from the
114
+ defaults dict if not there. Remove the item from kwargs_d.
115
+
116
+ Agumenets:
117
+ length: the number of lines to be plotted
118
+ defaults: a dictionary of default values
119
+ kwargs_d: a dictionary of keyword arguments
120
+
121
+ Returns a tuple of two dictionaries:
122
+ - the first is a dictionary populated with the arguments
123
+ from kwargs_d or the defaults dictionary, where the values
124
+ are placed in lists or tuples if not already in that format
125
+ - the second is a modified kwargs_d dictionary, with the default
126
+ keys removed.
127
+ """
128
+
129
+ returnable = {} # return vehicle
130
+
131
+ for option, default in defaults.items():
132
+ val = kwargs_d.get(option, default)
133
+ # make sure our return value is a list/tuple
134
+ returnable[option] = val if isinstance(val, (list, tuple)) else (val,)
135
+
136
+ # remove the option from kwargs
137
+ if option in kwargs_d:
138
+ del kwargs_d[option]
139
+
140
+ # repeat multi-item lists if not long enough for all lines to be plotted
141
+ if len(returnable[option]) < length and length > 1:
142
+ multiplier = math.ceil(length / len(returnable[option]))
143
+ returnable[option] = returnable[option] * multiplier
144
+
145
+ return returnable, kwargs_d
146
+
147
+
148
+ def get_color_list(count: int) -> list[str]:
149
+ """
150
+ Get a list of colours for plotting.
151
+
152
+ Args:
153
+ count: the number of colours to return
154
+
155
+ Returns:
156
+ A list of colours.
157
+ """
158
+
159
+ colors: dict[int, list[str]] = get_setting("colors")
160
+ if count in colors:
161
+ return colors[count]
162
+
163
+ if count < max(colors.keys()):
164
+ options = [k for k in colors.keys() if k > count]
165
+ return colors[min(options)][:count]
166
+
167
+ c = cm.get_cmap("nipy_spectral")(np.linspace(0, 1, count))
168
+ return [f"#{int(x*255):02x}{int(y*255):02x}{int(z*255):02x}" for x, y, z, _ in c]
169
+
170
+
171
+ def get_axes(**kwargs) -> tuple[Axes, dict[str, Any]]:
172
+ """
173
+ Get the axes to plot on.
174
+ If not passed in kwargs, create a new figure and axes.
175
+ """
176
+
177
+ ax = "ax"
178
+ if ax in kwargs and kwargs[ax] is not None:
179
+ axes: Axes = kwargs[ax]
180
+ if not isinstance(axes, Axes):
181
+ raise TypeError("The ax argument must be a matplotlib Axes object")
182
+ return axes, {}
183
+
184
+ figsize = kwargs.pop("figsize", get_setting("figsize"))
185
+ _fig, axes = subplots(figsize=figsize)
186
+ return axes, kwargs
187
+
188
+
189
+ def annotate_series(
190
+ series: Series,
191
+ axes: Axes,
192
+ rounding: int | bool = False,
193
+ color: str = "#444444",
194
+ fontsize: int | str = "small",
195
+ **kwargs,
196
+ ) -> None:
197
+ """Annotate the right-hand end-point of a line-plotted series."""
198
+
199
+ latest = series.dropna()
200
+ if latest.empty:
201
+ return
202
+
203
+ x, y = latest.index[-1], latest.iloc[-1]
204
+ if y is None or math.isnan(y):
205
+ return
206
+
207
+ r_string = f" {y}" # default to no rounding
208
+ original = rounding
209
+ if isinstance(rounding, bool) and rounding:
210
+ rounding = 0 if y >= 100 else 1 if y >= 10 else 2
211
+ if not isinstance(rounding, bool) and isinstance(rounding, int):
212
+ r_string = f" {y:.{rounding}f}"
213
+
214
+ if "test" in kwargs:
215
+ print(f"annotate_series: {x=}, {y=}, {original=} {rounding=} {r_string=}")
216
+ return
217
+
218
+ axes.text(
219
+ x=x,
220
+ y=y,
221
+ s=r_string,
222
+ ha="left",
223
+ va="center",
224
+ fontsize=fontsize,
225
+ color=color,
226
+ font="Helvetica",
227
+ )
228
+
229
+
230
+ # --- test code
231
+ if __name__ == "__main__":
232
+
233
+ # --- test check_clean_timeseries_data()
234
+ my_list = [np.nan, np.nan, 1.12345, 2.12345, 3.12345, 4.12345, 5.12345]
235
+ _ = Series(my_list, period_range(start="2023-01", periods=len(my_list), freq="M"))
236
+ _ = _.drop(index=[_.index[3]])
237
+ clean = check_clean_timeseries(_)
238
+ print(f"Cleaned data:\n{clean}")
239
+
240
+ # --- test annotate_series()
241
+ print()
242
+ _fig, ax_ = subplots(figsize=(9, 4.5))
243
+ series2_ = Series([1.12345, 2.12345, 3.12345, 4.12345, 5.12345])
244
+ rounding_ = (
245
+ False,
246
+ True,
247
+ 0,
248
+ 1,
249
+ 2,
250
+ 3,
251
+ )
252
+ for r in rounding_:
253
+ annotate_series(series2_, ax_, rounding=r, test=True)
254
+ print("Done")
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.4
2
+ Name: mgplot
3
+ Version: 0.1.0
4
+ Summary: mgplot is a frontend for matplotlib
5
+ Project-URL: Homepage, https://github.com/bpalmer4/mgplot
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: typing
10
+ Requires-Dist: numpy
11
+ Requires-Dist: matplotlib
12
+ Requires-Dist: numpy
13
+ Requires-Dist: pandas
14
+ Requires-Dist: pathlib
15
+ Requires-Dist: tabulate
16
+ Requires-Dist: black
17
+ Requires-Dist: mypy
18
+ Requires-Dist: ruff
19
+ Requires-Dist: pylint
20
+ Requires-Dist: pdoc
21
+ Requires-Dist: twine
22
+ Requires-Dist: pandas-stubs
23
+ Requires-Dist: types-tabulate
24
+ Provides-Extra: build
25
+ Requires-Dist: setuptools; extra == "build"
26
+ Requires-Dist: cython; extra == "build"
27
+ Dynamic: license-file
28
+
29
+ mgplot
30
+ ======
31
+
32
+ Description
33
+ -----------
34
+ mgplot is an open-source python frontend for the matplotlib
35
+ package to:
36
+ 1. produce time-series charts that can be a little difficult or
37
+ tricky to produce directly,
38
+ 2. finalise (or publish) charts with titles, xlabels, ylabels,
39
+ etc., all while
40
+ 3. minimising code duplication, and maintaining a common plot
41
+ style or look-and-feel.
42
+
43
+ Import
44
+ ------
45
+ ```
46
+ import mgplot as mg
47
+ ```
48
+
49
+ For more information
50
+ --------------------
51
+ - See the dicumentation folder
52
+
53
+ ---
@@ -0,0 +1,24 @@
1
+ mgplot/__init__.py,sha256=RJclV9vC_sv5--PRr3AOM_cPZM5yFwxd-0__OnaB1KQ,2787
2
+ mgplot/bar_plot.py,sha256=lNo4pgLsjL7AwEy-ZzLn3zs2O2BQMAlzb9X8m5bWYlA,3604
3
+ mgplot/colors.py,sha256=0cAbC5pqZO2dCCPnRRG2x9xas9TQXwIuI_OgC0JaFNE,5066
4
+ mgplot/date_utils.py,sha256=5ZSaXmwtfPyDKObeO69uoEzzeAgpwBYPYut_7TFsmz4,9285
5
+ mgplot/finalise_plot.py,sha256=_fPioM5WpWni5QyPsm5gEhiImn0tpzZx4AW58VI0i4k,11636
6
+ mgplot/finalisers.py,sha256=DU5MKAZ2MFv4iZqm4ea4k9U_oHKjJGP3_hdFznpmLco,10287
7
+ mgplot/growth_plot.py,sha256=isE1olT94se2brcSQ919X5yyC00K4XptUxEhSA_6nOw,9151
8
+ mgplot/kw_type_checking.py,sha256=zS_KAzaNdnaa1xl0EhLaSLsAPzF7DP9qrydYBQ9q5RM,14699
9
+ mgplot/line_plot.py,sha256=BjdbkY5v70_qes6NBGUGyDsVXUpuUS7iP5MF3eeOagc,5589
10
+ mgplot/multi_plot.py,sha256=3HI93vNBF5HD94Hm7WyRqPm86wUQqjZpcYNupiY6TZI,10412
11
+ mgplot/postcovid_plot.py,sha256=ZzYIMdZCrECaVQNMkHo1nuB9Sx6hIy4m_hffYPSNX-Q,3622
12
+ mgplot/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
13
+ mgplot/revision_plot.py,sha256=kOp4CSo2UMFLpc0XL2v3PBehNj2tdOpKaPCdPP5uBH4,1809
14
+ mgplot/run_plot.py,sha256=JlCxEVJjXXmBkuvqZKoUalTlMxs8vCftr1deawCjek8,5871
15
+ mgplot/seastrend_plot.py,sha256=SWMxUVfDV2QhfRCttSlRmloM5Zyh0XbZXpZa67FJO0M,2124
16
+ mgplot/settings.py,sha256=52yTecdnjUSGQ9NNb3X-rPSkBg8poH1rzDSrAYG0Pws,4716
17
+ mgplot/summary_plot.py,sha256=ze4TFksPHf-oVLC-IwIfCKy5q64z2XOOCBaV4acnLKk,7318
18
+ mgplot/test.py,sha256=ZOlsltSC8E6vRfQ70tXz6CDkWyDpJZ2e9DNYcCxAsMs,709
19
+ mgplot/utilities.py,sha256=vjHaKxQU5o0ehuFTAtM65bpv4ljz4iS_eDkXEhPTaF0,7682
20
+ mgplot-0.1.0.dist-info/licenses/LICENSE,sha256=6ZyruOwF8J7mY25-vZHdOibtY8cSNLz0vWeH59TLhtw,1082
21
+ mgplot-0.1.0.dist-info/METADATA,sha256=425bBf2LSHqpeb7O3JcR6VRvCJtwp8ZMB9lUrYo28DI,1212
22
+ mgplot-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
+ mgplot-0.1.0.dist-info/top_level.txt,sha256=qv2cns5Wdn4prddz5KkfsMZc50nwh8clzbIkkLaQV3E,7
24
+ mgplot-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,8 @@
1
+ Copyright 2025 Bryan Palmer (Canberra Australia)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
+
@@ -0,0 +1 @@
1
+ mgplot