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

SPF-OST / pytrnsys_process / 26558912126

28 May 2026 06:34AM UTC coverage: 95.936% (-0.01%) from 95.948%
26558912126

push

github

ahobeost
Refactoring to pass CI.

11 of 11 new or added lines in 2 files covered. (100.0%)

3 existing lines in 2 files now uncovered.

1322 of 1378 relevant lines covered (95.94%)

1.92 hits per line

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

98.85
/pytrnsys_process/plot/plot_wrappers.py
1
"""Plotting wrappers to provide a simplified interface to the User, while allow development of reusable OOP structures.
2

3
Note
4
____
5
    Many of these plotting routines do not add labels and legends.
6
    This should be done using the figure and axis handles afterwards.
7
"""
8

9
import typing as _tp
2✔
10
from collections import abc as _abc
2✔
11

12
import matplotlib.pyplot as _plt
2✔
13
import pandas as _pd
2✔
14

15
from pytrnsys_process import config as conf
2✔
16
from pytrnsys_process.plot import plotters as pltrs
2✔
17

18

19
def line_plot(
2✔
20
    df: _pd.DataFrame,
21
    columns: list[str],
22
    use_legend: bool = True,
23
    size: tuple[float, float] = conf.PlotSizes.A4.value,
24
    **kwargs: _tp.Any,
25
) -> tuple[_plt.Figure, _plt.Axes]:
26
    """
27
    Create a line plot using the provided DataFrame columns.
28

29
    Parameters
30
    __________
31
    df : pandas.DataFrame
32
        the dataframe to plot
33

34
    columns: list of str
35
        names of columns to plot
36

37
    use_legend: bool, default 'True'
38
        whether to show the legend or not
39

40
    size: tuple of (float, float)
41
        size of the figure (width, height)
42

43
    **kwargs :
44
        Additional keyword arguments are documented in
45
        :meth:`pandas.DataFrame.plot`.
46

47
    Returns
48
    _______
49
    tuple of (:class:`matplotlib.figure.Figure`, :class:`matplotlib.axes.Axes`)
50

51
    Examples
52
    ________
53
    .. plot::
54
        :context: close-figs
55

56
        >>> api.line_plot(simulation.hourly, ["QSrc1TIn", "QSrc1TOut"])
57
    """
58
    _validate_column_exists(df, columns)
2✔
59
    plotter = pltrs.LinePlot()
2✔
60
    return plotter.plot(
2✔
61
        df, columns, use_legend=use_legend, size=size, **kwargs
62
    )
63

64

65
def bar_chart(
2✔
66
    df: _pd.DataFrame,
67
    columns: list[str],
68
    use_legend: bool = True,
69
    size: tuple[float, float] = conf.PlotSizes.A4.value,
70
    **kwargs: _tp.Any,
71
) -> tuple[_plt.Figure, _plt.Axes]:
72
    """
73
    Create a bar chart with multiple columns displayed as grouped bars.
74
    The **kwargs are currently not passed on.
75

76
    Parameters
77
    __________
78
    df : pandas.DataFrame
79
        the dataframe to plot
80

81
    columns: list of str
82
        names of columns to plot
83

84
    use_legend: bool, default 'True'
85
        whether to show the legend or not
86

87
    size: tuple of (float, float)
88
        size of the figure (width, height)
89

90
    **kwargs :
91
        Additional keyword arguments to pass on to
92
        :meth:`pandas.DataFrame.plot`.
93

94
    Returns
95
    _______
96
    tuple of (:class:`matplotlib.figure.Figure`, :class:`matplotlib.axes.Axes`)
97

98
    Examples
99
    ________
100
    .. plot::
101
        :context: close-figs
102

103
        >>> api.bar_chart(simulation.monthly, ["QSnk60P","QSnk60PauxCondSwitch_kW"])
104
    """
105
    _validate_column_exists(df, columns)
2✔
106
    plotter = pltrs.BarChart()
2✔
107
    return plotter.plot(
2✔
108
        df, columns, use_legend=use_legend, size=size, **kwargs
109
    )
110

111

112
def stacked_bar_chart(
2✔
113
    df: _pd.DataFrame,
114
    columns: list[str],
115
    use_legend: bool = True,
116
    size: tuple[float, float] = conf.PlotSizes.A4.value,
117
    **kwargs: _tp.Any,
118
) -> tuple[_plt.Figure, _plt.Axes]:
119
    """
120
    Bar chart with stacked bars
121

122
    Parameters
123
    __________
124
    df : pandas.DataFrame
125
        the dataframe to plot
126

127
    columns: list of str
128
        names of columns to plot
129

130
    use_legend: bool, default 'True'
131
        whether to show the legend or not
132

133
    size: tuple of (float, float)
134
        size of the figure (width, height)
135

136
    **kwargs :
137
        Additional keyword arguments to pass on to
138
        :meth:`pandas.DataFrame.plot`.
139

140
    Returns
141
    _______
142
    tuple of (:class:`matplotlib.figure.Figure`, :class:`matplotlib.axes.Axes`)
143

144
    Examples
145
    ________
146
    .. plot::
147
        :context: close-figs
148

149
        >>> api.stacked_bar_chart(simulation.monthly, ["QSnk60P","QSnk60PauxCondSwitch_kW"])
150
    """
151
    _validate_column_exists(df, columns)
2✔
152
    plotter = pltrs.StackedBarChart()
2✔
153
    return plotter.plot(
2✔
154
        df, columns, use_legend=use_legend, size=size, **kwargs
155
    )
156

157

158
def histogram(
2✔
159
    df: _pd.DataFrame,
160
    columns: list[str],
161
    use_legend: bool = True,
162
    size: tuple[float, float] = conf.PlotSizes.A4.value,
163
    bins: int = 50,
164
    **kwargs: _tp.Any,
165
) -> tuple[_plt.Figure, _plt.Axes]:
166
    """
167
    Create a histogram from the given DataFrame columns.
168

169
    Parameters
170
    __________
171
    df : pandas.DataFrame
172
        the dataframe to plot
173

174
    columns: list of str
175
        names of columns to plot
176

177
    use_legend: bool, default 'True'
178
        whether to show the legend or not
179

180
    size: tuple of (float, float)
181
        size of the figure (width, height)
182

183
    bins: int
184
        number of histogram bins to be used
185

186
    **kwargs :
187
        Additional keyword arguments to pass on to
188
        :meth:`pandas.DataFrame.plot`.
189

190
    Returns
191
    _______
192
    tuple of (:class:`matplotlib.figure.Figure`, :class:`matplotlib.axes.Axes`)
193

194
    Examples
195
    ________
196
    .. plot::
197
        :context: close-figs
198

199
        >>> api.histogram(simulation.hourly, ["QSrc1TIn"], ylabel="")
200
    """
201
    _validate_column_exists(df, columns)
2✔
202
    plotter = pltrs.Histogram(bins)
2✔
203
    return plotter.plot(
2✔
204
        df, columns, use_legend=use_legend, size=size, **kwargs
205
    )
206

207

208
def energy_balance(
2✔
209
    df: _pd.DataFrame,
210
    q_in_columns: list[str],
211
    q_out_columns: list[str],
212
    q_imb_column: _tp.Optional[str] = None,
213
    use_legend: bool = True,
214
    size: tuple[float, float] = conf.PlotSizes.A4.value,
215
    **kwargs: _tp.Any,
216
) -> tuple[_plt.Figure, _plt.Axes]:
217
    """
218
    Create a stacked bar chart showing energy balance with inputs, outputs and imbalance.
219
    This function creates an energy balance visualization where:
220

221
    - Input energies are shown as positive values
222
    - Output energies are shown as negative values
223
    - Energy imbalance is either provided or calculated as (sum of inputs + sum of outputs)
224

225
    Parameters
226
    __________
227
    df : pandas.DataFrame
228
        the dataframe to plot
229

230
    q_in_columns: list of str
231
        column names representing energy inputs
232

233
    q_out_columns: list of str
234
        column names representing energy outputs
235

236
    q_imb_column: list of str, optional
237
        column name containing pre-calculated energy imbalance
238

239
    use_legend: bool, default 'True'
240
        whether to show the legend or not
241

242
    size: tuple of (float, float)
243
        size of the figure (width, height)
244

245
    **kwargs :
246
        Additional keyword arguments to pass on to
247
        :meth:`pandas.DataFrame.plot`.
248

249
    Returns
250
    _______
251
    tuple of (:class:`matplotlib.figure.Figure`, :class:`matplotlib.axes.Axes`)
252

253
    Examples
254
    ________
255
    .. plot::
256
        :context: close-figs
257

258
        >>> api.energy_balance(
259
        >>> simulation.monthly,
260
        >>> q_in_columns=["QSnk60PauxCondSwitch_kW"],
261
        >>> q_out_columns=["QSnk60P", "QSnk60dQlossTess", "QSnk60dQ"],
262
        >>> q_imb_column="QSnk60qImbTess",
263
        >>> xlabel=""
264
        >>> )
265
    """
266
    line_columns = None
2✔
267

268
    if "ylabel" in kwargs:
2✔
UNCOV
269
        kwargs["energy_balance_ylabel"] = kwargs["ylabel"]
×
270

271
    fig, lax, _ = energy_balance_with_lines(
2✔
272
        df,
273
        q_in_columns,
274
        q_out_columns,
275
        line_columns,
276
        q_imb_column,
277
        use_legend,
278
        size,
279
        **kwargs,
280
    )
281

282
    return fig, lax
2✔
283

284

285
# pylint: disable=too-many-arguments
286
def energy_balance_with_lines(
2✔
287
    df: _pd.DataFrame,
288
    q_in_columns: list[str],
289
    q_out_columns: list[str],
290
    line_columns: _tp.Optional[list[str]] = None,
291
    q_imb_column: _tp.Optional[str] = None,
292
    use_legend: bool = True,
293
    size: tuple[float, float] = conf.PlotSizes.A4.value,
294
    **kwargs: _tp.Any,
295
) -> tuple[_plt.Figure, _plt.Axes, _plt.Axes]:
296
    """
297
    Create a stacked bar chart showing energy balance with inputs, outputs and imbalance.
298
    On top of which one or more lines will be plotted.
299

300
    This function creates an energy balance visualization where:
301

302
    - Input energies are shown as positive values
303
    - Output energies are shown as negative values
304
    - Energy imbalance is either provided or calculated as (sum of inputs + sum of outputs)
305

306
    Parameters
307
    __________
308
    df : pandas.DataFrame
309
        the dataframe to plot
310

311
    q_in_columns: list of str
312
        column names representing energy inputs
313

314
    q_out_columns: list of str
315
        column names representing energy outputs
316

317
    q_imb_column: list of str, optional
318
        column name containing pre-calculated energy imbalance
319

320
    line_columns: list of str
321
        column names that should be plotted as line on top of the energy balance.
322

323
    use_legend: bool, default 'True'
324
        whether to show the legend or not
325

326
    size: tuple of (float, float)
327
        size of the figure (width, height)
328

329
    energy_balance_ylabel: str
330
        y-axis label for the energy balance.
331

332
    line_ylabel: str
333
        y-axis label for the lines.
334

335
    **kwargs :
336
        Additional keyword arguments to pass on to
337
        :meth:`pandas.DataFrame.plot`.
338

339
    Returns
340
    _______
341
    tuple of (:class:`matplotlib.figure.Figure`, :class:`matplotlib.axes.Axes`)
342

343
    Examples
344
    ________
345
    .. plot::
346
        :context: close-figs
347

348
        >>> api.energy_balance_with_lines(
349
        >>> simulation.monthly,
350
        >>> q_in_columns=["QSnk60PauxCondSwitch_kW"],
351
        >>> q_out_columns=["QSnk60P", "QSnk60dQlossTess", "QSnk60dQ"],
352
        >>> q_imb_column="QSnk60qImbTess",
353
        >>> xlabel=""
354
        >>> )
355
    """
356
    all_columns_for_validation = (
2✔
357
        q_in_columns
358
        + q_out_columns
359
        + (line_columns if line_columns is not None else [])
360
        + ([q_imb_column] if q_imb_column is not None else [])
361
    )
362
    _validate_column_exists(df, all_columns_for_validation)
2✔
363

364
    df_modified = df.copy()
2✔
365

366
    for col in q_out_columns:
2✔
367
        df_modified[col] = -df_modified[col]
2✔
368

369
    if q_imb_column is None:
2✔
370
        q_imb_column = "Qimb"
2✔
371
        df_modified[q_imb_column] = df_modified[
2✔
372
            q_in_columns + q_out_columns
373
        ].sum(axis=1)
374

375
    # imbalance is visually added where it is missing.
376
    df_modified[q_imb_column] *= -1
2✔
377

378
    columns_to_plot = {
2✔
379
        "q_in_columns": q_in_columns,
380
        "q_out_columns": q_out_columns,
381
        "q_imb_column": q_imb_column,
382
        "line_columns": line_columns,
383
    }
384

385
    plotter = pltrs.EnergyBalanceChart()
2✔
386
    return plotter.plot(  # type: ignore[return-value]
2✔
387
        df_modified,
388
        columns_to_plot,
389
        use_legend=use_legend,
390
        size=size,
391
        **kwargs,
392
    )
393

394

395
def scatter_plot(
2✔
396
    df: _pd.DataFrame,
397
    x_column: str,
398
    y_column: str,
399
    use_legend: bool = True,
400
    size: tuple[float, float] = conf.PlotSizes.A4.value,
401
    **kwargs: _tp.Any,
402
) -> tuple[_plt.Figure, _plt.Axes]:
403
    """
404
    Create a scatter plot to show numerical relationships between x and y variables.
405

406
    Note
407
    ____
408
    Use color and not cmap!
409

410
    See: https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.plot.scatter.html
411

412

413
    Parameters
414
    __________
415
    df : pandas.DataFrame
416
        the dataframe to plot
417

418
    x_column: str
419
        coloumn name for x-axis values
420

421
    y_column: str
422
        coloumn name for y-axis values
423

424

425
    use_legend: bool, default 'True'
426
        whether to show the legend or not
427

428
    size: tuple of (float, float)
429
        size of the figure (width, height)
430

431
    **kwargs :
432
        Additional keyword arguments to pass on to
433
        :meth:`pandas.DataFrame.plot.scatter`.
434

435
    Returns
436
    _______
437
    tuple of (:class:`matplotlib.figure.Figure`, :class:`matplotlib.axes.Axes`)
438

439
    Examples
440
    ________
441
    .. plot::
442
        :context: close-figs
443

444
        Simple scatter plot
445

446
        >>> api.scatter_plot(
447
        ...     simulation.monthly, x_column="QSnk60dQlossTess", y_column="QSnk60dQ"
448
        ... )
449

450
    """
451
    if "cmap" in kwargs:
2✔
452
        raise ValueError(
2✔
453
            "\nscatter_plot does not take a 'cmap'."
454
            "\nPlease use color instead."
455
        )
456

457
    columns_to_validate = [x_column, y_column]
2✔
458
    _validate_column_exists(df, [x_column, y_column])
2✔
459
    df = df[columns_to_validate]
2✔
460
    plotter = pltrs.ScatterPlot()
2✔
461

462
    return plotter.plot(
2✔
463
        df,
464
        columns=[x_column, y_column],
465
        use_legend=use_legend,
466
        size=size,
467
        **kwargs,
468
    )
469

470

471
# pylint: disable=too-many-arguments, too-many-positional-arguments
472
def scalar_compare_plot(
2✔
473
    df: _pd.DataFrame,
474
    x_column: str,
475
    y_column: str,
476
    group_by_color: str | None = None,
477
    group_by_marker: str | None = None,
478
    use_legend: bool = True,
479
    size: tuple[float, float] = conf.PlotSizes.A4.value,
480
    scatter_kwargs: dict[str, _tp.Any] | None = None,
481
    line_kwargs: dict[str, _tp.Any] | None = None,
482
    **kwargs: _tp.Any,
483
) -> tuple[_plt.Figure, _plt.Axes]:
484
    """
485
    Create a scalar comparison plot with up to two grouping variables.
486
    This visualization allows simultaneous analysis of:
487

488
    - Numerical relationships between x and y variables
489
    - Categorical grouping through color encoding
490
    - Secondary categorical grouping through marker styles
491

492
    Note
493
    ____
494
    To change the figure properties a separation is included.
495
    scatter_kwargs are used to change the markers.
496
    line_kwargs are used to change the lines.
497

498
    See:
499
    - markers: https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.scatter.html
500
    - lines: https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.plot.html
501

502

503
    Parameters
504
    __________
505
    df : pandas.DataFrame
506
        the dataframe to plot
507

508
    x_column: str
509
        column name for x-axis values
510

511
    y_column: str
512
        column name for y-axis values
513

514
    group_by_color: str, optional
515
        column name for color grouping
516

517
    group_by_marker: str, optional
518
        column name for marker style grouping
519

520
    use_legend: bool, default 'True'
521
        whether to show the legend or not
522

523
    size: tuple of (float, float)
524
        size of the figure (width, height)
525

526
    line_kwargs:
527
        Additional keyword arguments to pass on to
528
        :meth:`matplotlib.axes.Axes.plot`.
529

530
    scatter_kwargs:
531
        Additional keyword arguments to pass on to
532
        :meth:`matplotlib.axes.Axes.scatter`.
533

534
    **kwargs :
535
        Should never be used!
536
        Use 'line_kwargs' or 'scatter_kwargs' instead.
537

538

539
    Returns
540
    _______
541
    tuple of (:class:`matplotlib.figure.Figure`, :class:`matplotlib.axes.Axes`)
542

543
    Examples
544
    ________
545
    .. plot::
546
        :context: close-figs
547

548
        Compare plot
549

550
        >>> api.scalar_compare_plot(
551
        ...     comparison_data,
552
        ...     x_column="VIceSscaled",
553
        ...     y_column="VIceRatioMax",
554
        ...     group_by_color="yearly_demand_GWh",
555
        ...     group_by_marker="ratioDHWtoSH_allSinks",
556
        ... )
557

558

559
    """
560
    if kwargs:
2✔
561
        raise ValueError(
2✔
562
            f"\nTo adjust the figure properties, \nplease use the scatter_kwargs "
563
            f"to change the marker properties, \nand please use the line_kwargs "
564
            f"to change the line properties."
565
            f"\nReceived: {kwargs}"
566
        )
567

568
    if not group_by_marker and not group_by_color:
2✔
569
        raise ValueError(
2✔
570
            "\nAt least one of 'group_by_marker' or 'group_by_color' has to be set."
571
            f"\nFor a normal scatter plot, please use '{scatter_plot.__name__}'."
572
        )
573

574
    columns_to_validate = [x_column, y_column]
2✔
575
    if group_by_color:
2✔
576
        columns_to_validate.append(group_by_color)
2✔
577
    if group_by_marker:
2✔
578
        columns_to_validate.append(group_by_marker)
2✔
579
    _validate_column_exists(df, columns_to_validate)
2✔
580
    df = df[columns_to_validate]
2✔
581
    plotter = pltrs.ScalarComparePlot()
2✔
582
    return plotter.plot(
2✔
583
        df,
584
        columns=[x_column, y_column],
585
        group_by_color=group_by_color,
586
        group_by_marker=group_by_marker,
587
        use_legend=use_legend,
588
        size=size,
589
        scatter_kwargs=scatter_kwargs,
590
        line_kwargs=line_kwargs,
591
    )
592

593

594
def _validate_column_exists(
2✔
595
    df: _pd.DataFrame, columns: _abc.Sequence[str]
596
) -> None:
597
    """Validate that all requested columns exist in the DataFrame.
598

599
    Since PyTRNSYS is case-insensitive but Python is case-sensitive, this function
600
    provides helpful suggestions when columns differ only by case.
601

602
    Parameters
603
    __________
604
        df: DataFrame to check
605
        columns: Sequence of column names to validate
606

607
    Raises
608
    ______
609
        ColumnNotFoundError: If any columns are missing, with suggestions for case-mismatched names
610
    """
611
    missing_columns = set(columns) - set(df.columns)
2✔
612
    if not missing_columns:
2✔
613
        return
2✔
614

615
    # Create case-insensitive mapping of actual column names
616
    column_name_mapping = {col.casefold(): col for col in df.columns}
2✔
617

618
    # Categorize missing columns
619
    suggestions = []
2✔
620
    not_found = []
2✔
621

622
    for col in missing_columns:
2✔
623
        if col.casefold() in column_name_mapping:
2✔
624
            correct_name = column_name_mapping[col.casefold()]
2✔
625
            suggestions.append(f"'{col}' did you mean: '{correct_name}'")
2✔
626
        else:
627
            not_found.append(f"'{col}'")
2✔
628

629
    # Build error message
630
    parts = []
2✔
631
    if suggestions:
2✔
632
        parts.append(
2✔
633
            f"Case-insensitive matches found:\n{', \n'.join(suggestions)}\n"
634
        )
635
    if not_found:
2✔
636
        parts.append(f"No matches found for:\n{', \n'.join(not_found)}")
2✔
637

638
    error_msg = "Column validation failed. " + "".join(parts)
2✔
639
    raise ColumnNotFoundError(error_msg)
2✔
640

641

642
def get_figure_with_twin_x_axis() -> tuple[_plt.Figure, _plt.Axes, _plt.Axes]:
2✔
643
    """
644
    Used to make figures with different y axes on the left and right.
645
    To create such a figure, pass the lax to one plotting method and pass the rax to another.
646

647
    Warning
648
    _______
649
    Be careful when combining plots. MatPlotLib will not complain when you provide incompatible x-axes.
650
    An example:
651
    combining a time-series with dates with a histogram with temperatures.
652
    In this case, the histogram will disappear without any feedback.
653

654
    Note
655
    ____
656
    The legend of a twin_x plot is a special case:
657
    To have all entries into a single plot, use `fig.legend`
658
    https://matplotlib.org/stable/api/_as_gen/matplotlib.figure.Figure.legend.html
659

660
    To instead have two separate legends, one for each y-axis, use `lax.legend` and `rax.legend`.
661
    https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.legend.html
662

663

664
    Returns
665
    -------
666
    fig:
667
        Figure object
668

669
    lax:
670
        Axis object for the data on the left y-axis.
671

672
    rax:
673
        Axis object for the data on the right y-axis.
674

675
    Examples
676
    ________
677
    .. plot::
678
        :context: close-figs
679

680
        Twin axis plot with a single legend
681

682
        >>> fig, lax, rax = api.get_figure_with_twin_x_axis()
683
        >>> api.line_plot(simulation.monthly, ["QSnk60P",], ylabel="Power [kWh]", use_legend=False, fig=fig, ax=lax)
684
        >>> api.line_plot(simulation.monthly, ["QSnk60qImbTess", "QSnk60dQlossTess", "QSnk60dQ"], marker="*",
685
        ...     ylabel="Fluxes [kWh]", use_legend=False, fig=fig, ax=rax)
686
        >>> fig.legend(loc="center", bbox_to_anchor=(0.6, 0.7))
687

688
    .. plot::
689
        :context: close-figs
690

691
        Twin axis plot with two legends
692

693
        >>> fig, lax, rax = api.get_figure_with_twin_x_axis()
694
        >>> api.line_plot(simulation.monthly, ["QSnk60P",], ylabel="Power [kWh]", use_legend=False, fig=fig, ax=lax)
695
        >>> api.line_plot(simulation.monthly, ["QSnk60qImbTess", "QSnk60dQlossTess", "QSnk60dQ"], marker="*",
696
        ...     ylabel="Fluxes [kWh]", use_legend=False, fig=fig, ax=rax)
697
        >>> lax.legend(loc="center left")
698
        >>> rax.legend(loc="center right")
699
    """
700
    fig, lax = pltrs.ChartBase.get_fig_and_ax({}, conf.PlotSizes.A4.value)
2✔
701
    rax = lax.twinx()
2✔
702
    return fig, lax, rax
2✔
703

704

705
class ColumnNotFoundError(Exception):
2✔
706
    """This exception is raised when given column names are not available in the dataframe"""
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