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/__init__.py +121 -0
- mgplot/bar_plot.py +107 -0
- mgplot/colors.py +199 -0
- mgplot/date_utils.py +324 -0
- mgplot/finalise_plot.py +335 -0
- mgplot/finalisers.py +364 -0
- mgplot/growth_plot.py +275 -0
- mgplot/kw_type_checking.py +460 -0
- mgplot/line_plot.py +178 -0
- mgplot/multi_plot.py +339 -0
- mgplot/postcovid_plot.py +106 -0
- mgplot/py.typed +1 -0
- mgplot/revision_plot.py +60 -0
- mgplot/run_plot.py +182 -0
- mgplot/seastrend_plot.py +74 -0
- mgplot/settings.py +164 -0
- mgplot/summary_plot.py +240 -0
- mgplot/test.py +31 -0
- mgplot/utilities.py +254 -0
- mgplot-0.1.0.dist-info/METADATA +53 -0
- mgplot-0.1.0.dist-info/RECORD +24 -0
- mgplot-0.1.0.dist-info/WHEEL +5 -0
- mgplot-0.1.0.dist-info/licenses/LICENSE +8 -0
- mgplot-0.1.0.dist-info/top_level.txt +1 -0
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,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
|