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

SPF-OST / pytrnsys_process / 16748222285

05 Aug 2025 11:03AM UTC coverage: 49.518% (-46.5%) from 95.968%
16748222285

Pull #126

github

ahobeost
Reduce linux job to just test.
Pull Request #126: 125 bug step file not read when step used with type 25

5 of 6 new or added lines in 2 files covered. (83.33%)

578 existing lines in 11 files now uncovered.

616 of 1244 relevant lines covered (49.52%)

0.99 hits per line

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

20.0
/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
    """
UNCOV
58
    _validate_column_exists(df, columns)
×
UNCOV
59
    plotter = pltrs.LinePlot()
×
UNCOV
60
    return plotter.plot(
×
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
    """
UNCOV
105
    _validate_column_exists(df, columns)
×
UNCOV
106
    plotter = pltrs.BarChart()
×
UNCOV
107
    return plotter.plot(
×
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
    """
UNCOV
151
    _validate_column_exists(df, columns)
×
UNCOV
152
    plotter = pltrs.StackedBarChart()
×
UNCOV
153
    return plotter.plot(
×
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
    """
UNCOV
201
    _validate_column_exists(df, columns)
×
UNCOV
202
    plotter = pltrs.Histogram(bins)
×
UNCOV
203
    return plotter.plot(
×
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
    """
UNCOV
266
    all_columns_vor_validation = (
×
267
        q_in_columns
268
        + q_out_columns
269
        + ([q_imb_column] if q_imb_column is not None else [])
270
    )
UNCOV
271
    _validate_column_exists(df, all_columns_vor_validation)
×
272

UNCOV
273
    df_modified = df.copy()
×
274

UNCOV
275
    for col in q_out_columns:
×
UNCOV
276
        df_modified[col] = -df_modified[col]
×
277

UNCOV
278
    if q_imb_column is None:
×
UNCOV
279
        q_imb_column = "Qimb"
×
UNCOV
280
        df_modified[q_imb_column] = df_modified[
×
281
            q_in_columns + q_out_columns
282
        ].sum(axis=1)
283

UNCOV
284
    columns_to_plot = q_in_columns + q_out_columns + [q_imb_column]
×
285

UNCOV
286
    plotter = pltrs.StackedBarChart()
×
UNCOV
287
    return plotter.plot(
×
288
        df_modified,
289
        columns_to_plot,
290
        use_legend=use_legend,
291
        size=size,
292
        **kwargs,
293
    )
294

295

296
def scatter_plot(
2✔
297
    df: _pd.DataFrame,
298
    x_column: str,
299
    y_column: str,
300
    use_legend: bool = True,
301
    size: tuple[float, float] = conf.PlotSizes.A4.value,
302
    **kwargs: _tp.Any,
303
) -> tuple[_plt.Figure, _plt.Axes]:
304
    """
305
    Create a scatter plot to show numerical relationships between x and y variables.
306

307
    Note
308
    ____
309
    Use color and not cmap!
310

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

313

314
    Parameters
315
    __________
316
    df : pandas.DataFrame
317
        the dataframe to plot
318

319
    x_column: str
320
        coloumn name for x-axis values
321

322
    y_column: str
323
        coloumn name for y-axis values
324

325

326
    use_legend: bool, default 'True'
327
        whether to show the legend or not
328

329
    size: tuple of (float, float)
330
        size of the figure (width, height)
331

332
    **kwargs :
333
        Additional keyword arguments to pass on to
334
        :meth:`pandas.DataFrame.plot.scatter`.
335

336
    Returns
337
    _______
338
    tuple of (:class:`matplotlib.figure.Figure`, :class:`matplotlib.axes.Axes`)
339

340
    Examples
341
    ________
342
    .. plot::
343
        :context: close-figs
344

345
        Simple scatter plot
346

347
        >>> api.scatter_plot(
348
        ...     simulation.monthly, x_column="QSnk60dQlossTess", y_column="QSnk60dQ"
349
        ... )
350

351
    """
UNCOV
352
    if "cmap" in kwargs:
×
UNCOV
353
        raise ValueError(
×
354
            "\nscatter_plot does not take a 'cmap'."
355
            "\nPlease use color instead."
356
        )
357

UNCOV
358
    columns_to_validate = [x_column, y_column]
×
UNCOV
359
    _validate_column_exists(df, [x_column, y_column])
×
UNCOV
360
    df = df[columns_to_validate]
×
UNCOV
361
    plotter = pltrs.ScatterPlot()
×
362

UNCOV
363
    return plotter.plot(
×
364
        df,
365
        columns=[x_column, y_column],
366
        use_legend=use_legend,
367
        size=size,
368
        **kwargs,
369
    )
370

371

372
# pylint: disable=too-many-arguments, too-many-positional-arguments
373
def scalar_compare_plot(
2✔
374
    df: _pd.DataFrame,
375
    x_column: str,
376
    y_column: str,
377
    group_by_color: str | None = None,
378
    group_by_marker: str | None = None,
379
    use_legend: bool = True,
380
    size: tuple[float, float] = conf.PlotSizes.A4.value,
381
    scatter_kwargs: dict[str, _tp.Any] | None = None,
382
    line_kwargs: dict[str, _tp.Any] | None = None,
383
    **kwargs: _tp.Any,
384
) -> tuple[_plt.Figure, _plt.Axes]:
385
    """
386
    Create a scalar comparison plot with up to two grouping variables.
387
    This visualization allows simultaneous analysis of:
388

389
    - Numerical relationships between x and y variables
390
    - Categorical grouping through color encoding
391
    - Secondary categorical grouping through marker styles
392

393
    Note
394
    ____
395
    To change the figure properties a separation is included.
396
    scatter_kwargs are used to change the markers.
397
    line_kwargs are used to change the lines.
398

399
    See:
400
    - markers: https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.scatter.html
401
    - lines: https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.plot.html
402

403

404
    Parameters
405
    __________
406
    df : pandas.DataFrame
407
        the dataframe to plot
408

409
    x_column: str
410
        column name for x-axis values
411

412
    y_column: str
413
        column name for y-axis values
414

415
    group_by_color: str, optional
416
        column name for color grouping
417

418
    group_by_marker: str, optional
419
        column name for marker style grouping
420

421
    use_legend: bool, default 'True'
422
        whether to show the legend or not
423

424
    size: tuple of (float, float)
425
        size of the figure (width, height)
426

427
    line_kwargs:
428
        Additional keyword arguments to pass on to
429
        :meth:`matplotlib.axes.Axes.plot`.
430

431
    scatter_kwargs:
432
        Additional keyword arguments to pass on to
433
        :meth:`matplotlib.axes.Axes.scatter`.
434

435
    **kwargs :
436
        Should never be used!
437
        Use 'line_kwargs' or 'scatter_kwargs' instead.
438

439

440
    Returns
441
    _______
442
    tuple of (:class:`matplotlib.figure.Figure`, :class:`matplotlib.axes.Axes`)
443

444
    Examples
445
    ________
446
    .. plot::
447
        :context: close-figs
448

449
        Compare plot
450

451
        >>> api.scalar_compare_plot(
452
        ...     comparison_data,
453
        ...     x_column="VIceSscaled",
454
        ...     y_column="VIceRatioMax",
455
        ...     group_by_color="yearly_demand_GWh",
456
        ...     group_by_marker="ratioDHWtoSH_allSinks",
457
        ... )
458

459

460
    """
UNCOV
461
    if kwargs:
×
UNCOV
462
        raise ValueError(
×
463
            f"\nTo adjust the figure properties, \nplease use the scatter_kwargs "
464
            f"to change the marker properties, \nand please use the line_kwargs "
465
            f"to change the line properties."
466
            f"\nReceived: {kwargs}"
467
        )
468

UNCOV
469
    if not group_by_marker and not group_by_color:
×
UNCOV
470
        raise ValueError(
×
471
            "\nAt least one of 'group_by_marker' or 'group_by_color' has to be set."
472
            f"\nFor a normal scatter plot, please use '{scatter_plot.__name__}'."
473
        )
474

UNCOV
475
    columns_to_validate = [x_column, y_column]
×
UNCOV
476
    if group_by_color:
×
UNCOV
477
        columns_to_validate.append(group_by_color)
×
UNCOV
478
    if group_by_marker:
×
UNCOV
479
        columns_to_validate.append(group_by_marker)
×
UNCOV
480
    _validate_column_exists(df, columns_to_validate)
×
UNCOV
481
    df = df[columns_to_validate]
×
UNCOV
482
    plotter = pltrs.ScalarComparePlot()
×
UNCOV
483
    return plotter.plot(
×
484
        df,
485
        columns=[x_column, y_column],
486
        group_by_color=group_by_color,
487
        group_by_marker=group_by_marker,
488
        use_legend=use_legend,
489
        size=size,
490
        scatter_kwargs=scatter_kwargs,
491
        line_kwargs=line_kwargs,
492
    )
493

494

495
def _validate_column_exists(
2✔
496
    df: _pd.DataFrame, columns: _abc.Sequence[str]
497
) -> None:
498
    """Validate that all requested columns exist in the DataFrame.
499

500
    Since PyTRNSYS is case-insensitive but Python is case-sensitive, this function
501
    provides helpful suggestions when columns differ only by case.
502

503
    Parameters
504
    __________
505
        df: DataFrame to check
506
        columns: Sequence of column names to validate
507

508
    Raises
509
    ______
510
        ColumnNotFoundError: If any columns are missing, with suggestions for case-mismatched names
511
    """
UNCOV
512
    missing_columns = set(columns) - set(df.columns)
×
UNCOV
513
    if not missing_columns:
×
UNCOV
514
        return
×
515

516
    # Create case-insensitive mapping of actual column names
UNCOV
517
    column_name_mapping = {col.casefold(): col for col in df.columns}
×
518

519
    # Categorize missing columns
UNCOV
520
    suggestions = []
×
UNCOV
521
    not_found = []
×
522

UNCOV
523
    for col in missing_columns:
×
UNCOV
524
        if col.casefold() in column_name_mapping:
×
UNCOV
525
            correct_name = column_name_mapping[col.casefold()]
×
UNCOV
526
            suggestions.append(f"'{col}' did you mean: '{correct_name}'")
×
527
        else:
UNCOV
528
            not_found.append(f"'{col}'")
×
529

530
    # Build error message
UNCOV
531
    parts = []
×
UNCOV
532
    if suggestions:
×
UNCOV
533
        parts.append(
×
534
            f"Case-insensitive matches found:\n{', \n'.join(suggestions)}\n"
535
        )
UNCOV
536
    if not_found:
×
UNCOV
537
        parts.append(f"No matches found for:\n{', \n'.join(not_found)}")
×
538

UNCOV
539
    error_msg = "Column validation failed. " + "".join(parts)
×
UNCOV
540
    raise ColumnNotFoundError(error_msg)
×
541

542

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

548
    Warning
549
    _______
550
    Be careful when combining plots. MatPlotLib will not complain when you provide incompatible x-axes.
551
    An example:
552
    combining a time-series with dates with a histogram with temperatures.
553
    In this case, the histogram will disappear without any feedback.
554

555
    Note
556
    ____
557
    The legend of a twin_x plot is a special case:
558
    To have all entries into a single plot, use `fig.legend`
559
    https://matplotlib.org/stable/api/_as_gen/matplotlib.figure.Figure.legend.html
560

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

564

565
    Returns
566
    -------
567
    fig:
568
        Figure object
569

570
    lax:
571
        Axis object for the data on the left y-axis.
572

573
    rax:
574
        Axis object for the data on the right y-axis.
575

576
    Examples
577
    ________
578
    .. plot::
579
        :context: close-figs
580

581
        Twin axis plot with a single legend
582

583
        >>> fig, lax, rax = api.get_figure_with_twin_x_axis()
584
        >>> api.line_plot(simulation.monthly, ["QSnk60P",], ylabel="Power [kWh]", use_legend=False, fig=fig, ax=lax)
585
        >>> api.line_plot(simulation.monthly, ["QSnk60qImbTess", "QSnk60dQlossTess", "QSnk60dQ"], marker="*",
586
        ...     ylabel="Fluxes [kWh]", use_legend=False, fig=fig, ax=rax)
587
        >>> fig.legend(loc="center", bbox_to_anchor=(0.6, 0.7))
588

589
    .. plot::
590
        :context: close-figs
591

592
        Twin axis plot with two legends
593

594
        >>> fig, lax, rax = api.get_figure_with_twin_x_axis()
595
        >>> api.line_plot(simulation.monthly, ["QSnk60P",], ylabel="Power [kWh]", use_legend=False, fig=fig, ax=lax)
596
        >>> api.line_plot(simulation.monthly, ["QSnk60qImbTess", "QSnk60dQlossTess", "QSnk60dQ"], marker="*",
597
        ...     ylabel="Fluxes [kWh]", use_legend=False, fig=fig, ax=rax)
598
        >>> lax.legend(loc="center left")
599
        >>> rax.legend(loc="center right")
600
    """
UNCOV
601
    fig, lax = pltrs.ChartBase.get_fig_and_ax({}, conf.PlotSizes.A4.value)
×
UNCOV
602
    rax = lax.twinx()
×
UNCOV
603
    return fig, lax, rax
×
604

605

606
class ColumnNotFoundError(Exception):
2✔
607
    """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

© 2025 Coveralls, Inc