"""Plotting wrappers to provide a simplified interface to the User, while allow development of reusable OOP structures.
Note
____
Many of these plotting routines do not add labels and legends.
This should be done using the figure and axis handles afterwards.
"""
import typing as _tp
from collections import abc as _abc
import matplotlib.pyplot as _plt
import pandas as _pd
from pytrnsys_process import config as conf
from pytrnsys_process.plot import plotters as pltrs
[docs]
def line_plot(
df: _pd.DataFrame,
columns: list[str],
use_legend: bool = True,
size: tuple[float, float] = conf.PlotSizes.A4.value,
**kwargs: _tp.Any,
) -> tuple[_plt.Figure, _plt.Axes]:
"""
Create a line plot using the provided DataFrame columns.
Parameters
__________
df : pandas.DataFrame
the dataframe to plot
columns: list of str
names of columns to plot
use_legend: bool, default 'True'
whether to show the legend or not
size: tuple of (float, float)
size of the figure (width, height)
**kwargs :
Additional keyword arguments are documented in
:meth:`pandas.DataFrame.plot`.
Returns
_______
tuple of (:class:`matplotlib.figure.Figure`, :class:`matplotlib.axes.Axes`)
Examples
________
.. plot::
:context: close-figs
>>> api.line_plot(simulation.hourly, ["QSrc1TIn", "QSrc1TOut"])
"""
_validate_column_exists(df, columns)
plotter = pltrs.LinePlot()
return plotter.plot(
df, columns, use_legend=use_legend, size=size, **kwargs
)
[docs]
def bar_chart(
df: _pd.DataFrame,
columns: list[str],
use_legend: bool = True,
size: tuple[float, float] = conf.PlotSizes.A4.value,
**kwargs: _tp.Any,
) -> tuple[_plt.Figure, _plt.Axes]:
"""
Create a bar chart with multiple columns displayed as grouped bars.
The **kwargs are currently not passed on.
Parameters
__________
df : pandas.DataFrame
the dataframe to plot
columns: list of str
names of columns to plot
use_legend: bool, default 'True'
whether to show the legend or not
size: tuple of (float, float)
size of the figure (width, height)
**kwargs :
Additional keyword arguments to pass on to
:meth:`pandas.DataFrame.plot`.
Returns
_______
tuple of (:class:`matplotlib.figure.Figure`, :class:`matplotlib.axes.Axes`)
Examples
________
.. plot::
:context: close-figs
>>> api.bar_chart(simulation.monthly, ["QSnk60P","QSnk60PauxCondSwitch_kW"])
"""
_validate_column_exists(df, columns)
plotter = pltrs.BarChart()
return plotter.plot(
df, columns, use_legend=use_legend, size=size, **kwargs
)
[docs]
def stacked_bar_chart(
df: _pd.DataFrame,
columns: list[str],
use_legend: bool = True,
size: tuple[float, float] = conf.PlotSizes.A4.value,
**kwargs: _tp.Any,
) -> tuple[_plt.Figure, _plt.Axes]:
"""
Bar chart with stacked bars
Parameters
__________
df : pandas.DataFrame
the dataframe to plot
columns: list of str
names of columns to plot
use_legend: bool, default 'True'
whether to show the legend or not
size: tuple of (float, float)
size of the figure (width, height)
**kwargs :
Additional keyword arguments to pass on to
:meth:`pandas.DataFrame.plot`.
Returns
_______
tuple of (:class:`matplotlib.figure.Figure`, :class:`matplotlib.axes.Axes`)
Examples
________
.. plot::
:context: close-figs
>>> api.stacked_bar_chart(simulation.monthly, ["QSnk60P","QSnk60PauxCondSwitch_kW"])
"""
_validate_column_exists(df, columns)
plotter = pltrs.StackedBarChart()
return plotter.plot(
df, columns, use_legend=use_legend, size=size, **kwargs
)
[docs]
def histogram(
df: _pd.DataFrame,
columns: list[str],
use_legend: bool = True,
size: tuple[float, float] = conf.PlotSizes.A4.value,
bins: int = 50,
**kwargs: _tp.Any,
) -> tuple[_plt.Figure, _plt.Axes]:
"""
Create a histogram from the given DataFrame columns.
Parameters
__________
df : pandas.DataFrame
the dataframe to plot
columns: list of str
names of columns to plot
use_legend: bool, default 'True'
whether to show the legend or not
size: tuple of (float, float)
size of the figure (width, height)
bins: int
number of histogram bins to be used
**kwargs :
Additional keyword arguments to pass on to
:meth:`pandas.DataFrame.plot`.
Returns
_______
tuple of (:class:`matplotlib.figure.Figure`, :class:`matplotlib.axes.Axes`)
Examples
________
.. plot::
:context: close-figs
>>> api.histogram(simulation.hourly, ["QSrc1TIn"], ylabel="")
"""
_validate_column_exists(df, columns)
plotter = pltrs.Histogram(bins)
return plotter.plot(
df, columns, use_legend=use_legend, size=size, **kwargs
)
[docs]
def energy_balance(
df: _pd.DataFrame,
q_in_columns: list[str],
q_out_columns: list[str],
q_imb_column: _tp.Optional[str] = None,
use_legend: bool = True,
size: tuple[float, float] = conf.PlotSizes.A4.value,
**kwargs: _tp.Any,
) -> tuple[_plt.Figure, _plt.Axes]:
"""
Create a stacked bar chart showing energy balance with inputs, outputs and imbalance.
This function creates an energy balance visualization where:
- Input energies are shown as positive values
- Output energies are shown as negative values
- Energy imbalance is either provided or calculated as (sum of inputs + sum of outputs)
Parameters
__________
df : pandas.DataFrame
the dataframe to plot
q_in_columns: list of str
column names representing energy inputs
q_out_columns: list of str
column names representing energy outputs
q_imb_column: list of str, optional
column name containing pre-calculated energy imbalance
use_legend: bool, default 'True'
whether to show the legend or not
size: tuple of (float, float)
size of the figure (width, height)
**kwargs :
Additional keyword arguments to pass on to
:meth:`pandas.DataFrame.plot`.
Returns
_______
tuple of (:class:`matplotlib.figure.Figure`, :class:`matplotlib.axes.Axes`)
Examples
________
.. plot::
:context: close-figs
>>> api.energy_balance(
>>> simulation.monthly,
>>> q_in_columns=["QSnk60PauxCondSwitch_kW"],
>>> q_out_columns=["QSnk60P", "QSnk60dQlossTess", "QSnk60dQ"],
>>> q_imb_column="QSnk60qImbTess",
>>> xlabel=""
>>> )
"""
line_columns = None
if "ylabel" in kwargs:
kwargs["energy_balance_ylabel"] = kwargs["ylabel"]
fig, lax, _ = energy_balance_with_lines(
df,
q_in_columns,
q_out_columns,
line_columns,
q_imb_column,
use_legend,
size,
**kwargs,
)
return fig, lax
# pylint: disable=too-many-arguments
[docs]
def energy_balance_with_lines(
df: _pd.DataFrame,
q_in_columns: list[str],
q_out_columns: list[str],
line_columns: _tp.Optional[list[str]] = None,
q_imb_column: _tp.Optional[str] = None,
use_legend: bool = True,
size: tuple[float, float] = conf.PlotSizes.A4.value,
**kwargs: _tp.Any,
) -> tuple[_plt.Figure, _plt.Axes, _plt.Axes]:
"""
Create a stacked bar chart showing energy balance with inputs, outputs and imbalance.
On top of which one or more lines will be plotted.
This function creates an energy balance visualization where:
- Input energies are shown as positive values
- Output energies are shown as negative values
- Energy imbalance is either provided or calculated as (sum of inputs + sum of outputs)
Parameters
__________
df : pandas.DataFrame
the dataframe to plot
q_in_columns: list of str
column names representing energy inputs
q_out_columns: list of str
column names representing energy outputs
q_imb_column: list of str, optional
column name containing pre-calculated energy imbalance
line_columns: list of str
column names that should be plotted as line on top of the energy balance.
use_legend: bool, default 'True'
whether to show the legend or not
size: tuple of (float, float)
size of the figure (width, height)
energy_balance_ylabel: str
y-axis label for the energy balance.
line_ylabel: str
y-axis label for the lines.
**kwargs :
Additional keyword arguments to pass on to
:meth:`pandas.DataFrame.plot`.
Returns
_______
tuple of (:class:`matplotlib.figure.Figure`, :class:`matplotlib.axes.Axes`)
Examples
________
.. plot::
:context: close-figs
>>> api.energy_balance_with_lines(
>>> simulation.monthly,
>>> q_in_columns=["QSnk60PauxCondSwitch_kW"],
>>> q_out_columns=["QSnk60P", "QSnk60dQlossTess", "QSnk60dQ"],
>>> q_imb_column="QSnk60qImbTess",
>>> xlabel=""
>>> )
"""
all_columns_for_validation = (
q_in_columns
+ q_out_columns
+ (line_columns if line_columns is not None else [])
+ ([q_imb_column] if q_imb_column is not None else [])
)
_validate_column_exists(df, all_columns_for_validation)
df_modified = df.copy()
for col in q_out_columns:
df_modified[col] = -df_modified[col]
if q_imb_column is None:
q_imb_column = "Qimb"
df_modified[q_imb_column] = df_modified[
q_in_columns + q_out_columns
].sum(axis=1)
# imbalance is visually added where it is missing.
df_modified[q_imb_column] *= -1
columns_to_plot = {
"q_in_columns": q_in_columns,
"q_out_columns": q_out_columns,
"q_imb_column": q_imb_column,
"line_columns": line_columns,
}
plotter = pltrs.EnergyBalanceChart()
return plotter.plot( # type: ignore[return-value]
df_modified,
columns_to_plot,
use_legend=use_legend,
size=size,
**kwargs,
)
[docs]
def scatter_plot(
df: _pd.DataFrame,
x_column: str,
y_column: str,
use_legend: bool = True,
size: tuple[float, float] = conf.PlotSizes.A4.value,
**kwargs: _tp.Any,
) -> tuple[_plt.Figure, _plt.Axes]:
"""
Create a scatter plot to show numerical relationships between x and y variables.
Note
____
Use color and not cmap!
See: https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.plot.scatter.html
Parameters
__________
df : pandas.DataFrame
the dataframe to plot
x_column: str
coloumn name for x-axis values
y_column: str
coloumn name for y-axis values
use_legend: bool, default 'True'
whether to show the legend or not
size: tuple of (float, float)
size of the figure (width, height)
**kwargs :
Additional keyword arguments to pass on to
:meth:`pandas.DataFrame.plot.scatter`.
Returns
_______
tuple of (:class:`matplotlib.figure.Figure`, :class:`matplotlib.axes.Axes`)
Examples
________
.. plot::
:context: close-figs
Simple scatter plot
>>> api.scatter_plot(
... simulation.monthly, x_column="QSnk60dQlossTess", y_column="QSnk60dQ"
... )
"""
if "cmap" in kwargs:
raise ValueError(
"\nscatter_plot does not take a 'cmap'."
"\nPlease use color instead."
)
columns_to_validate = [x_column, y_column]
_validate_column_exists(df, [x_column, y_column])
df = df[columns_to_validate]
plotter = pltrs.ScatterPlot()
return plotter.plot(
df,
columns=[x_column, y_column],
use_legend=use_legend,
size=size,
**kwargs,
)
# pylint: disable=too-many-arguments, too-many-positional-arguments
[docs]
def scalar_compare_plot(
df: _pd.DataFrame,
x_column: str,
y_column: str,
group_by_color: str | None = None,
group_by_marker: str | None = None,
use_legend: bool = True,
size: tuple[float, float] = conf.PlotSizes.A4.value,
scatter_kwargs: dict[str, _tp.Any] | None = None,
line_kwargs: dict[str, _tp.Any] | None = None,
**kwargs: _tp.Any,
) -> tuple[_plt.Figure, _plt.Axes]:
"""
Create a scalar comparison plot with up to two grouping variables.
This visualization allows simultaneous analysis of:
- Numerical relationships between x and y variables
- Categorical grouping through color encoding
- Secondary categorical grouping through marker styles
Note
____
To change the figure properties a separation is included.
scatter_kwargs are used to change the markers.
line_kwargs are used to change the lines.
See:
- markers: https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.scatter.html
- lines: https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.plot.html
Parameters
__________
df : pandas.DataFrame
the dataframe to plot
x_column: str
column name for x-axis values
y_column: str
column name for y-axis values
group_by_color: str, optional
column name for color grouping
group_by_marker: str, optional
column name for marker style grouping
use_legend: bool, default 'True'
whether to show the legend or not
size: tuple of (float, float)
size of the figure (width, height)
line_kwargs:
Additional keyword arguments to pass on to
:meth:`matplotlib.axes.Axes.plot`.
scatter_kwargs:
Additional keyword arguments to pass on to
:meth:`matplotlib.axes.Axes.scatter`.
**kwargs :
Should never be used!
Use 'line_kwargs' or 'scatter_kwargs' instead.
Returns
_______
tuple of (:class:`matplotlib.figure.Figure`, :class:`matplotlib.axes.Axes`)
Examples
________
.. plot::
:context: close-figs
Compare plot
>>> api.scalar_compare_plot(
... comparison_data,
... x_column="VIceSscaled",
... y_column="VIceRatioMax",
... group_by_color="yearly_demand_GWh",
... group_by_marker="ratioDHWtoSH_allSinks",
... )
"""
if kwargs:
raise ValueError(
f"\nTo adjust the figure properties, \nplease use the scatter_kwargs "
f"to change the marker properties, \nand please use the line_kwargs "
f"to change the line properties."
f"\nReceived: {kwargs}"
)
if not group_by_marker and not group_by_color:
raise ValueError(
"\nAt least one of 'group_by_marker' or 'group_by_color' has to be set."
f"\nFor a normal scatter plot, please use '{scatter_plot.__name__}'."
)
columns_to_validate = [x_column, y_column]
if group_by_color:
columns_to_validate.append(group_by_color)
if group_by_marker:
columns_to_validate.append(group_by_marker)
_validate_column_exists(df, columns_to_validate)
df = df[columns_to_validate]
plotter = pltrs.ScalarComparePlot()
return plotter.plot(
df,
columns=[x_column, y_column],
group_by_color=group_by_color,
group_by_marker=group_by_marker,
use_legend=use_legend,
size=size,
scatter_kwargs=scatter_kwargs,
line_kwargs=line_kwargs,
)
def _validate_column_exists(
df: _pd.DataFrame, columns: _abc.Sequence[str]
) -> None:
"""Validate that all requested columns exist in the DataFrame.
Since PyTRNSYS is case-insensitive but Python is case-sensitive, this function
provides helpful suggestions when columns differ only by case.
Parameters
__________
df: DataFrame to check
columns: Sequence of column names to validate
Raises
______
ColumnNotFoundError: If any columns are missing, with suggestions for case-mismatched names
"""
missing_columns = set(columns) - set(df.columns)
if not missing_columns:
return
# Create case-insensitive mapping of actual column names
column_name_mapping = {col.casefold(): col for col in df.columns}
# Categorize missing columns
suggestions = []
not_found = []
for col in missing_columns:
if col.casefold() in column_name_mapping:
correct_name = column_name_mapping[col.casefold()]
suggestions.append(f"'{col}' did you mean: '{correct_name}'")
else:
not_found.append(f"'{col}'")
# Build error message
parts = []
if suggestions:
parts.append(
f"Case-insensitive matches found:\n{', \n'.join(suggestions)}\n"
)
if not_found:
parts.append(f"No matches found for:\n{', \n'.join(not_found)}")
error_msg = "Column validation failed. " + "".join(parts)
raise ColumnNotFoundError(error_msg)
[docs]
class ColumnNotFoundError(Exception):
"""This exception is raised when given column names are not available in the dataframe"""