leanplot 1.0.0__tar.gz

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.
@@ -0,0 +1,16 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joel Kimmich
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ persons to whom the Software is furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: leanplot
3
+ Version: 1.0.0
4
+ Summary: Provides some classes for fast matplotlib plotting based on ISO 80000.
5
+ Author-email: Joel Kimmich <software@igte.uni-stuttgart.de>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://codeberg.org/IGTE/leanplot
8
+ Project-URL: Documentation, https://codeberg.org/IGTE/leanplot
9
+ Project-URL: Repository, https://codeberg.org/IGTE/leanplot
10
+ Project-URL: Issues, https://codeberg.org/IGTE/leanplot/issues
11
+ Project-URL: Changelog, https://codeberg.org/IGTE/leanplot/CHANGELOG.rst
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Intended Audience :: Education
15
+ Classifier: Topic :: Documentation
16
+ Classifier: Topic :: Scientific/Engineering :: Visualization
17
+ Classifier: Topic :: Utilities
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Programming Language :: Python :: 3 :: Only
24
+ Classifier: Typing :: Typed
25
+ Classifier: Operating System :: Microsoft :: Windows
26
+ Classifier: Operating System :: POSIX
27
+ Classifier: Operating System :: Unix
28
+ Classifier: Operating System :: MacOS
29
+ Requires-Python: >=3.11
30
+ Description-Content-Type: text/markdown
31
+ License-File: LICENSE.txt
32
+ Requires-Dist: matplotlib>=3.10.0
33
+ Requires-Dist: numpy>=2.0.0
34
+ Requires-Dist: cmcrameri
35
+ Provides-Extra: dev
36
+ Requires-Dist: ruff>=0.15.8; extra == "dev"
37
+ Requires-Dist: build>=1.4.2; extra == "dev"
38
+ Requires-Dist: pytest; extra == "dev"
39
+ Requires-Dist: pytest-cov; extra == "dev"
40
+ Requires-Dist: twine; extra == "dev"
41
+ Dynamic: license-file
42
+
43
+ [//]: # (<h1 align="center">)
44
+
45
+ [//]: # (<img src="somePath" alt="LeanPlot Logo" height="65">)
46
+
47
+ [//]: # (</h1><br>)
48
+
49
+ # LeanPlot
50
+
51
+ [![PyPI - Version](https://shields.io/pypi/v/leanplot)](https://pypi.org/project/leanplot/)
52
+ [![PyPI - License](https://shields.io/pypi/l/leanplot)](https://pypi.org/project/leanplot/)
53
+ [![Typing](https://shields.io/pypi/types/leanplot)](https://pypi.org/project/leanplot/)
54
+
55
+ LeanPlot is a baseline plotting tool for formatted matplotlib plots based on ISO 80000 using "," as decimal and a
56
+ short protected space as thousands separator. Also, it makes it easy to plot over time with correct formatting.
57
+
58
+ The result can always be manipulated like any other matplotlib axes and figures.
59
+
60
+ ## Installation
61
+
62
+ ```bash
63
+ pip install leanplot
64
+ ```
65
+
66
+ ## License
67
+
68
+ See LICENSE.txt for the license.
@@ -0,0 +1,26 @@
1
+ [//]: # (<h1 align="center">)
2
+
3
+ [//]: # (<img src="somePath" alt="LeanPlot Logo" height="65">)
4
+
5
+ [//]: # (</h1><br>)
6
+
7
+ # LeanPlot
8
+
9
+ [![PyPI - Version](https://shields.io/pypi/v/leanplot)](https://pypi.org/project/leanplot/)
10
+ [![PyPI - License](https://shields.io/pypi/l/leanplot)](https://pypi.org/project/leanplot/)
11
+ [![Typing](https://shields.io/pypi/types/leanplot)](https://pypi.org/project/leanplot/)
12
+
13
+ LeanPlot is a baseline plotting tool for formatted matplotlib plots based on ISO 80000 using "," as decimal and a
14
+ short protected space as thousands separator. Also, it makes it easy to plot over time with correct formatting.
15
+
16
+ The result can always be manipulated like any other matplotlib axes and figures.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install leanplot
22
+ ```
23
+
24
+ ## License
25
+
26
+ See LICENSE.txt for the license.
@@ -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_)
@@ -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)