leanplot 1.0.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.
leanplot/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ # ------------------------------------------------------------------------------
2
+ # Copyright (c) 2026 Joel Kimmich
3
+ # ------------------------------------------------------------------------------
4
+ # License
5
+ # This file is part of LeanPlot, distributed under MIT license.
6
+ # ------------------------------------------------------------------------------
7
+ #
8
+
9
+ __all__ = []
10
+
11
+ from leanplot.core import *
12
+ from leanplot.core import __all__ as _core_all_
13
+
14
+ __all__.extend(_core_all_)
@@ -0,0 +1,38 @@
1
+ # ------------------------------------------------------------------------------
2
+ # Copyright (c) 2026 Joel Kimmich
3
+ # ------------------------------------------------------------------------------
4
+ # License
5
+ # This file is part of LeanPlot, distributed under MIT license.
6
+ # ------------------------------------------------------------------------------
7
+ #
8
+
9
+ from leanplot.core.bar import BarPlot, TimeBarPlot
10
+ from leanplot.core.double_y import DoubleY
11
+ from leanplot.core.functions import (
12
+ cm_to_inch,
13
+ custom_numeric_ticks_formatter,
14
+ format_date_x_axis,
15
+ is_numeric,
16
+ round_valid,
17
+ )
18
+ from leanplot.core.line import LinePlot, TimePlot
19
+ from leanplot.core.mixin import PlotMixin
20
+
21
+ __all__ = ["BarPlot", "TimeBarPlot"]
22
+ __all__.extend(["DoubleY"])
23
+ __all__.extend(
24
+ [
25
+ "is_numeric",
26
+ "cm_to_inch",
27
+ "custom_numeric_ticks_formatter",
28
+ "format_date_x_axis",
29
+ "round_valid",
30
+ ]
31
+ )
32
+ __all__.extend(["LinePlot", "TimePlot"])
33
+ __all__.extend(["PlotMixin"])
34
+
35
+ from .colors import *
36
+ from .colors import __all__ as _colors_all_
37
+
38
+ __all__.extend(_colors_all_)
leanplot/core/bar.py ADDED
@@ -0,0 +1,64 @@
1
+ # ------------------------------------------------------------------------------
2
+ # Copyright (c) 2026 Joel Kimmich
3
+ # ------------------------------------------------------------------------------
4
+ # License
5
+ # This file is part of LeanPlot, distributed under MIT license.
6
+ # ------------------------------------------------------------------------------
7
+ #
8
+ from datetime import timedelta
9
+
10
+ import matplotlib.dates as mdates
11
+ import numpy as np
12
+
13
+ from leanplot.core.functions import format_date_x_axis, is_numeric
14
+ from leanplot.core.mixin import PlotMixin
15
+
16
+
17
+ class BarPlot(PlotMixin):
18
+ def __init__(self, bar_values, bar_positions=None, y_title="Heat / kWh", **kwargs):
19
+ assert len(bar_values) > 1
20
+ if bar_positions is None:
21
+ bar_positions = range(len(bar_values))
22
+ super().__init__(x=bar_positions, ys=bar_values, y_title=y_title, **kwargs)
23
+
24
+ @property
25
+ def bar_width(self):
26
+ if len(self.x) > 1:
27
+ diff = np.diff(self.x)
28
+ min_diff = np.min(diff)
29
+ return min_diff * 0.8
30
+ return 1
31
+
32
+ def plot(self):
33
+ self.ax.bar(self.x, self.ys, width=self.bar_width, color=next(self.next_color))
34
+ self.format()
35
+
36
+ def format(self):
37
+ super().format()
38
+ if is_numeric(self.x[0]):
39
+ self.ax.set_xlim(left=self.x[0] - self.bar_width / 0.8 / 2, right=self.x[-1] + self.bar_width / 0.8 / 2)
40
+ self.format_numeric_ticks()
41
+
42
+
43
+ class TimeBarPlot(BarPlot):
44
+ def __init__(self, bar_values, bar_positions, y_title="Heat / kWh", **kwargs):
45
+ super().__init__(bar_values, bar_positions, y_title=y_title, **kwargs)
46
+
47
+ @property
48
+ def bar_width(self) -> float:
49
+ if len(self.x) > 1:
50
+ date_nums = mdates.date2num(self.x)
51
+ time_diffs = np.diff(date_nums)
52
+ min_diff = time_diffs[0]
53
+ return float(min_diff * 0.8)
54
+ else:
55
+ return 1 / 24
56
+
57
+ def format(self):
58
+ super().format()
59
+ format_date_x_axis(self.x[0], self.x[-1], self.ax, return_version=False)
60
+ self.ax.set_xlim(
61
+ left=self.x[0] - timedelta(days=self.bar_width / 0.8 / 2),
62
+ right=self.x[-1] + timedelta(days=self.bar_width / 0.8 / 2),
63
+ )
64
+ self.format_numeric_ticks()
@@ -0,0 +1,11 @@
1
+ # ------------------------------------------------------------------------------
2
+ # Copyright (c) 2026 Joel Kimmich
3
+ # ------------------------------------------------------------------------------
4
+ # License
5
+ # This file is part of LeanPlot, distributed under MIT license.
6
+ # ------------------------------------------------------------------------------
7
+ #
8
+
9
+ from leanplot.core.colors.categorical import ColorPalette
10
+
11
+ __all__ = ["ColorPalette"]
@@ -0,0 +1,72 @@
1
+ # ------------------------------------------------------------------------------
2
+ # Copyright (c) 2026 Joel Kimmich
3
+ # ------------------------------------------------------------------------------
4
+ # License
5
+ # This file is part of LeanPlot, distributed under MIT license.
6
+ # ------------------------------------------------------------------------------
7
+ #
8
+
9
+ import cmcrameri.cm as cmc
10
+
11
+
12
+ class ColorPalette:
13
+ """
14
+ Provide a categorical color palette from a pre-defined categorical colormap.
15
+
16
+ Parameters
17
+ ----------
18
+ colormap_name : str, default="batlow"
19
+ Base name of the colormap defined in `cmcrameri.cm`, without the categorical suffix
20
+ (for example "batlow", "grayC", "oslo"). The categorical variant is accessed by appending "S".
21
+ number_of_colors : int, default=5
22
+ Number of categorical colors to use from the colormap.
23
+ """
24
+
25
+ def __init__(self, colormap_name: str = "oslo", number_of_colors: int = 5) -> None:
26
+ self.colormap_name = colormap_name
27
+ self.number_of_colors = number_of_colors
28
+
29
+ @property
30
+ def colors(self) -> list[tuple[float, float, float]]:
31
+ """
32
+ Return categorical colors from the predefined categorical colormap.
33
+
34
+ Returns
35
+ -------
36
+ list[tuple[float, float, float]]
37
+ List of RGB color tuples with values in the range [0, 1].
38
+
39
+ Raises
40
+ ------
41
+ AttributeError
42
+ If the categorical colormap `f"{colormap_name}S"` is not defined in `cmcrameri.cm`.
43
+ ValueError
44
+ If `number_of_colors` exceeds the number of colors available in the categorical colormap.
45
+ """
46
+ categorical_colormap = getattr(cmc, f"{self.colormap_name}S")
47
+ available_colors = categorical_colormap.colors
48
+ if self.number_of_colors > len(available_colors):
49
+ raise ValueError(
50
+ f"Requested {self.number_of_colors} colors, but colormap "
51
+ f"'{self.colormap_name}S' only provides {len(available_colors)} colors."
52
+ )
53
+ return [tuple(color[:3]) for color in available_colors[: self.number_of_colors]]
54
+
55
+ @staticmethod
56
+ def available_categorical_colormap_names() -> list[str]:
57
+ """
58
+ Return the base names of all colormaps in `cmcrameri.cm` that provide a categorical variant.
59
+
60
+ Returns
61
+ -------
62
+ list[str]
63
+ Sorted list of base colormap names (without the categorical "S" suffix and without
64
+ reversed "_r" variants) for which `f"{name}S"` exists in `cmcrameri.cm`.
65
+ """
66
+ categorical_suffix = "S"
67
+ reversed_suffix = "_r"
68
+ base_names = []
69
+ for attribute_name in dir(cmc):
70
+ if attribute_name.endswith(categorical_suffix) and not attribute_name.endswith(reversed_suffix):
71
+ base_names.append(attribute_name[: -len(categorical_suffix)])
72
+ return sorted(base_names)
@@ -0,0 +1,219 @@
1
+ # ------------------------------------------------------------------------------
2
+ # Copyright (c) 2026 Joel Kimmich
3
+ # ------------------------------------------------------------------------------
4
+ # License
5
+ # This file is part of LeanPlot, distributed under MIT license.
6
+ # ------------------------------------------------------------------------------
7
+ #
8
+
9
+ # ------------------------------------------------------------------------------
10
+ # Copyright (c) 2026 Joel Kimmich
11
+ # ------------------------------------------------------------------------------
12
+ # License
13
+ # This file is part of LeanPlot, distributed under MIT license.
14
+ # ------------------------------------------------------------------------------
15
+ #
16
+ import numpy as np
17
+
18
+ from leanplot.core.mixin import PlotMixin
19
+
20
+
21
+ class DoubleY(PlotMixin):
22
+ def __init__(
23
+ self,
24
+ x,
25
+ ys_left: list,
26
+ ys_right: list,
27
+ labels_left=None,
28
+ labels_right=None,
29
+ y_title_left="Heat Flux / W",
30
+ y_title_right="Temperature / K",
31
+ marker_size=6,
32
+ **kwargs,
33
+ ):
34
+ if not isinstance(ys_left, list):
35
+ ys_left = [ys_left]
36
+ if not isinstance(ys_right, list):
37
+ ys_right = [ys_right]
38
+ ys_left = [np.array(y) for y in ys_left]
39
+ ys_right = [np.array(y) for y in ys_right]
40
+
41
+ super().__init__(x=np.array(x), ys=ys_left, y_title=y_title_left, marker_size=marker_size, **kwargs)
42
+
43
+ self.ys_left = ys_left
44
+ self.ys_right = ys_right
45
+ self.y_title_right = y_title_right
46
+
47
+ self.ax_right = self.ax.twinx()
48
+
49
+ if labels_left is None:
50
+ labels_left = [None] * len(ys_left)
51
+ if labels_right is None:
52
+ labels_right = [None] * len(ys_right)
53
+ if not isinstance(labels_left, list):
54
+ labels_left = [labels_left]
55
+ if not isinstance(labels_right, list):
56
+ labels_right = [labels_right]
57
+
58
+ self.labels_left = labels_left
59
+ self.labels_right = labels_right
60
+
61
+ @property
62
+ def ax_left(self):
63
+ return self.ax
64
+
65
+ def scale_right_axis(self):
66
+ """
67
+ Scales the right axis so that the major ticks matches the left one.
68
+ """
69
+ left_ticks = self.ax.get_yticks()
70
+ num_ticks = len(left_ticks)
71
+
72
+ # Get right axis data range
73
+ right_data_min, right_data_max = self.ax_right.get_ylim()
74
+ right_range = right_data_max - right_data_min
75
+
76
+ # Generate nice spacings: [1,2,5] * 10^n
77
+ nice_spacings = []
78
+ for n in range(-10, 10):
79
+ for base in [1, 2, 3, 4, 5, 6]:
80
+ nice_spacings.append(base * 10**n)
81
+
82
+ # Find the minimum spacing that can cover the data range with num_ticks-1 intervals
83
+ target_spacing = right_range / (num_ticks - 1)
84
+ right_tick_spacing = min([s for s in nice_spacings if s >= target_spacing])
85
+
86
+ # Calculate new right axis limits based on nice spacing
87
+ right_min = np.floor(right_data_min / right_tick_spacing) * right_tick_spacing
88
+ right_max = right_min + (num_ticks - 1) * right_tick_spacing
89
+
90
+ # Create right axis ticks
91
+ right_ticks = np.linspace(right_min, right_max, num_ticks)
92
+
93
+ self.ax_right.set_ylim(right_min, right_max)
94
+ self.ax_right.set_yticks(right_ticks)
95
+
96
+ def plot(self):
97
+
98
+ for i, y in enumerate(self.ys_left):
99
+ self._add_line_and_label(
100
+ self.ax.plot(
101
+ self.x,
102
+ y,
103
+ label=self.labels_left[i],
104
+ color=next(self.next_color),
105
+ marker=next(self.next_marker),
106
+ markersize=self.marker_size,
107
+ linestyle="None",
108
+ ),
109
+ self.labels_left[i],
110
+ )
111
+
112
+ for i, y in enumerate(self.ys_right):
113
+ self._add_line_and_label(
114
+ self.ax_right.plot(
115
+ self.x,
116
+ y,
117
+ label=self.labels_right[i],
118
+ color=next(self.next_color),
119
+ marker=next(self.next_marker),
120
+ markersize=self.marker_size,
121
+ linestyle="None",
122
+ ),
123
+ self.labels_right[i],
124
+ )
125
+
126
+ self.format()
127
+
128
+ def format(self):
129
+ if self.x_title is not None:
130
+ self.ax.set_xlabel(self.x_title)
131
+ if self.y_title is not None:
132
+ self.ax.set_ylabel(self.y_title)
133
+ if self.y_title_right is not None:
134
+ self.ax_right.set_ylabel(self.y_title_right)
135
+
136
+ self.ax.grid(True)
137
+
138
+ if not isinstance(self.x[0], str):
139
+ dx = self.x[1] - self.x[0] if len(self.x) > 1 else 0
140
+ self.ax.set_xlim(left=self.x[0] - dx / 2, right=self.x[-1] + dx / 2)
141
+
142
+ if self.x_is_datetime:
143
+ self.formate_x_datetime(self.x[0] - dx / 2, self.x[-1] + dx / 2)
144
+
145
+ self.format_numeric_ticks()
146
+
147
+
148
+ class DoubleYSeparated(DoubleY):
149
+ def __init__(
150
+ self,
151
+ x,
152
+ ys_left: list,
153
+ ys_right: list,
154
+ labels=None,
155
+ y_title_left="Heat Flux / W",
156
+ y_title_right="Temperature / K",
157
+ marker_size=6,
158
+ same_marker=False,
159
+ ):
160
+ super().__init__(
161
+ x, ys_left, ys_right, y_title_left=y_title_left, y_title_right=y_title_right, marker_size=marker_size
162
+ )
163
+
164
+ if labels is None:
165
+ labels = [None] * len(ys_left)
166
+ if not isinstance(labels, list):
167
+ labels = [labels]
168
+ self.labels = labels
169
+ self.same_marker = same_marker
170
+
171
+ def plot(self):
172
+
173
+ for i, (y_left, y_right) in enumerate(zip(self.ys_left, self.ys_right)):
174
+ marker = next(self.next_marker)
175
+
176
+ self._add_line(
177
+ self.ax.plot(
178
+ self.x,
179
+ y_left,
180
+ color="black",
181
+ marker=marker,
182
+ markersize=self.marker_size,
183
+ linestyle="None",
184
+ )
185
+ )
186
+
187
+ if not self.same_marker:
188
+ marker = next(self.next_marker)
189
+
190
+ self._add_line(
191
+ self.ax_right.plot(
192
+ self.x,
193
+ y_right,
194
+ color=self.colors[0],
195
+ marker=marker,
196
+ markersize=self.marker_size,
197
+ linestyle="None",
198
+ )
199
+ )
200
+
201
+ if self.labels[i] is not None and not self.same_marker:
202
+ self.ax.plot(
203
+ [],
204
+ [],
205
+ color="black",
206
+ marker=marker,
207
+ linestyle="None",
208
+ markersize=self.marker_size,
209
+ label=self.labels[i],
210
+ )
211
+
212
+ self.format()
213
+
214
+ def format(self):
215
+ super().format()
216
+ self.ax.yaxis.label.set_color("black")
217
+ self.ax.tick_params(axis="y", colors="black")
218
+ self.ax_right.yaxis.label.set_color(self.colors[0])
219
+ self.ax_right.tick_params(axis="y", colors=self.colors[0])
@@ -0,0 +1,145 @@
1
+ # ------------------------------------------------------------------------------
2
+ # Copyright (c) 2026 Joel Kimmich
3
+ # ------------------------------------------------------------------------------
4
+ # License
5
+ # This file is part of LeanPlot, distributed under MIT license.
6
+ # ------------------------------------------------------------------------------
7
+ #
8
+ from datetime import datetime, timedelta
9
+
10
+ import matplotlib.dates as mdates
11
+ import numpy as np
12
+
13
+
14
+ def is_numeric(value):
15
+ """
16
+ Checks if value is a numeric value.
17
+
18
+ Parameters
19
+ ----------
20
+ value : Any
21
+ Value to check.
22
+
23
+ Returns
24
+ -------
25
+ bool
26
+ True if value is numeric, False otherwise
27
+ """
28
+ list_of_numerics = [float, int, np.number] # add numeric data types here if necessary
29
+ for numeric in list_of_numerics:
30
+ if isinstance(value, numeric):
31
+ return True
32
+ return False
33
+
34
+
35
+ def cm_to_inch(value):
36
+ return value / 2.54
37
+
38
+
39
+ def custom_numeric_ticks_formatter(x, pos):
40
+ """
41
+ Format numbers/ticks to match ISO 80000.
42
+
43
+ Parameters
44
+ ----------
45
+ x : float | int
46
+ The number to format.
47
+ pos
48
+
49
+ Returns
50
+ -------
51
+ str :
52
+ The formatted number with a short protected space as thousands separator and a "," as decimal.
53
+ """
54
+ if abs(x) >= 1000:
55
+ s = f"{x:,.0f}".replace(",", "\u202f").replace(".", ",")
56
+ return s
57
+ else:
58
+ return f"{x:g}".replace(".", ",")
59
+
60
+
61
+ def format_date_x_axis(start_date: datetime, end_date: datetime, ax, return_version: bool = True) -> str | None:
62
+ """
63
+ Formats the ticks of an axis as date.
64
+
65
+ Depending on the date range it parses the time/date to days, weeks, months and years with matching minor ticks.
66
+
67
+ Parameters
68
+ ----------
69
+ start_date : datetime
70
+ The start date of the axis.
71
+ end_date : datetime
72
+ The end date of the axis.
73
+ ax : matplotlib.axis.Axis
74
+ The axis to format.
75
+ return_version : bool
76
+ Whether to return the version of the axis or not.
77
+
78
+ Returns
79
+ -------
80
+ str | None :
81
+ The formatted Axis if return_version is True, otherwise None.
82
+ """
83
+ time_delta = end_date - start_date
84
+ if time_delta <= timedelta(days=3):
85
+ version = "day"
86
+ ax.xaxis.set_major_locator(mdates.HourLocator(byhour=(0, 6, 12, 18)))
87
+ ax.xaxis.set_minor_locator(mdates.HourLocator())
88
+ ax.xaxis.set_major_formatter(mdates.DateFormatter("%d.%m. %-H Uhr"))
89
+ elif time_delta <= timedelta(days=10):
90
+ version = "week"
91
+ ax.xaxis.set_major_locator(mdates.DayLocator())
92
+ ax.xaxis.set_minor_locator(mdates.HourLocator(byhour=(0, 6, 12, 18)))
93
+ ax.xaxis.set_major_formatter(mdates.DateFormatter("%d.%m."))
94
+ elif time_delta <= timedelta(days=80):
95
+ version = "month"
96
+ ax.xaxis.set_major_locator(mdates.DayLocator(bymonthday=(1, 7, 15, 23)))
97
+ ax.xaxis.set_minor_locator(mdates.DayLocator())
98
+ ax.xaxis.set_major_formatter(mdates.DateFormatter("%d.%m."))
99
+ else:
100
+ version = "year"
101
+ ax.xaxis.set_major_locator(mdates.MonthLocator())
102
+ ax.xaxis.set_minor_locator(mdates.DayLocator(bymonthday=(1, 7, 14, 21, 28)))
103
+ ax.xaxis.set_major_formatter(mdates.DateFormatter("%-m."))
104
+ ax.grid(True)
105
+ ax.tick_params(axis="x", which="minor", bottom=True)
106
+ ax.set_xlim(left=start_date)
107
+ ax.set_xlim(right=end_date)
108
+ if return_version:
109
+ return version
110
+
111
+
112
+ def round_valid(number: float | int, valid_digits: int, return_str: bool = False):
113
+ if np.isnan(number):
114
+ if return_str:
115
+ return "NaN"
116
+ return np.nan
117
+ if number == np.inf:
118
+ if return_str:
119
+ return "Infinity"
120
+ return np.inf
121
+ if number == -np.inf:
122
+ if return_str:
123
+ return "-Infinity"
124
+ return -np.inf
125
+ if int(number) == 0:
126
+ # number < 1
127
+ if number == 0:
128
+ if return_str:
129
+ return "0"
130
+ return 0
131
+ counter = 0
132
+ while int(number) == 0:
133
+ number = number * 10
134
+ counter += 1
135
+ if return_str:
136
+ format_str = f"{{:{counter + 1 + valid_digits}.{valid_digits + counter - 1}f}}"
137
+ return format_str.format(np.round(number, valid_digits) / 10**counter)
138
+ return np.round(np.round(number, valid_digits) / 10**counter, counter + valid_digits - 1)
139
+ else:
140
+ ten_digits = int(np.log10(abs(number))) + 1
141
+ if ten_digits >= valid_digits:
142
+ number = int(number)
143
+ if return_str:
144
+ return f"{np.round(number, valid_digits - ten_digits)}"
145
+ return np.round(number, valid_digits - ten_digits)
leanplot/core/line.py ADDED
@@ -0,0 +1,135 @@
1
+ # ------------------------------------------------------------------------------
2
+ # Copyright (c) 2026 Joel Kimmich
3
+ # ------------------------------------------------------------------------------
4
+ # License
5
+ # This file is part of LeanPlot, distributed under MIT license.
6
+ # ------------------------------------------------------------------------------
7
+ #
8
+ import numpy as np
9
+
10
+ from leanplot.core.mixin import PlotMixin
11
+
12
+
13
+ class LinePlot(PlotMixin):
14
+ def __init__(
15
+ self,
16
+ x,
17
+ ys: list | np.ndarray,
18
+ labels=None,
19
+ y_scale=1,
20
+ y_title="Values",
21
+ linewidth=1.8,
22
+ x_title=None,
23
+ width_mm=160,
24
+ height_mm=90,
25
+ ):
26
+ if not isinstance(ys, list):
27
+ if not (isinstance(ys, np.ndarray) and len(ys.shape) > 1 and ys.shape[0] > 1 and ys.shape[1] > 1):
28
+ ys = [np.array(ys)]
29
+ else:
30
+ if len(ys) == len(x):
31
+ ys = [np.array(ys)]
32
+ ys = [np.array(y) for y in ys]
33
+ super().__init__(x=np.array(x), ys=ys, y_title=y_title, x_title=x_title, width_mm=width_mm, height_mm=height_mm)
34
+
35
+ if labels is None:
36
+ labels = [None] * len(ys)
37
+ if not isinstance(labels, list):
38
+ labels = [labels]
39
+ self.labels = labels
40
+
41
+ self.y_scale = y_scale
42
+ self.line_width = linewidth
43
+
44
+ def plot(self, x=None, ys=None, labels=None):
45
+ if x is None:
46
+ x = self.x
47
+ if ys is None:
48
+ ys = self.ys
49
+ if labels is None:
50
+ labels = self.labels
51
+ if not isinstance(labels, list):
52
+ labels = [labels]
53
+ for i, y in enumerate(ys):
54
+ self._add_line(
55
+ self.ax.plot(
56
+ x,
57
+ np.array(y) * self.y_scale,
58
+ label=labels[i],
59
+ color=next(self.next_color),
60
+ linewidth=self.line_width,
61
+ linestyle=next(self.next_line_style),
62
+ )
63
+ )
64
+ self.format()
65
+
66
+ def add_points(self, x, ys, labels=None):
67
+ """
68
+ Add points to the plot (e.g. measurement data to interpolated lines)
69
+
70
+ The ys values are not scaled with self.y_scale!
71
+
72
+ Parameters
73
+ ----------
74
+ x : float | int | np.ndarray | list
75
+ x values
76
+ ys : float | int | np.ndarray | list
77
+ y values. If two-dimensional several point plots are added with different colors.
78
+ labels : list | str, optional
79
+ The labels of the point groups, accordingly
80
+ """
81
+ if not isinstance(x, np.ndarray):
82
+ x = np.array(x)
83
+ if not isinstance(ys, np.ndarray):
84
+ ys = np.array(ys)
85
+ ys = np.atleast_2d(ys)
86
+ if labels is None:
87
+ labels = [None] * len(ys)
88
+ if not isinstance(labels, list):
89
+ labels = [labels]
90
+ assert len(labels) == len(ys)
91
+
92
+ for i, y in enumerate(ys):
93
+ self._add_line_and_label(
94
+ self.ax.plot(
95
+ x,
96
+ np.array(y),
97
+ label=labels[i],
98
+ color=next(self.next_color),
99
+ linestyle="none",
100
+ marker=next(self.next_marker),
101
+ ),
102
+ labels[i],
103
+ )
104
+ self.format()
105
+
106
+ def plot_marker(self):
107
+ for i, y in enumerate(self.ys):
108
+ self.ax.plot(
109
+ self.x,
110
+ np.array(y) * self.y_scale,
111
+ label=self.labels[i],
112
+ color=next(self.next_color),
113
+ marker=next(self.next_marker),
114
+ )
115
+ self.format()
116
+
117
+ def plot_stack(self):
118
+ self.ax.stackplot(
119
+ self.x,
120
+ *[y * self.y_scale for y in self.ys],
121
+ labels=self.labels,
122
+ colors=self.colors,
123
+ )
124
+ self.format()
125
+
126
+ def format(self):
127
+ super().format()
128
+ self.format_numeric_ticks()
129
+
130
+
131
+ class TimePlot(LinePlot):
132
+ def format(self):
133
+ super().format()
134
+ self.formate_x_datetime()
135
+ self.format_numeric_ticks()