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
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
"""
|
|
2
|
+
kw_type_checking.py
|
|
3
|
+
- report_kwargs()
|
|
4
|
+
- validate_kwargs()
|
|
5
|
+
- validate_expected()
|
|
6
|
+
- limit_kwargs()
|
|
7
|
+
|
|
8
|
+
Private functions used for validating the arguments passed
|
|
9
|
+
to the major functions as **kwargs keyword arguments. This
|
|
10
|
+
allows us to warn when an unexpected argument appears or
|
|
11
|
+
when the value is not of the expected type.
|
|
12
|
+
|
|
13
|
+
This module is not intended to be used directly by the user.
|
|
14
|
+
|
|
15
|
+
The assumption is that most keyword arguments are one of the
|
|
16
|
+
following types:
|
|
17
|
+
- simple types (such as str, int, float, bool, complex, NoneType)
|
|
18
|
+
- Sequences (such as list, tuple, but excluding strings, and without
|
|
19
|
+
being infinitely recursive, like a list of lists of lists ...)
|
|
20
|
+
- Sets (such as set, frozenset)
|
|
21
|
+
- Mappings (such as dict)
|
|
22
|
+
|
|
23
|
+
Note: this means some Python types are only partially supported.
|
|
24
|
+
Others are unsupported, such as: generators, iterators, and
|
|
25
|
+
coroutines.
|
|
26
|
+
|
|
27
|
+
In order to check the **kwargs dictionary, we need to construct
|
|
28
|
+
a dictionary of expected keywords and their expected types.
|
|
29
|
+
An example follows.
|
|
30
|
+
|
|
31
|
+
expected = {
|
|
32
|
+
"arg1": str, # arg1 is expected to be a string
|
|
33
|
+
"arg2": (int, float), # arg2 is an int or a float
|
|
34
|
+
"arg3": (list, (bool,)), # arg3 is a list of Booleans
|
|
35
|
+
"arg4": (list, (float, int)), # arg4 is a list of floats or ints
|
|
36
|
+
"arg5": (Sequence, (float, int)), # a sequence of floats or ints
|
|
37
|
+
"arg6": (dict, (str, int)), # a dictionary with str keys and int values
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
Parsing Rules:
|
|
41
|
+
- If the type is a single type, it is used as is.
|
|
42
|
+
- if the type is a tuple of simple types, it is treated as a union.
|
|
43
|
+
- if the type of non-String Sequence, the subsequent tuple is a
|
|
44
|
+
union of Sequence member types.
|
|
45
|
+
- eg, (list, (float, int)) is a list of floats or ints.
|
|
46
|
+
- eg, (int, float, list, (int, float)) is an int, a float or
|
|
47
|
+
a list of ints or floats.
|
|
48
|
+
- if the type of a Mapping, the subsequent 2-part tuple is treated
|
|
49
|
+
as the types of the keys and values of the Mapping.
|
|
50
|
+
- eg, (dict, (str, int)) is a dictionary with str keys and int values.
|
|
51
|
+
- eg, (dict, (str, (int, float))) is a dictionary with str keys and
|
|
52
|
+
an int or float values.
|
|
53
|
+
- eg, (dict, (str, list, (int, float)), (list, (int, float))) is a
|
|
54
|
+
dictionary with str keys and a list of ints or floats as values.
|
|
55
|
+
- Sets are treated like Sequences.
|
|
56
|
+
|
|
57
|
+
Limitations:
|
|
58
|
+
- cannot specify multiple types of Sequence as a type - for example
|
|
59
|
+
((list, tuple), int) - but you can specify (Sequence, int) which
|
|
60
|
+
will match list and tuple types. or you might do it as follows:
|
|
61
|
+
(list, (int, float), tuple, (int, float)).
|
|
62
|
+
- strings, bytearrays, bytes are treated as simple types, not Sequences.
|
|
63
|
+
- You cannot use generators or iterators as types, they would be
|
|
64
|
+
consumed in the testing.
|
|
65
|
+
- Sequence, Set and Mapping must be imported from collections.abc
|
|
66
|
+
and not from the older typing module. A world of pain awaits
|
|
67
|
+
if you do.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
# --- imports
|
|
71
|
+
from typing import Any, Final, Union, Optional
|
|
72
|
+
from typing import Sequence as TypingSequence
|
|
73
|
+
from typing import Set as TypingSet
|
|
74
|
+
from typing import Iterable as TypingIterable
|
|
75
|
+
from typing import Mapping as TypingMapping
|
|
76
|
+
|
|
77
|
+
from collections.abc import Sequence, Set # Iterable and Sized
|
|
78
|
+
from collections.abc import Mapping
|
|
79
|
+
from collections.abc import Iterable, Sized, Container, Callable, Generator, Iterator
|
|
80
|
+
|
|
81
|
+
import textwrap
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# --- constants
|
|
85
|
+
type NestedTypeTuple = tuple[type | NestedTypeTuple, ...] # recursive type
|
|
86
|
+
type ExpectedTypeDict = dict[str, type | NestedTypeTuple]
|
|
87
|
+
|
|
88
|
+
NOT_SEQUENCE: Final[tuple[type, ...]] = (str, bytearray, bytes, memoryview)
|
|
89
|
+
REPORT_KWARGS: Final[str] = "report_kwargs" # special case
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# --- module-scoped global variable
|
|
93
|
+
module_testing: bool = False
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# --- functions
|
|
97
|
+
|
|
98
|
+
# === keyword argument reporting ===
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def report_kwargs(
|
|
102
|
+
called_from: str,
|
|
103
|
+
**kwargs,
|
|
104
|
+
) -> None:
|
|
105
|
+
"""
|
|
106
|
+
Dump the received keyword arguments to the console.
|
|
107
|
+
Useful for debugging purposes.
|
|
108
|
+
|
|
109
|
+
Arguments:
|
|
110
|
+
- called_from: str - the name of the function that called this
|
|
111
|
+
function, used for debugging.
|
|
112
|
+
- **kwargs - the keyword arguments to be reported, but only if
|
|
113
|
+
the REPORT_KWARGS key is present and set to True.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
if kwargs.get(REPORT_KWARGS, False):
|
|
117
|
+
wrapped = textwrap.fill(str(kwargs), width=79)
|
|
118
|
+
print(f"{called_from} kwargs:\n{wrapped}\n".strip())
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# === limit kwargs to those in an approved list
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def limit_kwargs(
|
|
125
|
+
expected: ExpectedTypeDict,
|
|
126
|
+
**kwargs,
|
|
127
|
+
) -> dict[str, Any]:
|
|
128
|
+
"""
|
|
129
|
+
Limit the keyword arguments to those in the expected dict.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
return {k: v for k, v in kwargs.items() if k in expected or k == REPORT_KWARGS}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# === Keyword expectation validation ===
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _check_expected_tuple(
|
|
139
|
+
t: NestedTypeTuple,
|
|
140
|
+
) -> bool:
|
|
141
|
+
|
|
142
|
+
post_mapping = post_sequence = False
|
|
143
|
+
empty = True
|
|
144
|
+
for element in t:
|
|
145
|
+
empty = False
|
|
146
|
+
|
|
147
|
+
if isinstance(element, type):
|
|
148
|
+
if post_mapping or post_sequence:
|
|
149
|
+
return False
|
|
150
|
+
if issubclass(element, NOT_SEQUENCE):
|
|
151
|
+
post_mapping = post_sequence = False
|
|
152
|
+
continue
|
|
153
|
+
if issubclass(element, (Sequence, Set)):
|
|
154
|
+
post_sequence = True
|
|
155
|
+
continue
|
|
156
|
+
if issubclass(element, Mapping):
|
|
157
|
+
post_mapping = True
|
|
158
|
+
continue
|
|
159
|
+
post_mapping = post_sequence = False
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
if isinstance(element, tuple):
|
|
163
|
+
if not (post_mapping or post_sequence):
|
|
164
|
+
return False
|
|
165
|
+
if post_sequence:
|
|
166
|
+
check = _check_expectations(element)
|
|
167
|
+
if not check:
|
|
168
|
+
return False
|
|
169
|
+
post_sequence = False
|
|
170
|
+
if post_mapping:
|
|
171
|
+
if len(element) != 2:
|
|
172
|
+
return False
|
|
173
|
+
check = _check_expectations(element[0]) and _check_expectations(
|
|
174
|
+
element[1]
|
|
175
|
+
)
|
|
176
|
+
if not check:
|
|
177
|
+
return False
|
|
178
|
+
post_mapping = False
|
|
179
|
+
if empty:
|
|
180
|
+
return False
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _check_expected_type(t: type) -> bool:
|
|
185
|
+
"""
|
|
186
|
+
Check t is an acceptable stand alone type
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
if issubclass(t, NOT_SEQUENCE):
|
|
190
|
+
return True
|
|
191
|
+
if issubclass(t, (Sequence, Set, Mapping)):
|
|
192
|
+
return False
|
|
193
|
+
return True
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _check_expectations(
|
|
197
|
+
t: type | NestedTypeTuple,
|
|
198
|
+
) -> bool:
|
|
199
|
+
"""
|
|
200
|
+
Check t is a type or a tuple of types.
|
|
201
|
+
|
|
202
|
+
Where a Sequence or Mapping type is found, check that
|
|
203
|
+
the subsequent tuple contains valid member types.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
# --- simple case
|
|
207
|
+
if isinstance(t, type):
|
|
208
|
+
return _check_expected_type(t)
|
|
209
|
+
|
|
210
|
+
# --- more challenging case
|
|
211
|
+
if isinstance(t, tuple):
|
|
212
|
+
return _check_expected_tuple(t)
|
|
213
|
+
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def validate_expected(
|
|
218
|
+
expected: ExpectedTypeDict,
|
|
219
|
+
called_from: str,
|
|
220
|
+
) -> None:
|
|
221
|
+
"""
|
|
222
|
+
Check the expected types dictionary is properly formed.
|
|
223
|
+
This function should be used on all the expected types
|
|
224
|
+
dictionaries in the module.
|
|
225
|
+
|
|
226
|
+
It is not intended to be used by the user.
|
|
227
|
+
|
|
228
|
+
This function raises an ValueError exception if the expected
|
|
229
|
+
types dictionary is malformed.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
def check_members(key: str, t: type | NestedTypeTuple) -> str:
|
|
233
|
+
"""
|
|
234
|
+
Recursively check each element of the NestedTypeTuple.
|
|
235
|
+
to ensure it is a type or a tuple of types. Returns a string
|
|
236
|
+
description of any problems found.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
problems = ""
|
|
240
|
+
# --- start with the things that are types
|
|
241
|
+
if t in (Iterable, Sized, Container, Callable, Generator, Iterator):
|
|
242
|
+
# note: these collections.abc types *are* types
|
|
243
|
+
problems += f"{key}: the collections.abc type {t} in {called_from} is unsupported.\n"
|
|
244
|
+
elif t in (Any,):
|
|
245
|
+
# Any is also an instance of type
|
|
246
|
+
problems += f"{key}: please use 'object' rather than 'typing.Any'.\n"
|
|
247
|
+
elif isinstance(t, type):
|
|
248
|
+
pass # Fantastic!
|
|
249
|
+
# --- then the things that are not types
|
|
250
|
+
elif isinstance(t, tuple):
|
|
251
|
+
for element in t:
|
|
252
|
+
problems += check_members(key, element)
|
|
253
|
+
elif t in (
|
|
254
|
+
# note: these typing types *are not* types
|
|
255
|
+
TypingSequence,
|
|
256
|
+
TypingSet,
|
|
257
|
+
TypingMapping,
|
|
258
|
+
TypingIterable,
|
|
259
|
+
Union,
|
|
260
|
+
Optional,
|
|
261
|
+
):
|
|
262
|
+
problems += (
|
|
263
|
+
f"{key}: Only use the collection.abc types: {t} in {called_from}.\n"
|
|
264
|
+
)
|
|
265
|
+
else:
|
|
266
|
+
problems += f"{key}: Malformed typing '{t}' in {called_from}.\n"
|
|
267
|
+
return problems
|
|
268
|
+
|
|
269
|
+
problems = ""
|
|
270
|
+
for key, value in expected.items():
|
|
271
|
+
if not isinstance(key, str):
|
|
272
|
+
problems += f"Key '{key}' is not a string - {called_from=}.\n"
|
|
273
|
+
continue
|
|
274
|
+
problems += check_members(key, value)
|
|
275
|
+
if not _check_expectations(value):
|
|
276
|
+
problems += f"{key}: Malformed '{value}' in {called_from}.\n"
|
|
277
|
+
if problems:
|
|
278
|
+
# Other than testing, we want to raise an exception here
|
|
279
|
+
statement = (
|
|
280
|
+
"Expected types validation failed "
|
|
281
|
+
+ f"(this is an internal package error):\n{problems}"
|
|
282
|
+
)
|
|
283
|
+
if not module_testing:
|
|
284
|
+
raise ValueError(statement)
|
|
285
|
+
print(statement)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# === keyword validation: (1) if expected, (2) of the right type ===
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _check_tuple(
|
|
292
|
+
value: Any,
|
|
293
|
+
typeinfo: NestedTypeTuple, # we know this is a tuple
|
|
294
|
+
) -> bool:
|
|
295
|
+
"""
|
|
296
|
+
Check the value against the expected tuple type.
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
check_sequence = check_mapping = False
|
|
300
|
+
for thistype in typeinfo:
|
|
301
|
+
|
|
302
|
+
if check_mapping or check_sequence: # the guard-rail
|
|
303
|
+
if not isinstance(thistype, tuple):
|
|
304
|
+
return False
|
|
305
|
+
|
|
306
|
+
if check_sequence and isinstance(thistype, tuple):
|
|
307
|
+
for v in value:
|
|
308
|
+
check = _type_check_kwargs(v, thistype)
|
|
309
|
+
if not check:
|
|
310
|
+
check_sequence = False
|
|
311
|
+
continue
|
|
312
|
+
return True
|
|
313
|
+
|
|
314
|
+
if check_mapping and isinstance(thistype, tuple):
|
|
315
|
+
for k, v in value.items():
|
|
316
|
+
check = _type_check_kwargs(k, thistype[0]) and _type_check_kwargs(
|
|
317
|
+
v, thistype[1]
|
|
318
|
+
)
|
|
319
|
+
if not check:
|
|
320
|
+
check_mapping = False
|
|
321
|
+
continue
|
|
322
|
+
return True
|
|
323
|
+
|
|
324
|
+
if isinstance(thistype, type) and isinstance(value, thistype):
|
|
325
|
+
if thistype in NOT_SEQUENCE:
|
|
326
|
+
return True
|
|
327
|
+
if issubclass(thistype, (Sequence, Set)):
|
|
328
|
+
check_sequence = True
|
|
329
|
+
continue
|
|
330
|
+
if issubclass(thistype, Mapping):
|
|
331
|
+
check_mapping = True
|
|
332
|
+
continue
|
|
333
|
+
return True
|
|
334
|
+
|
|
335
|
+
return False
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _type_check_kwargs(
|
|
339
|
+
value: Any,
|
|
340
|
+
typeinfo: type | NestedTypeTuple,
|
|
341
|
+
) -> bool:
|
|
342
|
+
"""
|
|
343
|
+
Check the type of the value against the expected type.
|
|
344
|
+
"""
|
|
345
|
+
|
|
346
|
+
# --- the simple case
|
|
347
|
+
if isinstance(typeinfo, type):
|
|
348
|
+
return isinstance(value, typeinfo)
|
|
349
|
+
|
|
350
|
+
# --- complex
|
|
351
|
+
if isinstance(typeinfo, tuple):
|
|
352
|
+
return _check_tuple(value, typeinfo)
|
|
353
|
+
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def validate_kwargs(
|
|
358
|
+
expected: ExpectedTypeDict,
|
|
359
|
+
called_from: str,
|
|
360
|
+
**kwargs,
|
|
361
|
+
) -> None:
|
|
362
|
+
"""
|
|
363
|
+
This function is used to validate the keyword arguments.
|
|
364
|
+
To check we don't have unexpected keyword arguments, and
|
|
365
|
+
to check that the values are of the expected type.
|
|
366
|
+
|
|
367
|
+
Arguments
|
|
368
|
+
- expected: ExpectedTypeDict - the expected keyword arguments and their types.
|
|
369
|
+
- called_from: str - the name of the function that called this function,
|
|
370
|
+
- **kwargs - the keyword arguments to be validated.
|
|
371
|
+
|
|
372
|
+
It is not intended to be used by the user.
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
problems = ""
|
|
376
|
+
for key, value in kwargs.items():
|
|
377
|
+
if key == REPORT_KWARGS and isinstance(value, bool):
|
|
378
|
+
# This is a special case - and always okay if the value is boolean
|
|
379
|
+
continue
|
|
380
|
+
if key not in expected:
|
|
381
|
+
problems += (
|
|
382
|
+
f"{key}: unexpected keyword argument with {value=}in {called_from}.\n"
|
|
383
|
+
)
|
|
384
|
+
continue
|
|
385
|
+
if not _type_check_kwargs(value, expected[key]):
|
|
386
|
+
problems += (
|
|
387
|
+
f"{key}: with {value=} had the type "
|
|
388
|
+
f"'{type(value)}' in {called_from}. Expected: {expected[key]}\n"
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
if problems:
|
|
392
|
+
# don't raise an exception - just warn instead
|
|
393
|
+
statement = f"Keyword argument validation issues:\n{problems}"
|
|
394
|
+
print(statement)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
# --- test code
|
|
398
|
+
if __name__ == "__main__":
|
|
399
|
+
# Test the type_check_kwargs function
|
|
400
|
+
module_testing = True # pylint: disable=invalid-name
|
|
401
|
+
|
|
402
|
+
# --- test the validate_expected() function
|
|
403
|
+
expected_gb: ExpectedTypeDict = {
|
|
404
|
+
# - these ones should pass
|
|
405
|
+
"good1": str,
|
|
406
|
+
"good2": (int, float),
|
|
407
|
+
"good3": bool,
|
|
408
|
+
"good4": (list, (float, int)),
|
|
409
|
+
"good5": (Sequence, (float, int)),
|
|
410
|
+
"good6": (dict, (str, int)),
|
|
411
|
+
"good7": (int, float, list, (int, float)),
|
|
412
|
+
"good8": (dict, (str, (int, float))),
|
|
413
|
+
"good9": (set, (str,)),
|
|
414
|
+
"good10": (frozenset, (str,), int, complex),
|
|
415
|
+
"good11": (dict, ((str, int), (int, float))),
|
|
416
|
+
"good12": (list, (dict, ((str, int), (list, (complex,))))),
|
|
417
|
+
"good13": (Sequence, (int, float), Set, (int, float)),
|
|
418
|
+
"good14": (Sequence, (str,)),
|
|
419
|
+
# - these ones should fail
|
|
420
|
+
"bad1": list,
|
|
421
|
+
"bad2": (int, (str, bool)),
|
|
422
|
+
"bad3": tuple(),
|
|
423
|
+
"bad4": (int, float, set, bool, float),
|
|
424
|
+
"bad5": (list, float),
|
|
425
|
+
"bad6": ((list, tuple), (int, float)),
|
|
426
|
+
"bad7": (dict, (str, int), (int, float)),
|
|
427
|
+
"bad8": (TypingSequence, (int, float)),
|
|
428
|
+
# "bad9": (list, [int, float]),
|
|
429
|
+
"bad10": (dict, (str,)),
|
|
430
|
+
"bad11": (Iterable, (int, float)),
|
|
431
|
+
# "bad12": Any,
|
|
432
|
+
}
|
|
433
|
+
validate_expected(expected_gb, "testing")
|
|
434
|
+
|
|
435
|
+
# --- test the validate_kwargs() function
|
|
436
|
+
# bad means the KWARGS are not of the expected type
|
|
437
|
+
|
|
438
|
+
expected_kw: ExpectedTypeDict = {
|
|
439
|
+
"good_1": str,
|
|
440
|
+
"good_2": (Sequence, (int, float), int, float),
|
|
441
|
+
"good_3": (int, float, Sequence, (int, float)),
|
|
442
|
+
"good_4": (Sequence, (str,)),
|
|
443
|
+
"bad_1": str,
|
|
444
|
+
"bad_2": (int, float),
|
|
445
|
+
}
|
|
446
|
+
validate_expected(expected_kw, "test")
|
|
447
|
+
|
|
448
|
+
kwargs_test = {
|
|
449
|
+
# - these ones should pass
|
|
450
|
+
"good_1": "hello",
|
|
451
|
+
"good_2": [1, 2, 3],
|
|
452
|
+
"good_3": (),
|
|
453
|
+
"good_4": ["fred", "bill", "janice"],
|
|
454
|
+
"report_kwargs": True, # special case
|
|
455
|
+
# - these ones should fail
|
|
456
|
+
"missing": "hello",
|
|
457
|
+
"bad_1": 3.14,
|
|
458
|
+
"bad_2": (3, 4),
|
|
459
|
+
}
|
|
460
|
+
validate_kwargs(expected_kw, "test", **kwargs_test)
|
mgplot/line_plot.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""
|
|
2
|
+
line_plot.py:
|
|
3
|
+
Plot a series or a dataframe with lines.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# --- imports
|
|
7
|
+
from typing import Any
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
import matplotlib.pyplot as plt
|
|
10
|
+
from pandas import DataFrame, Period
|
|
11
|
+
|
|
12
|
+
from mgplot.settings import DataT, get_setting
|
|
13
|
+
from mgplot.kw_type_checking import (
|
|
14
|
+
report_kwargs,
|
|
15
|
+
validate_kwargs,
|
|
16
|
+
validate_expected,
|
|
17
|
+
ExpectedTypeDict,
|
|
18
|
+
)
|
|
19
|
+
from mgplot.utilities import (
|
|
20
|
+
apply_defaults,
|
|
21
|
+
get_color_list,
|
|
22
|
+
get_axes,
|
|
23
|
+
annotate_series,
|
|
24
|
+
constrain_data,
|
|
25
|
+
check_clean_timeseries,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# --- constants
|
|
30
|
+
DATA = "data"
|
|
31
|
+
AX = "ax"
|
|
32
|
+
STYLE, WIDTH, COLOR, ALPHA = "style", "width", "color", "alpha"
|
|
33
|
+
ANNOTATE = "annotate"
|
|
34
|
+
ROUNDING = "rounding"
|
|
35
|
+
FONTSIZE = "fontsize"
|
|
36
|
+
DROPNA = "dropna"
|
|
37
|
+
DRAWSTYLE, MARKER, MARKERSIZE = "drawstyle", "marker", "markersize"
|
|
38
|
+
PLOT_FROM = "plot_from" # used to constrain the data to a starting point
|
|
39
|
+
LEGEND = "legend"
|
|
40
|
+
|
|
41
|
+
LP_KW_TYPES: ExpectedTypeDict = {
|
|
42
|
+
AX: (plt.Axes, type(None)),
|
|
43
|
+
STYLE: (str, Sequence, (str,)),
|
|
44
|
+
WIDTH: (float, int, Sequence, (float, int)),
|
|
45
|
+
COLOR: (str, Sequence, (str,)),
|
|
46
|
+
ALPHA: (float, Sequence, (float,)),
|
|
47
|
+
DRAWSTYLE: (str, Sequence, (str,), type(None)),
|
|
48
|
+
MARKER: (str, Sequence, (str,), type(None)),
|
|
49
|
+
MARKERSIZE: (float, Sequence, (float,), int, type(None)),
|
|
50
|
+
DROPNA: (bool, Sequence, (bool,)),
|
|
51
|
+
ANNOTATE: (bool, Sequence, (bool,)),
|
|
52
|
+
ROUNDING: (Sequence, (bool, int), int, bool, type(None)),
|
|
53
|
+
FONTSIZE: (Sequence, (str, int), str, int, type(None)),
|
|
54
|
+
PLOT_FROM: (int, Period, type(None)),
|
|
55
|
+
LEGEND: (dict, (str, object), bool, type(None)),
|
|
56
|
+
}
|
|
57
|
+
validate_expected(LP_KW_TYPES, "line_plot")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# --- functions
|
|
61
|
+
def _get_style_width_color_etc(
|
|
62
|
+
item_count, num_data_points, **kwargs
|
|
63
|
+
) -> tuple[dict[str, list | tuple], dict[str, Any]]:
|
|
64
|
+
"""
|
|
65
|
+
Get the plot-line attributes arguemnts.
|
|
66
|
+
Returns a dictionary of lists of attributes for each line, and
|
|
67
|
+
a modified kwargs dictionary.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
data_point_thresh = 151
|
|
71
|
+
defaults: dict[str, Any] = {
|
|
72
|
+
STYLE: "-",
|
|
73
|
+
WIDTH: (
|
|
74
|
+
get_setting("line_normal")
|
|
75
|
+
if num_data_points > data_point_thresh
|
|
76
|
+
else get_setting("line_wide")
|
|
77
|
+
),
|
|
78
|
+
COLOR: kwargs.get(COLOR, get_color_list(item_count)),
|
|
79
|
+
ALPHA: 1.0,
|
|
80
|
+
DRAWSTYLE: None,
|
|
81
|
+
MARKER: None,
|
|
82
|
+
MARKERSIZE: 10,
|
|
83
|
+
DROPNA: True,
|
|
84
|
+
ANNOTATE: False,
|
|
85
|
+
ROUNDING: True,
|
|
86
|
+
FONTSIZE: "small",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return apply_defaults(item_count, defaults, kwargs)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def line_plot(data: DataT, **kwargs) -> plt.Axes:
|
|
93
|
+
"""
|
|
94
|
+
Build a single plot from the data passed in.
|
|
95
|
+
This can be a single- or multiple-line plot.
|
|
96
|
+
Return the axes object for the build.
|
|
97
|
+
|
|
98
|
+
Agruments:
|
|
99
|
+
- data: DataFrame | Series - data to plot
|
|
100
|
+
- kwargs:
|
|
101
|
+
- ax: plt.Axes | None - axes to plot on (optional)
|
|
102
|
+
- dropna: bool | list[bool] - whether to delete NAs frm the
|
|
103
|
+
data before plotting [optional]
|
|
104
|
+
- color: str | list[str] - line colors.
|
|
105
|
+
- width: float | list[float] - line widths [optional].
|
|
106
|
+
- style: str | list[str] - line styles [optional].
|
|
107
|
+
- alpha: float | list[float] - line transparencies [optional].
|
|
108
|
+
- marker: str | list[str] - line markers [optional].
|
|
109
|
+
- marker_size: float | list[float] - line marker sizes [optional].
|
|
110
|
+
- annotate: bool | list[bool] - whether to annotate a series.
|
|
111
|
+
- rounding: int | bool | list[int | bool] - number of decimal places
|
|
112
|
+
to round an annotation. If True, a default between 0 and 2 is
|
|
113
|
+
used.
|
|
114
|
+
- fontsize: int | str | list[int | str] - font size for the
|
|
115
|
+
annotation.
|
|
116
|
+
- drawstyle: str | list[str] - matplotlib line draw styles.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
- axes: plt.Axes - the axes object for the plot
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
# sanity checks
|
|
123
|
+
report_kwargs(called_from="line_plot", **kwargs)
|
|
124
|
+
data = check_clean_timeseries(data)
|
|
125
|
+
validate_kwargs(LP_KW_TYPES, called_from="line_plot", **kwargs)
|
|
126
|
+
|
|
127
|
+
# the data to be plotted:
|
|
128
|
+
df = DataFrame(data) # really we are only plotting DataFrames
|
|
129
|
+
df, kwargs = constrain_data(df, **kwargs)
|
|
130
|
+
if df.empty:
|
|
131
|
+
print("Warning: No data to plot.")
|
|
132
|
+
|
|
133
|
+
# get the arguments for each line we will plot ...
|
|
134
|
+
item_count = len(df.columns)
|
|
135
|
+
num_data_points = len(df)
|
|
136
|
+
swce, kwargs = _get_style_width_color_etc(item_count, num_data_points, **kwargs)
|
|
137
|
+
|
|
138
|
+
# Let's plot
|
|
139
|
+
axes, kwargs = get_axes(**kwargs) # get the axes to plot on
|
|
140
|
+
|
|
141
|
+
for i, column in enumerate(df.columns):
|
|
142
|
+
series = df[column]
|
|
143
|
+
series = series.dropna() if DROPNA in swce and swce[DROPNA][i] else series
|
|
144
|
+
if series.empty or series.isna().all():
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
axes = series.plot(
|
|
148
|
+
ls=swce[STYLE][i],
|
|
149
|
+
lw=swce[WIDTH][i],
|
|
150
|
+
color=swce[COLOR][i],
|
|
151
|
+
alpha=swce[ALPHA][i],
|
|
152
|
+
marker=swce[MARKER][i],
|
|
153
|
+
ms=swce[MARKERSIZE][i],
|
|
154
|
+
drawstyle=swce[DRAWSTYLE][i],
|
|
155
|
+
ax=axes,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if swce[ANNOTATE][i] is None or not swce[ANNOTATE][i]:
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
annotate_series(
|
|
162
|
+
series,
|
|
163
|
+
axes,
|
|
164
|
+
rounding=swce[ROUNDING][i],
|
|
165
|
+
color=swce[COLOR][i],
|
|
166
|
+
fontsize=swce[FONTSIZE][i],
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# add a legend if requested
|
|
170
|
+
if len(df.columns) > 1:
|
|
171
|
+
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)
|
|
177
|
+
|
|
178
|
+
return axes
|