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/multi_plot.py ADDED
@@ -0,0 +1,339 @@
1
+ """
2
+ multi_plot.py
3
+
4
+ This module provides a function to create multiple plots
5
+ from a single dataset
6
+ - multi_start()
7
+ - multi_column()
8
+ And to chain a plotting function with the finalise_plot() function.
9
+ - plot_then_finalise()
10
+
11
+ Underlying assumptions:
12
+ - every plot function:
13
+ - has a mandatory data: DataFrame | Series argument first (noting
14
+ that some plotting functions only work with Series data, and they
15
+ will raise an error if they are passed a DataFrame).
16
+ - accepts an optional plot_from: int | Period keyword argument
17
+ - returns a matplotlib Axes object
18
+ - the multi functions (all in this module)
19
+ - have a mandatory data: DataFrame | Series argument
20
+ - have a mandatory function: Callable | list[Callable] argument
21
+ and otherwise pass their kwargs to the next function
22
+ when execution is transferred to the next function.
23
+ - the multi functions can be chained together.
24
+ - return None.
25
+
26
+ And why are these three public functions all in the same modules?
27
+ - They all work with the same underlying assumptions.
28
+ - They all take a function argument/list to which execution is
29
+ passed.
30
+ - They all use the same underlying logic to extract the first
31
+ function from the function argument, and to store any remaining
32
+ functions in the kwargs['function'] argument.
33
+
34
+ Note: rather than pass the kwargs dict directly, we will re-pack-it
35
+
36
+ """
37
+
38
+ # --- imports
39
+ from typing import Any, Callable, Final
40
+ from collections.abc import Iterable
41
+ from pandas import Period, DataFrame, Series, period_range
42
+ from numpy import random
43
+
44
+ from mgplot.kw_type_checking import (
45
+ limit_kwargs,
46
+ ExpectedTypeDict,
47
+ report_kwargs,
48
+ )
49
+ from mgplot.finalise_plot import finalise_plot, FINALISE_KW_TYPES
50
+ from mgplot.settings import DataT
51
+ from mgplot.test import prepare_for_test
52
+ from mgplot.utilities import check_clean_timeseries
53
+
54
+ from mgplot.line_plot import line_plot, LP_KW_TYPES
55
+ from mgplot.bar_plot import bar_plot, BAR_PLOT_KW_TYPES
56
+ from mgplot.seastrend_plot import seastrend_plot
57
+ from mgplot.postcovid_plot import postcovid_plot
58
+ from mgplot.revision_plot import revision_plot, REVISION_KW_TYPES
59
+ from mgplot.run_plot import run_plot, RUN_KW_TYPES
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
+
63
+
64
+ # --- constants
65
+ EXPECTED_CALLABLES: Final[dict[Callable, ExpectedTypeDict]] = {
66
+ # used by plot_then_finalise() to (1) check the target function
67
+ # is one of the expected functions, and (2) to limit the kwargs
68
+ # passed on, to the expected keyword arguments for that function.
69
+ line_plot: LP_KW_TYPES,
70
+ bar_plot: BAR_PLOT_KW_TYPES,
71
+ seastrend_plot: LP_KW_TYPES, # just calls line_plot under the hood
72
+ postcovid_plot: LP_KW_TYPES, # just calls line_plot under the hood
73
+ revision_plot: REVISION_KW_TYPES,
74
+ run_plot: LP_KW_TYPES | RUN_KW_TYPES,
75
+ summary_plot: SUMMARY_KW_TYPES,
76
+ series_growth_plot: GROWTH_KW_TYPES,
77
+ raw_growth_plot: GROWTH_KW_TYPES,
78
+ }
79
+
80
+
81
+ # --- private functions
82
+ def first_unchain(
83
+ function: Callable | list[Callable],
84
+ **kwargs,
85
+ ) -> tuple[Callable, dict[str, Any]]:
86
+ """
87
+ Extract the first Callable from function (which may be
88
+ a stand alone Callable or a nonr-empty list of Callables).
89
+ Store the remaining Callables in kwargs['function'].
90
+ This allows for chaining multiple functions together.
91
+
92
+ Parameters
93
+ - kwargs - keyword arguments
94
+
95
+ Returns a tuple containing the first function and the updated kwargs.
96
+ if function is a list of Callables, the first function will be removed
97
+ from the the list, and the remaining functions will be stored in a
98
+ list under the key "function" in kwargs.
99
+
100
+ Raises ValueError if function is an empty list.
101
+
102
+ Not intended for direct use by the user.
103
+ """
104
+
105
+ error_msg = "function must be a Callable or a non-empty list of Callables"
106
+
107
+ if isinstance(function, list):
108
+ if len(function) == 0:
109
+ raise ValueError(error_msg)
110
+ first, *rest = function
111
+ elif callable(function):
112
+ first, rest = function, []
113
+ else:
114
+ raise ValueError(error_msg)
115
+
116
+ if rest:
117
+ kwargs["function"] = rest
118
+
119
+ return first, kwargs
120
+
121
+
122
+ # --- public functions
123
+ def plot_then_finalise(
124
+ data: DataT,
125
+ function: Callable | list[Callable],
126
+ **kwargs,
127
+ ) -> None:
128
+ """
129
+ Chain a plotting function with the finalise_plot() function.
130
+ This is designed to be the last function in a chain.
131
+
132
+ Parameters
133
+ - data: Series | DataFrame - The data to be plotted.
134
+ - function: Callable | list[Callable] - The plotting function
135
+ to be used.
136
+ - **kwargs: Additional keyword arguments to be passed to
137
+ the plotting function, and then the finalise_plot() function.
138
+
139
+ Returns None.
140
+ """
141
+
142
+ # --- sanity checks
143
+ report_kwargs(called_from="plot_then_finalise", **kwargs)
144
+ data = check_clean_timeseries(data)
145
+
146
+ # --- check the function argument
147
+ first, kwargs_ = first_unchain(function, **kwargs)
148
+
149
+ if first in EXPECTED_CALLABLES:
150
+ expected = EXPECTED_CALLABLES[first]
151
+ plot_kwargs = limit_kwargs(expected, **kwargs)
152
+ else:
153
+ # this is an unexpected Callable, so we will give it a try
154
+ print(f"Unknown proposed function: {first}; nonetheless, will give it a try.")
155
+ plot_kwargs = kwargs_
156
+
157
+ # --- call the first function with the data and kwargs
158
+
159
+ axes = first(data, **plot_kwargs)
160
+
161
+ # --- finalise the plot
162
+ fp_kwargs = limit_kwargs(FINALISE_KW_TYPES, **kwargs)
163
+ finalise_plot(axes, **fp_kwargs)
164
+
165
+
166
+ def multi_start(
167
+ data: DataT,
168
+ function: Callable | list[Callable],
169
+ starts: Iterable[None | Period | int],
170
+ **kwargs,
171
+ ) -> None:
172
+ """
173
+ Create multiple plots with different starting points.
174
+ Each plot will start from the specified starting point.
175
+
176
+ Parameters
177
+ - data: Series | DataFrame - The data to be plotted.
178
+ - function: Callable | list[Callable] - The plotting function
179
+ to be used.
180
+ - starts: Iterable[Period | int | None] - The starting points
181
+ for each plot (None means use the entire data).
182
+ - **kwargs: Additional keyword arguments to be passed to
183
+ the plotting function.
184
+
185
+ Returns None.
186
+
187
+ Raises
188
+ - ValueError if the starts is not an iterable of None, Period or int.
189
+
190
+ Note: kwargs['tag'] is used to create a unique tag for each plot.
191
+ """
192
+
193
+ # --- sanity checks
194
+ report_kwargs(called_from="multi_start", **kwargs)
195
+ data = check_clean_timeseries(data)
196
+ if not isinstance(starts, Iterable):
197
+ raise ValueError("starts must be an iterable of None, Period or int")
198
+
199
+ # --- check the function argument
200
+ original_tag: Final[str] = kwargs.get("tag", "")
201
+ first, kwargs = first_unchain(function, **kwargs)
202
+
203
+ # --- iterate over the starts
204
+ for i, start in enumerate(starts):
205
+ kw = kwargs.copy() # copy to avoid modifying the original kwargs
206
+ this_tag = f"{original_tag}_{i}"
207
+ kw["tag"] = this_tag
208
+ kw["plot_from"] = start # rely on plotting function to constrain the data
209
+ first(data, **kw)
210
+
211
+
212
+ def multi_column(
213
+ data: DataFrame,
214
+ function: Callable | list[Callable],
215
+ **kwargs,
216
+ ) -> None:
217
+ """
218
+ Create multiple plots, one for each column in a DataFrame.
219
+ The plot title will be the column name.
220
+
221
+ Parameters
222
+ - data: DataFrame - The data to be plotted.
223
+ - function: Callable - The plotting function to be used.
224
+ - **kwargs: Additional keyword arguments to be passed to
225
+ the plotting function.
226
+
227
+ Returns None.
228
+ """
229
+
230
+ # --- sanity checks
231
+ report_kwargs(called_from="multi_column", **kwargs)
232
+ data = check_clean_timeseries(data)
233
+
234
+ # --- check the function argument
235
+ title_stem = kwargs.get("title", "")
236
+ tag: Final[str] = kwargs.get("tag", "")
237
+ first, kwargs = first_unchain(function, **kwargs)
238
+
239
+ # --- iterate over the columns
240
+ for i, col in enumerate(data.columns):
241
+
242
+ series = data[[col]]
243
+ kwargs["title"] = f"{title_stem}{col}" if title_stem else col
244
+
245
+ this_tag = f"_{tag}_{i}".replace("__", "_")
246
+ kwargs["tag"] = this_tag
247
+
248
+ first(series, **kwargs)
249
+
250
+
251
+ # --- test
252
+ if __name__ == "__main__":
253
+
254
+ prepare_for_test("multi_plot")
255
+
256
+ dates = period_range("2020-01-01", "2020-12-31", freq="D")
257
+ df = DataFrame(
258
+ {
259
+ "Series 1": random.rand(len(dates)),
260
+ "Series 2": random.rand(len(dates)),
261
+ "Series 3": random.rand(len(dates)),
262
+ },
263
+ index=dates,
264
+ )
265
+
266
+ # Test multi_start
267
+ multi_start(
268
+ df,
269
+ function=[plot_then_finalise, line_plot],
270
+ starts=[None, 50, 100, Period("2020-06-01", freq="D")],
271
+ title="Test Multi Start: ",
272
+ tag="tag_test",
273
+ )
274
+
275
+ # Test multi_column
276
+ multi_column(
277
+ df, function=[plot_then_finalise, line_plot], title="Test Multi Column: "
278
+ )
279
+
280
+ # Test Test Multi Column / Multi start
281
+ multi_column(
282
+ df,
283
+ [multi_start, plot_then_finalise, line_plot],
284
+ title="Test Multi Column / Multi start: ",
285
+ starts=[None, 180],
286
+ verbose=False,
287
+ )
288
+
289
+ # bar plot
290
+ # Test 1
291
+ series_ = Series([1, 2, 3, 4, 5], index=list("ABCDE"))
292
+ plot_then_finalise(
293
+ series_,
294
+ function=bar_plot,
295
+ title="Bar Plot Example 1a",
296
+ xlabel="X-axis Label",
297
+ ylabel="Y-axis Label",
298
+ rotation=45,
299
+ )
300
+ multi_start(
301
+ series_,
302
+ function=[plot_then_finalise, bar_plot],
303
+ starts=[0, -2],
304
+ title="Multi-start Bar Plot Example 1b",
305
+ xlabel="X-axis Label",
306
+ ylabel="Y-axis Label",
307
+ )
308
+
309
+
310
+ # --- test
311
+ if __name__ == "__main__":
312
+
313
+ # --- check that this fails
314
+ try:
315
+ multi_start(
316
+ data=DataFrame(),
317
+ function=[plot_then_finalise],
318
+ starts=[0, 1],
319
+ )
320
+ except (ValueError, TypeError) as e:
321
+ print(f"Expected error: {e}")
322
+
323
+ # --- check that tjis fails
324
+ try:
325
+ multi_column(
326
+ data=Series([1, 2, 3]), # type: ignore # Series is not a DataFrame
327
+ function=[plot_then_finalise],
328
+ )
329
+ except (ValueError, TypeError) as e:
330
+ print(f"Expected error: {e}")
331
+
332
+ # --- check that this fails
333
+ try:
334
+ plot_then_finalise(
335
+ data=Series([1, 2, 3]),
336
+ function=[],
337
+ )
338
+ except (ValueError, TypeError) as e:
339
+ print(f"Expected error: {e}")
@@ -0,0 +1,106 @@
1
+ """
2
+ covid_recovery_plot.py
3
+ Plot the pre-COVID trajectory against the current trend.
4
+ """
5
+
6
+ # --- imports
7
+ from pandas import DataFrame, Series, Period, PeriodIndex
8
+ from matplotlib.pyplot import Axes
9
+ from numpy import arange, polyfit
10
+
11
+ from mgplot.settings import DataT, get_setting
12
+ from mgplot.line_plot import line_plot
13
+ from mgplot.utilities import check_clean_timeseries
14
+ from mgplot.kw_type_checking import report_kwargs
15
+
16
+
17
+ # --- constants
18
+ WIDTH = "width"
19
+ STYLE = "style"
20
+ START_R = "start_r"
21
+ END_R = "end_r"
22
+
23
+
24
+ # --- functions
25
+ def get_projection(original: Series, to_period: Period) -> Series:
26
+ """
27
+ Projection based on data from the start of a series
28
+ to the to_period (inclusive). Returns projection over the whole
29
+ period of the original series.
30
+ """
31
+
32
+ y_regress = original[original.index <= to_period].copy()
33
+ x_regress = arange(len(y_regress))
34
+ m, b = polyfit(x_regress, y_regress, 1)
35
+
36
+ x_complete = arange(len(original))
37
+ projection = Series((x_complete * m) + b, index=original.index)
38
+
39
+ return projection
40
+
41
+
42
+ def postcovid_plot(data: DataT, **kwargs) -> Axes:
43
+ """
44
+ Plots a series with a PeriodIndex.
45
+
46
+ Arguments
47
+ - data - the series to be plotted (note that this function
48
+ is designed to work with a single series, not a DataFrame).
49
+ - **kwargs - same as for line_plot() and finalise_plot().
50
+
51
+ Raises:
52
+ - TypeError if series is not a pandas Series
53
+ - TypeError if series does not have a PeriodIndex
54
+ - ValueError if series does not have a D, M or Q frequency
55
+ - ValueError if regression start is after regression end
56
+ """
57
+
58
+ # --- sanity checks
59
+ report_kwargs(called_from="postcovid_plot", **kwargs)
60
+ data = check_clean_timeseries(data)
61
+ if not isinstance(data, Series):
62
+ raise TypeError("The series argument must be a pandas Series")
63
+ series: Series = data
64
+ series_index = PeriodIndex(series.index) # syntactic sugar for type hinting
65
+ if series_index.freqstr[:1] not in ("Q", "M", "D"):
66
+ raise ValueError("The series index must have a D, M or Q freq")
67
+ # rely on line_plot() to validate kwargs
68
+ if "plot_from" in kwargs:
69
+ print("Warning: the 'plot_from' argument is ignored in postcovid_plot().")
70
+ del kwargs["plot_from"]
71
+
72
+ # --- plot COVID counterfactural
73
+ freq = PeriodIndex(series.index).freqstr # syntactic sugar for type hinting
74
+ match freq[0]:
75
+ case "Q":
76
+ start_regression = Period("2014Q4", freq=freq)
77
+ end_regression = Period("2019Q4", freq=freq)
78
+ case "M":
79
+ start_regression = Period("2015-01", freq=freq)
80
+ end_regression = Period("2020-01", freq=freq)
81
+ case "D":
82
+ start_regression = Period("2015-01-01", freq=freq)
83
+ end_regression = Period("2020-01-01", freq=freq)
84
+
85
+ start_regression = Period(kwargs.pop("start_r", start_regression), freq=freq)
86
+ end_regression = Period(kwargs.pop("end_r", end_regression), freq=freq)
87
+ if start_regression >= end_regression:
88
+ raise ValueError("Start period must be before end period")
89
+
90
+ # --- combine data and projection
91
+ recent = series[series.index >= start_regression].copy()
92
+ recent.name = "Series"
93
+ projection = get_projection(recent, end_regression)
94
+ projection.name = "Pre-COVID projection"
95
+ data_set = DataFrame([projection, recent]).T
96
+
97
+ kwargs[WIDTH] = kwargs.pop(
98
+ WIDTH, [get_setting("line_normal"), get_setting("line_wide")]
99
+ )
100
+ kwargs[STYLE] = kwargs.pop(STYLE, ["--", "-"])
101
+ kwargs["legend"] = kwargs.pop("legend", True)
102
+
103
+ return line_plot(
104
+ data_set,
105
+ **kwargs,
106
+ )
mgplot/py.typed ADDED
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,60 @@
1
+ """
2
+ revision_plot.py
3
+ Plot ABS revisions to estimates over time.
4
+ """
5
+
6
+ # --- imports
7
+ from pandas import Series
8
+ from matplotlib.pyplot import Axes
9
+
10
+
11
+ from mgplot.utilities import annotate_series, check_clean_timeseries
12
+ from mgplot.line_plot import LP_KW_TYPES, line_plot
13
+ from mgplot.kw_type_checking import validate_kwargs, validate_expected
14
+ from mgplot.kw_type_checking import report_kwargs
15
+ from mgplot.settings import DataT
16
+ from mgplot.kw_type_checking import ExpectedTypeDict
17
+
18
+
19
+ # --- constants
20
+ ROUNDING = "rounding"
21
+ REVISION_KW_TYPES: ExpectedTypeDict = {
22
+ ROUNDING: (int, bool),
23
+ } | LP_KW_TYPES
24
+ validate_expected(REVISION_KW_TYPES, "revision_plot")
25
+
26
+
27
+ # --- functions
28
+ def revision_plot(data: DataT, **kwargs) -> Axes:
29
+ """
30
+ Plot the revisions to ABS data.
31
+
32
+ Arguments
33
+ data: pd.DataFrame - the data to plot, the DataFrame has a
34
+ column for each data revision
35
+ recent: int - the number of recent data points to plot
36
+ kwargs : dict :
37
+ - units: str - the units for the data (Note: you may need to
38
+ recalibrate the units for the y-axis)
39
+ - rounding: int | bool - if True apply default rounding, otherwise
40
+ apply int rounding.
41
+ """
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)
47
+
48
+ # --- critical defaults
49
+ kwargs["plot_from"] = kwargs.get("plot_from", -18)
50
+
51
+ # --- plot
52
+ axes = line_plot(data, **kwargs)
53
+
54
+ # --- Annotate the last value in each series ...
55
+ rounding: int | bool = kwargs.pop(ROUNDING, True)
56
+ for c in data.columns:
57
+ col: Series = data.loc[:, c].dropna()
58
+ annotate_series(col, axes, color="#222222", rounding=rounding, fontsize="small")
59
+
60
+ return axes
mgplot/run_plot.py ADDED
@@ -0,0 +1,182 @@
1
+ """
2
+ run_plot.py
3
+ This code contains a function to plot and highlighted
4
+ the 'runs' in a series.
5
+ """
6
+
7
+ # --- imports
8
+ from collections.abc import Sequence
9
+ from pandas import Series, concat
10
+ from matplotlib.pyplot import Axes
11
+ from matplotlib import patheffects as pe
12
+
13
+ from mgplot.settings import DataT
14
+ from mgplot.line_plot import line_plot
15
+ from mgplot.kw_type_checking import (
16
+ limit_kwargs,
17
+ ExpectedTypeDict,
18
+ validate_kwargs,
19
+ validate_expected,
20
+ report_kwargs,
21
+ )
22
+ from mgplot.line_plot import LP_KW_TYPES
23
+ from mgplot.utilities import constrain_data, check_clean_timeseries
24
+
25
+
26
+ # --- constants
27
+ THRESHOLD = "threshold"
28
+ ROUND = "round"
29
+ HIGHLIGHT = "highlight"
30
+ DIRECTION = "direction"
31
+
32
+ RUN_KW_TYPES: ExpectedTypeDict = {
33
+ THRESHOLD: float,
34
+ ROUND: int,
35
+ HIGHLIGHT: (str, Sequence, (str,)), # colors for highlighting the runs
36
+ DIRECTION: str, # "up", "down" or "both"
37
+ }
38
+ validate_expected(RUN_KW_TYPES, "run_highlight_plot")
39
+
40
+ # --- functions
41
+
42
+
43
+ def _identify_runs(
44
+ series: Series,
45
+ threshold: float,
46
+ up: bool, # False means down
47
+ ) -> tuple[Series, Series]:
48
+ """Identify monotonic increasing/decreasing runs."""
49
+
50
+ diffed = series.diff()
51
+ change_points = concat(
52
+ [diffed[diffed.gt(threshold)], diffed[diffed.lt(-threshold)]]
53
+ ).sort_index()
54
+ if series.index[0] not in change_points.index:
55
+ starting_point = Series([0], index=[series.index[0]])
56
+ change_points = concat([change_points, starting_point]).sort_index()
57
+ facing = change_points > 0 if up else change_points < 0
58
+ cycles = (facing & ~facing.shift().astype(bool)).cumsum()
59
+ return cycles[facing], change_points
60
+
61
+
62
+ def _plot_runs(
63
+ axes: Axes,
64
+ series: Series,
65
+ up: bool,
66
+ **kwargs,
67
+ ) -> None:
68
+ """Highlight the runs of a series."""
69
+
70
+ threshold = kwargs[THRESHOLD]
71
+ match kwargs.get(HIGHLIGHT): # make sure highlight is a color string
72
+ case str():
73
+ highlight = kwargs.get(HIGHLIGHT)
74
+ case Sequence():
75
+ highlight = kwargs[HIGHLIGHT][0] if up else kwargs[HIGHLIGHT][1]
76
+ case _:
77
+ raise ValueError(
78
+ f"Invalid type for highlight: {type(kwargs.get(HIGHLIGHT))}. "
79
+ "Expected str or Sequence."
80
+ )
81
+
82
+ # highlight the runs
83
+ stretches, change_points = _identify_runs(series, threshold, up=up)
84
+ for k in range(1, stretches.max() + 1):
85
+ stretch = stretches[stretches == k]
86
+ axes.axvspan(
87
+ stretch.index.min(),
88
+ stretch.index.max(),
89
+ color=highlight,
90
+ zorder=-1,
91
+ )
92
+ space_above = series.max() - series[stretch.index].max()
93
+ space_below = series[stretch.index].min() - series.min()
94
+ y_pos, vert_align = (
95
+ (series.max(), "top")
96
+ if space_above > space_below
97
+ else (series.min(), "bottom")
98
+ )
99
+ text = axes.text(
100
+ x=stretch.index.min(),
101
+ y=y_pos,
102
+ s=(
103
+ change_points[stretch.index].sum().round(kwargs["round"]).astype(str)
104
+ + " pp"
105
+ ),
106
+ va=vert_align,
107
+ ha="left",
108
+ fontsize="small",
109
+ rotation=90,
110
+ )
111
+ text.set_path_effects([pe.withStroke(linewidth=5, foreground="w")])
112
+
113
+
114
+ def run_plot(series: DataT, **kwargs) -> Axes:
115
+ """Plot a series of percentage rates, highlighting the increasing runs.
116
+
117
+ Arguments
118
+ - series - ordered pandas Series of percentages, with PeriodIndex
119
+ - **kwargs
120
+ - threshold - float - used to ignore micro noise near zero
121
+ (for example, threshhold=0.01)
122
+ - round - int - rounding for highlight text
123
+ - highlight - str or Sequence[str] - color(s) for highlighting the
124
+ runs, two colors can be specified in a list if direction is "both"
125
+ - direction - str - whether the highlight is for an upward
126
+ or downward or both runs. Options are "up", "down" or "both".
127
+ - in addition the **kwargs for line_plot are accepted.
128
+
129
+ Return
130
+ - matplotlib Axes object"""
131
+
132
+ # --- sanity checks
133
+ series = check_clean_timeseries(series)
134
+ if not isinstance(series, Series):
135
+ raise TypeError("series must be a pandas Series for run_plot()")
136
+ series, kwargs = constrain_data(series, **kwargs)
137
+
138
+ # --- check the kwargs
139
+ report_kwargs(called_from="run_plot", **kwargs)
140
+ expected = RUN_KW_TYPES | LP_KW_TYPES
141
+ validate_kwargs(expected, "run_plot", **kwargs)
142
+
143
+ # --- default arguments - in **kwargs
144
+ kwargs[THRESHOLD] = kwargs.get(THRESHOLD, 0.1)
145
+ kwargs[ROUND] = kwargs.get(ROUND, 2)
146
+ direct = kwargs[DIRECTION] = kwargs.get(DIRECTION, "up")
147
+ kwargs[HIGHLIGHT], kwargs["color"] = (
148
+ (kwargs.get(HIGHLIGHT, "gold"), kwargs.get("color", "#dd0000"))
149
+ if direct == "up"
150
+ else (
151
+ (kwargs.get(HIGHLIGHT, "skyblue"), kwargs.get("color", "navy"))
152
+ if direct == "down"
153
+ else (
154
+ kwargs.get(HIGHLIGHT, ("gold", "skyblue")),
155
+ kwargs.get("color", "navy"),
156
+ )
157
+ )
158
+ )
159
+
160
+ # defauls for line_plot
161
+ kwargs["width"] = kwargs.get("width", 2)
162
+
163
+ # plot the line
164
+ kwargs["drawstyle"] = kwargs.get("drawstyle", "steps-post")
165
+ lp_kwargs = limit_kwargs(LP_KW_TYPES, **kwargs)
166
+ axes = line_plot(series, **lp_kwargs)
167
+
168
+ # plot the runs
169
+ match kwargs[DIRECTION]:
170
+ case "up":
171
+ _plot_runs(axes, series, up=True, **kwargs)
172
+ case "down":
173
+ _plot_runs(axes, series, up=False, **kwargs)
174
+ case "both":
175
+ _plot_runs(axes, series, up=True, **kwargs)
176
+ _plot_runs(axes, series, up=False, **kwargs)
177
+ case _:
178
+ raise ValueError(
179
+ f"Invalid value for direction: {kwargs[DIRECTION]}. "
180
+ "Expected 'up', 'down', or 'both'."
181
+ )
182
+ return axes