• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SPF-OST / pytrnsys_process / 26460350544

26 May 2026 04:06PM UTC coverage: 95.948% (-0.4%) from 96.386%
26460350544

push

github

ahobeost
Passed linting.

9 of 11 new or added lines in 1 file covered. (81.82%)

9 existing lines in 2 files now uncovered.

1326 of 1382 relevant lines covered (95.95%)

0.96 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.04
/pytrnsys_process/plot/plotters.py
1
import typing as _tp
1✔
2
import warnings
1✔
3
from abc import abstractmethod
1✔
4
from dataclasses import dataclass
1✔
5

6
import numpy as _np
1✔
7
import pandas as _pd
1✔
8
import matplotlib.pyplot as _plt
1✔
9
from matplotlib import dates as _dates, ticker as _tick
1✔
10

11
from pytrnsys_process import config as conf
1✔
12

13
# TODO: provide A4 and half A4 plots to test sizes in latex # pylint: disable=fixme
14
# TODO: provide height as input for plot?  # pylint: disable=fixme
15
# TODO: deal with legends (curve names, fonts, colors, linestyles) # pylint: disable=fixme
16
# TODO: clean up old stuff by refactoring # pylint: disable=fixme
17
# TODO: make issue for docstrings of plotting # pylint: disable=fixme
18
# TODO: Add colormap support # pylint: disable=fixme
19

20

21
# TODO find a better place for this to live in # pylint : disable=fixme
22
plot_settings = conf.global_settings.plot
1✔
23
"Settings shared by all plots"
1✔
24

25

26
class ChartBase:
1✔
27
    cmap: str | None = None
1✔
28

29
    def plot(
1✔
30
        self,
31
        df: _pd.DataFrame,
32
        columns: list[str],
33
        **kwargs,
34
    ) -> tuple[_plt.Figure, _plt.Axes]:
35
        fig, ax = self._do_plot(df, columns, **kwargs)
1✔
36
        return fig, ax
1✔
37

38
    @abstractmethod
1✔
39
    def _do_plot(
1✔
40
        self,
41
        df: _pd.DataFrame,
42
        columns: list[str],
43
        use_legend: bool = True,
44
        size: tuple[float, float] = conf.PlotSizes.A4.value,
45
        **kwargs: _tp.Any,
46
    ) -> tuple[_plt.Figure, _plt.Axes]:
47
        """Implement actual plotting logic in subclasses"""
48

49
    def check_for_cmap(self, kwargs, plot_kwargs):
1✔
50
        if "cmap" not in kwargs and "colormap" not in kwargs:
1✔
51
            plot_kwargs["cmap"] = self.cmap
1✔
52
        return plot_kwargs
1✔
53

54
    def get_cmap(self, kwargs) -> str | None:
1✔
55
        if not kwargs:
1✔
56
            return self.cmap
1✔
57

58
        if "cmap" not in kwargs and "colormap" not in kwargs:
1✔
59
            return self.cmap
1✔
60

61
        if "cmap" in kwargs:
1✔
62
            return kwargs["cmap"]
1✔
63

64
        if "colormap" in kwargs:
1✔
65
            return kwargs["colormap"]
1✔
66

67
        raise ValueError  # pragma: no cover
68

69
    @staticmethod
1✔
70
    def get_fig_and_ax(kwargs, size):
1✔
71
        if "fig" not in kwargs and "ax" not in kwargs:
1✔
72
            fig, ax = _plt.subplots(
1✔
73
                figsize=size,
74
                layout="constrained",
75
            )
76
            kwargs["ax"] = ax
1✔
77
        else:
78
            fig = kwargs["fig"]
1✔
79
            ax = kwargs["ax"]
1✔
80
        return fig, ax
1✔
81

82
    def get_fig_and_multi_ax(self, kwargs, size):
1✔
83
        if (
1✔
84
            "fig" in kwargs
85
            or "lax" in kwargs
86
            or "rax" in kwargs
87
            or "ax" in kwargs
88
        ):
UNCOV
89
            warnings.warn(
×
90
                "get_fig_and_multi_ax does not pass the figure handle, nor the axis handles."
91
            )
92

93
        fig, lax, leg_ax = self.prep_subplots_for_legend_outside_of_plot(size)
1✔
94
        rax = lax.twinx()
1✔
95

96
        return fig, lax, rax, leg_ax
1✔
97

98
    @staticmethod
1✔
99
    def prep_subplots_for_legend_outside_of_plot(
1✔
100
        size,
101
    ) -> tuple[_plt.Figure, _plt.Axes, _plt.Axes]:
102
        # See: https://stackoverflow.com/questions/4700614/
103
        # how-to-put-the-legend-outside-the-plot
104
        # This is required to place the legend in a dedicated subplot
105
        fig, (ax, leg_ax) = _plt.subplots(
1✔
106
            layout="constrained",
107
            figsize=size,
108
            ncols=2,
109
            gridspec_kw={"width_ratios": [4, 1]},
110
        )
111
        return fig, ax, leg_ax
1✔
112

113

114
class StackedBarChart(ChartBase):
1✔
115
    cmap: str | None = "inferno_r"
1✔
116

117
    def _do_plot(
1✔
118
        self,
119
        df: _pd.DataFrame,
120
        columns: list[str],
121
        use_legend: bool = True,
122
        size: tuple[float, float] = conf.PlotSizes.A4.value,
123
        **kwargs: _tp.Any,
124
    ) -> tuple[_plt.Figure, _plt.Axes]:
125
        fig, ax = self.get_fig_and_ax(kwargs, size)
1✔
126

127
        plot_kwargs = {
1✔
128
            "stacked": True,
129
            "legend": use_legend,
130
            **kwargs,
131
        }
132
        self.check_for_cmap(kwargs, plot_kwargs)
1✔
133
        ax = df[columns].plot.bar(**plot_kwargs)
1✔
134
        ax.set_xticklabels(
1✔
135
            _pd.to_datetime(df.index).strftime(plot_settings.date_format)
136
        )
137

138
        return fig, ax
1✔
139

140

141
class EnergyBalanceChart(ChartBase):
1✔
142
    cmap: str | None = "inferno_r"
1✔
143

144
    def plot(  # type: ignore[override]
1✔
145
        self,
146
        df: _pd.DataFrame,
147
        columns: dict[_tp.Any, _tp.Any],  # type: ignore[arg-type]
148
        **kwargs,
149
    ) -> tuple[_plt.Figure, _plt.Axes, _plt.Axes]:
150
        fig, lax, rax = self._do_plot(df, columns, **kwargs)
1✔
151
        return fig, lax, rax
1✔
152

153
    # pylint: disable=too-many-locals, too-many-statements
154
    def _do_plot(  # typing: ignore[override]
1✔
155
        self,
156
        df: _pd.DataFrame,
157
        columns: dict[_tp.Any, _tp.Any],
158
        use_legend: bool = True,
159
        size: tuple[float, float] = conf.PlotSizes.A4.value,
160
        **kwargs: _tp.Any,
161
    ) -> tuple[_plt.Figure, _plt.Axes, _plt.Axes]:
162
        fig, lax, rax, leg_ax = self.get_fig_and_multi_ax(kwargs, size)
1✔
163

164
        # TODO: implement other kwargs?  # pylint: disable=fixme
165
        plot_kwargs = {
1✔
166
            **kwargs,
167
        }
168

169
        q_in_columns = columns["q_in_columns"]
1✔
170
        q_out_columns = columns["q_out_columns"]
1✔
171
        q_imb_column = columns["q_imb_column"]
1✔
172
        line_columns = columns["line_columns"]
1✔
173

174
        pos_bottom = _np.zeros(len(df))
1✔
175
        neg_bottom = _np.zeros(len(df))
1✔
176
        self.check_for_cmap(kwargs, plot_kwargs)
1✔
177
        n_colors = len(q_in_columns) + len(q_out_columns) + 1
1✔
178
        cmap = self.get_cmap(kwargs)
1✔
179
        cm = _plt.get_cmap(cmap, n_colors)
1✔
180

181
        date_time = _dates.date2num(df.index)
1✔
182

183
        data_frequency = get_frequency_of_data(df)
1✔
184

185
        bar_width = None
1✔
186
        if data_frequency == "step":
1✔
187
            bar_width = 0.0008
1✔
188
        elif data_frequency == "hourly":
1✔
189
            bar_width = 0.02
1✔
190
        elif data_frequency == "monthly":
1✔
191
            bar_width = 25
1✔
192

193
        # build positive stack
194
        i_color = 0
1✔
195
        for column in q_in_columns:
1✔
196
            values = df[column].clip(lower=0)
1✔
197
            lax.bar(
1✔
198
                date_time,
199
                values,
200
                bottom=pos_bottom,
201
                label=column,
202
                width=bar_width,
203
                color=cm(i_color),
204
            )
205
            pos_bottom += values
1✔
206
            i_color += 1
1✔
207

208
        # build negative stack
209
        for column in q_out_columns:
1✔
210
            values = df[column].clip(upper=0)
1✔
211
            lax.bar(
1✔
212
                date_time,
213
                values,
214
                bottom=neg_bottom,
215
                label=column,
216
                width=bar_width,
217
                color=cm(i_color),
218
            )
219
            neg_bottom += values
1✔
220
            i_color += 1
1✔
221

222
        values = df[q_imb_column]
1✔
223
        lax.bar(
1✔
224
            date_time,
225
            values.clip(lower=0),
226
            bottom=pos_bottom,
227
            label=q_imb_column,
228
            width=bar_width,
229
            color="black",
230
        )
231
        pos_bottom += values.clip(lower=0)  # used for legend location
1✔
232
        i_color += 1
1✔
233
        lax.bar(
1✔
234
            date_time,
235
            values.clip(upper=0),
236
            bottom=neg_bottom,
237
            width=bar_width,
238
            color="black",
239
        )
240
        neg_bottom += values.clip(upper=0)  # used for legend location
1✔
241

242
        for column in line_columns:
1✔
243
            rax.plot(date_time, df[column], label=f"{column}")
1✔
244

245
        lax.axhline(0, color="black")
1✔
246

247
        format_date_time_twin_axis(lax, rax, data_frequency)
1✔
248

249
        if use_legend:
1✔
250
            balance_handles, _ = lax.get_legend_handles_labels()
1✔
251
            line_handles, _ = rax.get_legend_handles_labels()
1✔
252
            line_legend = leg_ax.legend(
1✔
253
                handles=line_handles,
254
                loc="upper left",
255
                bbox_to_anchor=(0, 0, 1, 1),
256
            )
257
            leg_ax.add_artist(line_legend)
1✔
258
            leg_ax.legend(
1✔
259
                handles=balance_handles,
260
                loc="upper left",
261
                bbox_to_anchor=(0, 0, 1, 0.7),
262
            )
263
            leg_ax.axis("off")
1✔
264

265
        if "xlabel" in kwargs:
1✔
266
            lax.set_xlabel(kwargs["xlabel"])
1✔
267
        if "energy_balance_ylabel" in kwargs:
1✔
268
            lax.set_ylabel(kwargs["energy_balance_ylabel"])
1✔
269
        if "line_ylabel" in kwargs:
1✔
270
            rax.set_ylabel(kwargs["line_ylabel"])
1✔
271

272
        return fig, lax, rax
1✔
273

274

275
class BarChart(ChartBase):
1✔
276
    cmap = None
1✔
277

278
    def _do_plot(
1✔
279
        self,
280
        df: _pd.DataFrame,
281
        columns: list[str],
282
        use_legend: bool = True,
283
        size: tuple[float, float] = conf.PlotSizes.A4.value,
284
        **kwargs: _tp.Any,
285
    ) -> tuple[_plt.Figure, _plt.Axes]:
286
        # TODO: deal with colors  # pylint: disable=fixme
287
        fig, ax = self.get_fig_and_ax(kwargs, size)
1✔
288

289
        x = _np.arange(len(df.index))
1✔
290
        width = 0.8 / len(columns)
1✔
291

292
        cmap = self.get_cmap(kwargs)
1✔
293
        if cmap:
1✔
294
            cm = _plt.get_cmap(cmap)
1✔
295
            colors = cm(_np.linspace(0, 1, len(columns)))
1✔
296
        else:
297
            colors = [None] * len(columns)
1✔
298

299
        for i, col in enumerate(columns):
1✔
300
            ax.bar(x + i * width, df[col], width, label=col, color=colors[i])
1✔
301

302
        if use_legend:
1✔
303
            ax.legend()
1✔
304

305
        ax.set_xticks(x + width * (len(columns) - 1) / 2)
1✔
306
        ax.set_xticklabels(
1✔
307
            _pd.to_datetime(df.index).strftime(plot_settings.date_format)
308
        )
309
        ax.tick_params(axis="x", labelrotation=90)
1✔
310
        return fig, ax
1✔
311

312

313
class LinePlot(ChartBase):
1✔
314
    cmap: str | None = None
1✔
315

316
    def _do_plot(
1✔
317
        self,
318
        df: _pd.DataFrame,
319
        columns: list[str],
320
        use_legend: bool = True,
321
        size: tuple[float, float] = conf.PlotSizes.A4.value,
322
        **kwargs: _tp.Any,
323
    ) -> tuple[_plt.Figure, _plt.Axes]:
324
        fig, ax = self.get_fig_and_ax(kwargs, size)
1✔
325

326
        plot_kwargs = {
1✔
327
            "legend": use_legend,
328
            **kwargs,
329
        }
330
        self.check_for_cmap(kwargs, plot_kwargs)
1✔
331

332
        df[columns].plot.line(**plot_kwargs)
1✔
333
        return fig, ax
1✔
334

335

336
@dataclass()
1✔
337
class Histogram(ChartBase):
1✔
338
    bins: int = 50
1✔
339

340
    def _do_plot(
1✔
341
        self,
342
        df: _pd.DataFrame,
343
        columns: list[str],
344
        use_legend: bool = True,
345
        size: tuple[float, float] = conf.PlotSizes.A4.value,
346
        **kwargs: _tp.Any,
347
    ) -> tuple[_plt.Figure, _plt.Axes]:
348
        fig, ax = self.get_fig_and_ax(kwargs, size)
1✔
349

350
        plot_kwargs = {
1✔
351
            "legend": use_legend,
352
            "bins": self.bins,
353
            **kwargs,
354
        }
355
        self.check_for_cmap(kwargs, plot_kwargs)
1✔
356
        df[columns].plot.hist(**plot_kwargs)
1✔
357
        return fig, ax
1✔
358

359

360
def _validate_inputs(
1✔
361
    current_class,
362
    columns: list[str],
363
) -> None:
364
    if len(columns) != 2:
1✔
365
        raise ValueError(
1✔
366
            f"\n{type(current_class).__name__} requires exactly 2 columns (x and y)"
367
        )
368

369

370
class ScatterPlot(ChartBase):
1✔
371
    cmap = "Paired"  # This is ignored when no categorical groupings are used.
1✔
372

373
    def _do_plot(
1✔
374
        self,
375
        df: _pd.DataFrame,
376
        columns: list[str],
377
        use_legend: bool = True,
378
        size: tuple[float, float] = conf.PlotSizes.A4.value,
379
        **kwargs: _tp.Any,
380
    ) -> tuple[_plt.Figure, _plt.Axes]:
381
        _validate_inputs(self, columns)
1✔
382
        x_column, y_column = columns
1✔
383

384
        fig, ax = self.get_fig_and_ax(kwargs, size)
1✔
385
        df.plot.scatter(x=x_column, y=y_column, **kwargs)
1✔
386

387
        return fig, ax
1✔
388

389

390
class ScalarComparePlot(ChartBase):
1✔
391
    """Handles comparative scatter plots with dual grouping by color and markers."""
392

393
    cmap = "Paired"  # This is ignored when no categorical groupings are used.
1✔
394

395
    # pylint: disable=too-many-arguments,too-many-locals, too-many-positional-arguments
396
    def _do_plot(  # type: ignore[override]
1✔
397
        self,
398
        df: _pd.DataFrame,
399
        columns: list[str],
400
        use_legend: bool = True,
401
        size: tuple[float, float] = conf.PlotSizes.A4.value,
402
        group_by_color: str | None = None,
403
        group_by_marker: str | None = None,
404
        line_kwargs: dict[str, _tp.Any] | None = None,
405
        scatter_kwargs: dict[str, _tp.Any] | None = None,
406
    ) -> tuple[_plt.Figure, _plt.Axes]:
407

408
        _validate_inputs(self, columns)
1✔
409
        x_column, y_column = columns
1✔
410

411
        # ===========================================
412
        # The following simplifies the code later on,
413
        # while being compatible with linting.
414
        if not line_kwargs:
1✔
415
            line_kwargs = {}
1✔
416
        if not scatter_kwargs:
1✔
417
            scatter_kwargs = {}
1✔
418
        # ===========================================
419

420
        if group_by_color and group_by_marker:
1✔
421
            fig, ax, lax = self.prep_subplots_for_legend_outside_of_plot(size)
1✔
422
            secondary_axis_used = True
1✔
423
        else:
424
            secondary_axis_used = False
1✔
425
            fig, ax = self.get_fig_and_ax({}, size)
1✔
426
            lax = ax
1✔
427

428
        df_grouped, group_values = self._prepare_grouping(
1✔
429
            df, group_by_color, group_by_marker
430
        )
431
        cmap = self.get_cmap(line_kwargs)
1✔
432
        color_map, marker_map = self._create_style_mappings(
1✔
433
            *group_values, cmap=cmap
434
        )
435

436
        self._plot_groups(
1✔
437
            df_grouped,
438
            x_column,
439
            y_column,
440
            color_map,
441
            marker_map,
442
            ax,
443
            line_kwargs,
444
            scatter_kwargs,
445
        )
446

447
        use_color_legend = False
1✔
448
        if group_by_color:
1✔
449
            use_color_legend = True
1✔
450

451
        if use_legend:
1✔
452
            self._create_legends(
1✔
453
                lax,
454
                color_map,
455
                marker_map,
456
                group_by_color,
457
                group_by_marker,
458
                use_color_legend=use_color_legend,
459
                secondary_axis_used=secondary_axis_used,
460
            )
461

462
        return fig, ax
1✔
463

464
    @staticmethod
1✔
465
    def _prepare_grouping(
1✔
466
        df: _pd.DataFrame,
467
        by_color: str | None,
468
        by_marker: str | None,
469
    ) -> tuple[
470
        _pd.core.groupby.generic.DataFrameGroupBy, tuple[list[str], list[str]]
471
    ]:
472
        group_by = []
1✔
473
        if by_color:
1✔
474
            group_by.append(by_color)
1✔
475
        if by_marker:
1✔
476
            group_by.append(by_marker)
1✔
477

478
        df_grouped = df.groupby(group_by)
1✔
479

480
        color_values = sorted(df[by_color].unique()) if by_color else []
1✔
481
        marker_values = sorted(df[by_marker].unique()) if by_marker else []
1✔
482

483
        return df_grouped, (color_values, marker_values)
1✔
484

485
    @staticmethod
1✔
486
    def _create_style_mappings(
1✔
487
        color_values: list[str],
488
        marker_values: list[str],
489
        cmap: str | None,
490
    ) -> tuple[dict[str, _tp.Any], dict[str, str]]:
491
        if color_values:
1✔
492
            cm = _plt.get_cmap(cmap, len(color_values))
1✔
493
            color_map = {val: cm(i) for i, val in enumerate(color_values)}
1✔
494
        else:
495
            cm = _plt.get_cmap(cmap, len(marker_values))
1✔
496
            color_map = {val: cm(i) for i, val in enumerate(marker_values)}
1✔
497
        if marker_values:
1✔
498
            marker_map = dict(zip(marker_values, plot_settings.markers))
1✔
499
        else:
500
            marker_map = {}
1✔
501

502
        return color_map, marker_map
1✔
503

504
    # pylint: disable=too-many-arguments
505
    @staticmethod
1✔
506
    def _plot_groups(
1✔
507
        df_grouped: _pd.core.groupby.generic.DataFrameGroupBy,
508
        x_column: str,
509
        y_column: str,
510
        color_map: dict[str, _tp.Any],
511
        marker_map: dict[str, str] | str,
512
        ax: _plt.Axes,
513
        line_kwargs: dict[str, _tp.Any],
514
        scatter_kwargs: dict[str, _tp.Any],
515
    ) -> None:
516
        ax.set_xlabel(x_column, fontsize=plot_settings.label_font_size)
1✔
517
        ax.set_ylabel(y_column, fontsize=plot_settings.label_font_size)
1✔
518
        for val, group in df_grouped:
1✔
519
            sorted_group = group.sort_values(x_column)
1✔
520
            x = sorted_group[x_column]
1✔
521
            y = sorted_group[y_column]
1✔
522

523
            plot_args = {"color": "black"}
1✔
524
            if color_map:
1✔
525
                plot_args["color"] = color_map[val[0]]
1✔
526

527
            for key, value in line_kwargs.items():
1✔
528
                if key not in ["cmap", "colormap"]:
1✔
529
                    plot_args[key] = value
1✔
530

531
            scatter_args = {"marker": "None", "color": "black", "alpha": 0.5}
1✔
532
            if marker_map:
1✔
533
                scatter_args["marker"] = marker_map[val[-1]]
1✔
534

535
            for key, value in scatter_kwargs.items():
1✔
536
                if key in ["marker"] and marker_map:
1✔
537
                    continue
1✔
538

539
                scatter_args[key] = value
1✔
540

541
            ax.plot(x, y, **plot_args)  # type: ignore
1✔
542
            ax.scatter(x, y, **scatter_args)  # type: ignore
1✔
543

544
    def _create_legends(
1✔
545
        self,
546
        lax: _plt.Axes,
547
        color_map: dict[str, _tp.Any],
548
        marker_map: dict[str, str],
549
        color_legend_title: str | None,
550
        marker_legend_title: str | None,
551
        use_color_legend: bool,
552
        secondary_axis_used: bool,
553
    ) -> None:
554

555
        if secondary_axis_used:
1✔
556
            # Secondary axis should be turned off.
557
            # Primary axis should stay the same.
558
            lax.axis("off")
1✔
559

560
        if use_color_legend:
1✔
561
            self._create_color_legend(
1✔
562
                lax,
563
                color_map,
564
                color_legend_title,
565
                bool(marker_map),
566
                secondary_axis_used,
567
            )
568
        if marker_map:
1✔
569
            self._create_marker_legend(
1✔
570
                lax,
571
                marker_map,
572
                marker_legend_title,
573
                bool(use_color_legend),
574
                secondary_axis_used,
575
            )
576

577
    @staticmethod
1✔
578
    def _create_color_legend(
1✔
579
        lax: _plt.Axes,
580
        color_map: dict[str, _tp.Any],
581
        color_legend_title: str | None,
582
        has_markers: bool,
583
        secondary_axis_used: bool,
584
    ) -> None:
585
        color_handles = [
1✔
586
            _plt.Line2D([], [], color=color, linestyle="-", label=label)
587
            for label, color in color_map.items()
588
        ]
589

590
        if secondary_axis_used:
1✔
591
            loc = "upper left"
1✔
592
            alignment = "left"
1✔
593
        else:
594
            loc = "best"
1✔
595
            alignment = "center"
1✔
596

597
        legend = lax.legend(
1✔
598
            handles=color_handles,
599
            title=color_legend_title,
600
            bbox_to_anchor=(0, 0, 1, 1),
601
            loc=loc,
602
            alignment=alignment,
603
            fontsize=plot_settings.legend_font_size,
604
            borderaxespad=0,
605
        )
606

607
        if has_markers:
1✔
608
            lax.add_artist(legend)
1✔
609

610
    @staticmethod
1✔
611
    def _create_marker_legend(
1✔
612
        lax: _plt.Axes,
613
        marker_map: dict[str, str],
614
        marker_legend_title: str | None,
615
        has_colors: bool,
616
        secondary_axis_used: bool,
617
    ) -> None:
618
        marker_position = 0.7 if has_colors else 1
1✔
619
        marker_handles = [
1✔
620
            _plt.Line2D(
621
                [],
622
                [],
623
                color="black",
624
                marker=marker,
625
                linestyle="None",
626
                label=label,
627
            )
628
            for label, marker in marker_map.items()
629
            if label is not None
630
        ]
631

632
        if secondary_axis_used:
1✔
633
            loc = "upper left"
1✔
634
            alignment = "left"
1✔
635
        else:
636
            loc = "best"
1✔
637
            alignment = "center"
1✔
638

639
        lax.legend(
1✔
640
            handles=marker_handles,
641
            title=marker_legend_title,
642
            bbox_to_anchor=(0, 0, 1, marker_position),
643
            loc=loc,
644
            alignment=alignment,
645
            fontsize=plot_settings.legend_font_size,
646
            borderaxespad=0,
647
        )
648

649

650
def get_date_time_axis_locator_and_formatter(
1✔
651
    data_frequency: _tp.Literal["step", "hourly", "monthly"],
652
):
653
    """
654
    Method to prepare an axis locator and a date time formattter to adjust the date time formatting.
655
    Can be used as follows:
656
    ax.xaxis.set_major_formatter(formatter)
657
    ax.xaxis.set_major_locator(date_locator)
658

659
    Parameters
660
    ----------
661
    data_frequency: str
662
        Size of the timestep. This can be 'step', 'hourly', and 'monthly'.
663
    """
664
    sub_hour_interval = 15  # minutes
1✔
665

666
    def formatter_hourly_with_midnight_date(
1✔
667
        x, pos
668
    ):  # pylint: disable=unused-argument
669
        dt = _dates.num2date(x)
1✔
670
        if dt.hour == 0:
1✔
671
            return dt.strftime("%b-%d")
1✔
672

673
        return dt.strftime("%H")
1✔
674

675
    def formatter_sub_hour_with_midnight_date(
1✔
676
        x, pos
677
    ):  # pylint: disable=unused-argument
678
        dt = _dates.num2date(x)
1✔
679
        if dt.hour == 0 and dt.minute < sub_hour_interval:
1✔
680
            return dt.strftime("%b-%d")
1✔
681

682
        return dt.strftime("%H:%M")
1✔
683

684
    def formatter_monthly(  # pylint: disable=unused-argument
1✔
685
        x, pos
686
    ) -> _tp.Tuple[_dates.RRuleLocator, _tick.FuncFormatter]:
687
        dt = _dates.num2date(x, tz=None)
1✔
688
        return dt.strftime("%b")
1✔
689

690
    date_locator: _dates.RRuleLocator | None = None
1✔
691
    formatter_function = None
1✔
692
    if data_frequency == "step":
1✔
693
        date_locator = _dates.MinuteLocator(interval=sub_hour_interval)
1✔
694
        formatter_function = formatter_sub_hour_with_midnight_date
1✔
695
    elif data_frequency == "hourly":
1✔
696
        date_locator = _dates.HourLocator()
1✔
697
        formatter_function = formatter_hourly_with_midnight_date
1✔
698
    elif data_frequency == "monthly":
1✔
699
        date_locator = _dates.MonthLocator()
1✔
700
        formatter_function = formatter_monthly
1✔
701
    else:
NEW
702
        raise ValueError(f"Incorrect data frequency: {data_frequency}")
×
703

704
    formatter = _tick.FuncFormatter(formatter_function)  # type: ignore[arg-type]
1✔
705

706
    return date_locator, formatter
1✔
707

708

709
def get_frequency_of_data(
1✔
710
    df: _pd.DataFrame,
711
) -> _tp.Literal["step", "hourly", "monthly"]:
712
    """
713
    Method to identify the timestep size of the give dataframe.
714
    Can return 'step', 'hourly', and 'monthly'.
715
    """
716
    delta_time = df.index[1] - df.index[0]
1✔
717
    data_frequency = None
1✔
718
    if delta_time < _pd.Timedelta(hours=1):
1✔
719
        data_frequency = "step"
1✔
720
    elif delta_time == _pd.Timedelta(hours=1):
1✔
721
        data_frequency = "hourly"
1✔
722
    elif delta_time >= _pd.Timedelta(days=28):
1✔
723
        data_frequency = "monthly"
1✔
724
    else:
NEW
725
        raise ValueError(
×
726
            f"Timesteps should be hourly, monthly, or less then hourly, recieved: {delta_time}"
727
        )
728

729
    return data_frequency  # type: ignore[return-value]
1✔
730

731

732
def format_date_time_twin_axis(
1✔
733
    lax: _plt.Axes,
734
    rax: _plt.Axes,
735
    data_frequency: _tp.Literal["step", "hourly", "monthly"],
736
):
737
    """Method to update dateTime formatting for twin axes.
738

739
    Parameters
740
    ----------
741
    lax: _plt.Axes
742
        left-axis handle
743

744
    rax: _plt.Axes
745
        right-axis handle
746

747
    data_frequency: str
748
        Size of the timestep. This can be 'step', 'hourly', and 'monthly'.
749
    """
750
    date_locator, formatter = get_date_time_axis_locator_and_formatter(
1✔
751
        data_frequency
752
    )
753
    lax.xaxis_date()
1✔
754
    lax.xaxis.set_major_formatter(formatter)
1✔
755
    lax.xaxis.set_major_locator(date_locator)
1✔
756
    rax.xaxis_date()
1✔
757
    rax.xaxis.set_major_formatter(formatter)
1✔
758
    rax.xaxis.set_major_locator(date_locator)
1✔
759
    lax.tick_params(axis="x", rotation=90)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc