Skip to content

Commit 2d9c920

Browse files
authored
V3.0.0/plotting (#285)
* Use indexer to reliably plot solutions with and wihtout scenarios/years
1 parent f73fe99 commit 2d9c920

1 file changed

Lines changed: 40 additions & 41 deletions

File tree

flixopt/results.py

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -652,22 +652,17 @@ def plot_heatmap(
652652
"""
653653
dataarray = self.solution[variable_name]
654654

655-
dataarray, suffix_parts = _apply_indexer_to_data(dataarray, indexer)
656-
657-
# Create name
658-
suffix = '--' + '-'.join(suffix_parts) if suffix_parts else ''
659-
name = variable_name if not suffix_parts else f'{variable_name}--{'-'.join(suffix_parts)}' if suffix else variable_name
660-
661655
return plot_heatmap(
662656
dataarray=dataarray,
663-
name=name,
657+
name=variable_name,
664658
folder=self.folder,
665659
heatmap_timeframes=heatmap_timeframes,
666660
heatmap_timesteps_per_frame=heatmap_timesteps_per_frame,
667661
color_map=color_map,
668662
save=save,
669663
show=show,
670664
engine=engine,
665+
indexer=indexer,
671666
)
672667

673668
def plot_network(
@@ -848,20 +843,19 @@ def plot_node_balance(
848843
show: Whether to show the plot or not.
849844
colors: The colors to use for the plot. See `flixopt.plotting.ColorType` for options.
850845
engine: The engine to use for plotting. Can be either 'plotly' or 'matplotlib'.
851-
scenario: The scenario to plot. Defaults to the first scenario. Has no effect without scenarios present
846+
indexer: Optional selection dict, e.g., {'scenario': 'base', 'year': 2024}.
847+
If None, uses first value for each dimension (except time).
852848
mode: The mode to use for the dataset. Can be 'flow_rate' or 'flow_hours'.
853849
- 'flow_rate': Returns the flow_rates of the Node.
854850
- 'flow_hours': Returns the flow_hours of the Node. [flow_hours(t) = flow_rate(t) * dt(t)]. Renames suffixes to |flow_hours.
855851
drop_suffix: Whether to drop the suffix from the variable names.
856852
"""
857853
ds = self.node_balance(with_last_timestep=True, mode=mode, drop_suffix=drop_suffix)
858854

859-
title = f'{self.label} (flow rates)' if mode == 'flow_rate' else f'{self.label} (flow hours)'
855+
ds, suffix_parts = _apply_indexer_to_data(ds, indexer)
856+
suffix = '--' + '-'.join(suffix_parts) if suffix_parts else ''
860857

861-
if 'scenario' in ds.indexes:
862-
chosen_scenario = scenario or self._calculation_results.scenarios[0]
863-
ds = ds.sel(scenario=chosen_scenario).drop_vars('scenario')
864-
title = f'{title} - {chosen_scenario}'
858+
title = f'{self.label} (flow rates){suffix}' if mode == 'flow_rate' else f'{self.label} (flow hours){suffix}'
865859

866860
if engine == 'plotly':
867861
figure_like = plotting.with_plotly(
@@ -911,8 +905,8 @@ def plot_node_balance_pie(
911905
save: Whether to save the figure.
912906
show: Whether to show the figure.
913907
engine: Plotting engine to use. Only 'plotly' is implemented atm.
914-
scenario: If scenarios are present: The scenario to plot. If None, the first scenario is used.
915-
drop_suffix: Whether to drop the suffix from the variable names.
908+
indexer: Optional selection dict, e.g., {'scenario': 'base', 'year': 2024}.
909+
If None, uses first value for each dimension.
916910
"""
917911
inputs = sanitize_dataset(
918912
ds=self.solution[self.inputs] * self._calculation_results.hours_per_timestep,
@@ -928,16 +922,15 @@ def plot_node_balance_pie(
928922
zero_small_values=True,
929923
drop_suffix='|',
930924
)
931-
inputs = inputs.sum('time')
932-
outputs = outputs.sum('time')
933925

934-
title = f'{self.label} (total flow hours)'
926+
inputs, suffix_parts = _apply_indexer_to_data(inputs, indexer)
927+
outputs, suffix_parts = _apply_indexer_to_data(outputs, indexer)
928+
suffix = '--' + '-'.join(suffix_parts) if suffix_parts else ''
935929

936-
if 'scenario' in inputs.indexes:
937-
chosen_scenario = scenario or self._calculation_results.scenarios[0]
938-
inputs = inputs.sel(scenario=chosen_scenario).drop_vars('scenario')
939-
outputs = outputs.sel(scenario=chosen_scenario).drop_vars('scenario')
940-
title = f'{title} - {chosen_scenario}'
930+
title = f'{self.label} (total flow hours){suffix}'
931+
932+
inputs = inputs.sum('time')
933+
outputs = outputs.sum('time')
941934

942935
if engine == 'plotly':
943936
figure_like = plotting.dual_pie_with_plotly(
@@ -1060,7 +1053,8 @@ def plot_charge_state(
10601053
colors: The c
10611054
engine: Plotting engine to use. Only 'plotly' is implemented atm.
10621055
style: The plotting mode for the flow_rate
1063-
scenario: The scenario to plot. Defaults to the first scenario. Has no effect without scenarios present
1056+
indexer: Optional selection dict, e.g., {'scenario': 'base', 'year': 2024}.
1057+
If None, uses first value for each dimension.
10641058
10651059
Raises:
10661060
ValueError: If the Component is not a Storage.
@@ -1071,18 +1065,18 @@ def plot_charge_state(
10711065
ds = self.node_balance(with_last_timestep=True)
10721066
charge_state = self.charge_state
10731067

1074-
scenario_suffix = ''
1075-
if 'scenario' in ds.indexes:
1076-
chosen_scenario = scenario or self._calculation_results.scenarios[0]
1077-
ds = ds.sel(scenario=chosen_scenario).drop_vars('scenario')
1078-
charge_state = charge_state.sel(scenario=chosen_scenario).drop_vars('scenario')
1079-
scenario_suffix = f'--{chosen_scenario}'
1068+
ds, suffix_parts = _apply_indexer_to_data(ds, indexer)
1069+
charge_state, suffix_parts = _apply_indexer_to_data(charge_state, indexer)
1070+
suffix = '--' + '-'.join(suffix_parts) if suffix_parts else ''
1071+
1072+
title=f'Operation Balance of {self.label}{suffix}'
1073+
10801074
if engine == 'plotly':
10811075
fig = plotting.with_plotly(
10821076
ds.to_dataframe(),
10831077
colors=colors,
10841078
style=style,
1085-
title=f'Operation Balance of {self.label}{scenario_suffix}',
1079+
title=title,
10861080
)
10871081

10881082
# TODO: Use colors for charge state?
@@ -1098,7 +1092,7 @@ def plot_charge_state(
10981092
ds.to_dataframe(),
10991093
colors=colors,
11001094
style=style,
1101-
title=f'Operation Balance of {self.label}{scenario_suffix}',
1095+
title=title,
11021096
)
11031097

11041098
charge_state = charge_state.to_dataframe()
@@ -1108,7 +1102,7 @@ def plot_charge_state(
11081102

11091103
return plotting.export_figure(
11101104
fig,
1111-
default_path=self._calculation_results.folder / f'{self.label} (charge state){scenario_suffix}',
1105+
default_path=self._calculation_results.folder / title,
11121106
default_filetype='.html',
11131107
user_path=None if isinstance(save, bool) else pathlib.Path(save),
11141108
show=show,
@@ -1331,6 +1325,7 @@ def plot_heatmap(
13311325
save: Union[bool, pathlib.Path] = False,
13321326
show: bool = True,
13331327
engine: plotting.PlottingEngine = 'plotly',
1328+
indexer: Optional[Dict[str, Any]] = None,
13341329
):
13351330
"""
13361331
Plots a heatmap of the solution of a variable.
@@ -1346,6 +1341,10 @@ def plot_heatmap(
13461341
show: Whether to show the plot or not.
13471342
engine: The engine to use for plotting. Can be either 'plotly' or 'matplotlib'.
13481343
"""
1344+
dataarray, suffix_parts = _apply_indexer_to_data(dataarray, indexer, drop=True)
1345+
suffix = '--' + '-'.join(suffix_parts) if suffix_parts else ''
1346+
name = name if not suffix_parts else name + suffix
1347+
13491348
heatmap_data = plotting.heat_map_data_from_df(
13501349
dataarray.to_dataframe(name), heatmap_timeframes, heatmap_timesteps_per_frame, 'ffill'
13511350
)
@@ -1609,7 +1608,7 @@ def apply_filter(array, coord_name: str, coord_values: Union[Any, List[Any]]):
16091608
return da
16101609

16111610

1612-
def _apply_indexer_to_data(data: xr.DataArray, indexer: Optional[Dict[str, Any]] = None):
1611+
def _apply_indexer_to_data(data: Union[xr.DataArray, xr.Dataset], indexer: Optional[Dict[str, Any]] = None, drop=False):
16131612
"""
16141613
Apply indexer selection or auto-select first values for non-time dimensions.
16151614
@@ -1618,23 +1617,23 @@ def _apply_indexer_to_data(data: xr.DataArray, indexer: Optional[Dict[str, Any]]
16181617
indexer: Optional selection dict
16191618
16201619
Returns:
1621-
Tuple of (selected_data, suffix_parts_list)
1620+
Tuple of (selected_data, selection_string)
16221621
"""
1623-
suffix_parts = []
1622+
selection_string = []
16241623

16251624
if indexer is not None:
16261625
# User provided indexer
1627-
data = data.sel(indexer)
1628-
suffix_parts.extend(f"{v}[{k}]" for k, v in indexer.items())
1626+
data = data.sel(indexer, drop=drop)
1627+
selection_string.extend(f"{v}[{k}]" for k, v in indexer.items())
16291628
else:
16301629
# Auto-select first value for each dimension except 'time'
16311630
selection = {}
16321631
for dim in data.dims:
16331632
if dim != 'time' and dim in data.coords:
16341633
first_value = data.coords[dim].values[0]
16351634
selection[dim] = first_value
1636-
suffix_parts.append(f"{first_value}[{dim}]")
1635+
selection_string.append(f"{first_value}[{dim}]")
16371636
if selection:
1638-
data = data.sel(selection)
1637+
data = data.sel(selection, drop=drop)
16391638

1640-
return data, suffix_parts
1639+
return data, selection_string

0 commit comments

Comments
 (0)