flixopt 3.1.1__py3-none-any.whl → 3.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of flixopt might be problematic. Click here for more details.
- flixopt/aggregation.py +13 -4
- flixopt/calculation.py +2 -3
- flixopt/color_processing.py +261 -0
- flixopt/config.py +59 -4
- flixopt/flow_system.py +15 -5
- flixopt/interface.py +2 -1
- flixopt/io.py +239 -22
- flixopt/plotting.py +583 -789
- flixopt/results.py +445 -56
- flixopt/structure.py +1 -3
- {flixopt-3.1.1.dist-info → flixopt-3.2.1.dist-info}/METADATA +3 -3
- flixopt-3.2.1.dist-info/RECORD +26 -0
- flixopt/utils.py +0 -86
- flixopt-3.1.1.dist-info/RECORD +0 -26
- {flixopt-3.1.1.dist-info → flixopt-3.2.1.dist-info}/WHEEL +0 -0
- {flixopt-3.1.1.dist-info → flixopt-3.2.1.dist-info}/licenses/LICENSE +0 -0
- {flixopt-3.1.1.dist-info → flixopt-3.2.1.dist-info}/top_level.txt +0 -0
flixopt/results.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import copy
|
|
3
4
|
import datetime
|
|
4
|
-
import json
|
|
5
5
|
import logging
|
|
6
6
|
import pathlib
|
|
7
7
|
import warnings
|
|
@@ -11,10 +11,11 @@ import linopy
|
|
|
11
11
|
import numpy as np
|
|
12
12
|
import pandas as pd
|
|
13
13
|
import xarray as xr
|
|
14
|
-
import yaml
|
|
15
14
|
|
|
16
15
|
from . import io as fx_io
|
|
17
16
|
from . import plotting
|
|
17
|
+
from .color_processing import process_colors
|
|
18
|
+
from .config import CONFIG
|
|
18
19
|
from .flow_system import FlowSystem
|
|
19
20
|
|
|
20
21
|
if TYPE_CHECKING:
|
|
@@ -29,6 +30,23 @@ if TYPE_CHECKING:
|
|
|
29
30
|
logger = logging.getLogger('flixopt')
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
def load_mapping_from_file(path: pathlib.Path) -> dict[str, str | list[str]]:
|
|
34
|
+
"""Load color mapping from JSON or YAML file.
|
|
35
|
+
|
|
36
|
+
Tries loader based on file suffix first, with fallback to the other format.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
path: Path to config file (.json or .yaml/.yml)
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Dictionary mapping components to colors or colorscales to component lists
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ValueError: If file cannot be loaded as JSON or YAML
|
|
46
|
+
"""
|
|
47
|
+
return fx_io.load_config_file(path)
|
|
48
|
+
|
|
49
|
+
|
|
32
50
|
class _FlowSystemRestorationError(Exception):
|
|
33
51
|
"""Exception raised when a FlowSystem cannot be restored from dataset."""
|
|
34
52
|
|
|
@@ -107,6 +125,20 @@ class CalculationResults:
|
|
|
107
125
|
).mean()
|
|
108
126
|
```
|
|
109
127
|
|
|
128
|
+
Configure automatic color management for plots:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
# Dict-based configuration:
|
|
132
|
+
results.setup_colors({'Solar*': 'Oranges', 'Wind*': 'Blues', 'Battery': 'green'})
|
|
133
|
+
|
|
134
|
+
# All plots automatically use configured colors (colors=None is the default)
|
|
135
|
+
results['ElectricityBus'].plot_node_balance()
|
|
136
|
+
results['Battery'].plot_charge_state()
|
|
137
|
+
|
|
138
|
+
# Override when needed
|
|
139
|
+
results['ElectricityBus'].plot_node_balance(colors='turbo') # Ignores setup
|
|
140
|
+
```
|
|
141
|
+
|
|
110
142
|
Design Patterns:
|
|
111
143
|
**Factory Methods**: Use `from_file()` and `from_calculation()` for creation or access directly from `Calculation.results`
|
|
112
144
|
**Dictionary Access**: Use `results[element_label]` for element-specific results
|
|
@@ -137,8 +169,7 @@ class CalculationResults:
|
|
|
137
169
|
except Exception as e:
|
|
138
170
|
logger.critical(f'Could not load the linopy model "{name}" from file ("{paths.linopy_model}"): {e}')
|
|
139
171
|
|
|
140
|
-
|
|
141
|
-
summary = yaml.load(f, Loader=yaml.FullLoader)
|
|
172
|
+
summary = fx_io.load_yaml(paths.summary)
|
|
142
173
|
|
|
143
174
|
return cls(
|
|
144
175
|
solution=fx_io.load_dataset_from_netcdf(paths.solution),
|
|
@@ -240,6 +271,8 @@ class CalculationResults:
|
|
|
240
271
|
self._sizes = None
|
|
241
272
|
self._effects_per_component = None
|
|
242
273
|
|
|
274
|
+
self.colors: dict[str, str] = {}
|
|
275
|
+
|
|
243
276
|
def __getitem__(self, key: str) -> ComponentResults | BusResults | EffectResults:
|
|
244
277
|
if key in self.components:
|
|
245
278
|
return self.components[key]
|
|
@@ -306,6 +339,131 @@ class CalculationResults:
|
|
|
306
339
|
logger.level = old_level
|
|
307
340
|
return self._flow_system
|
|
308
341
|
|
|
342
|
+
def setup_colors(
|
|
343
|
+
self,
|
|
344
|
+
config: dict[str, str | list[str]] | str | pathlib.Path | None = None,
|
|
345
|
+
default_colorscale: str | None = None,
|
|
346
|
+
) -> dict[str, str]:
|
|
347
|
+
"""
|
|
348
|
+
Setup colors for all variables across all elements. Overwrites existing ones.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
config: Configuration for color assignment. Can be:
|
|
352
|
+
- dict: Maps components to colors/colorscales:
|
|
353
|
+
* 'component1': 'red' # Single component to single color
|
|
354
|
+
* 'component1': '#FF0000' # Single component to hex color
|
|
355
|
+
- OR maps colorscales to multiple components:
|
|
356
|
+
* 'colorscale_name': ['component1', 'component2'] # Colorscale across components
|
|
357
|
+
- str: Path to a JSON/YAML config file or a colorscale name to apply to all
|
|
358
|
+
- Path: Path to a JSON/YAML config file
|
|
359
|
+
- None: Use default_colorscale for all components
|
|
360
|
+
default_colorscale: Default colorscale for unconfigured components (default: 'turbo')
|
|
361
|
+
|
|
362
|
+
Examples:
|
|
363
|
+
setup_colors({
|
|
364
|
+
# Direct component-to-color mappings
|
|
365
|
+
'Boiler1': '#FF0000',
|
|
366
|
+
'CHP': 'darkred',
|
|
367
|
+
# Colorscale for multiple components
|
|
368
|
+
'Oranges': ['Solar1', 'Solar2'],
|
|
369
|
+
'Blues': ['Wind1', 'Wind2'],
|
|
370
|
+
'Greens': ['Battery1', 'Battery2', 'Battery3'],
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Complete variable-to-color mapping dictionary
|
|
375
|
+
"""
|
|
376
|
+
|
|
377
|
+
def get_all_variable_names(comp: str) -> list[str]:
|
|
378
|
+
"""Collect all variables from the component, including flows and flow_hours."""
|
|
379
|
+
comp_object = self.components[comp]
|
|
380
|
+
var_names = [comp] + list(comp_object._variable_names)
|
|
381
|
+
for flow in comp_object.flows:
|
|
382
|
+
var_names.extend([flow, f'{flow}|flow_hours'])
|
|
383
|
+
return var_names
|
|
384
|
+
|
|
385
|
+
# Set default colorscale if not provided
|
|
386
|
+
if default_colorscale is None:
|
|
387
|
+
default_colorscale = CONFIG.Plotting.default_qualitative_colorscale
|
|
388
|
+
|
|
389
|
+
# Handle different config input types
|
|
390
|
+
if config is None:
|
|
391
|
+
# Apply default colorscale to all components
|
|
392
|
+
config_dict = {}
|
|
393
|
+
elif isinstance(config, (str, pathlib.Path)):
|
|
394
|
+
# Try to load from file first
|
|
395
|
+
config_path = pathlib.Path(config)
|
|
396
|
+
if config_path.exists():
|
|
397
|
+
# Load config from file using helper
|
|
398
|
+
config_dict = load_mapping_from_file(config_path)
|
|
399
|
+
else:
|
|
400
|
+
# Treat as colorscale name to apply to all components
|
|
401
|
+
all_components = list(self.components.keys())
|
|
402
|
+
config_dict = {config: all_components}
|
|
403
|
+
elif isinstance(config, dict):
|
|
404
|
+
config_dict = config
|
|
405
|
+
else:
|
|
406
|
+
raise TypeError(f'config must be dict, str, Path, or None, got {type(config)}')
|
|
407
|
+
|
|
408
|
+
# Step 1: Build component-to-color mapping
|
|
409
|
+
component_colors: dict[str, str] = {}
|
|
410
|
+
|
|
411
|
+
# Track which components are configured
|
|
412
|
+
configured_components = set()
|
|
413
|
+
|
|
414
|
+
# Process each configuration entry
|
|
415
|
+
for key, value in config_dict.items():
|
|
416
|
+
# Check if value is a list (colorscale -> [components])
|
|
417
|
+
# or a string (component -> color OR colorscale -> [components])
|
|
418
|
+
|
|
419
|
+
if isinstance(value, list):
|
|
420
|
+
# key is colorscale, value is list of components
|
|
421
|
+
# Format: 'Blues': ['Wind1', 'Wind2']
|
|
422
|
+
components = value
|
|
423
|
+
colorscale_name = key
|
|
424
|
+
|
|
425
|
+
# Validate components exist
|
|
426
|
+
for component in components:
|
|
427
|
+
if component not in self.components:
|
|
428
|
+
raise ValueError(f"Component '{component}' not found")
|
|
429
|
+
|
|
430
|
+
configured_components.update(components)
|
|
431
|
+
|
|
432
|
+
# Use process_colors to get one color per component from the colorscale
|
|
433
|
+
colors_for_components = process_colors(colorscale_name, components)
|
|
434
|
+
component_colors.update(colors_for_components)
|
|
435
|
+
|
|
436
|
+
elif isinstance(value, str):
|
|
437
|
+
# Check if key is an existing component
|
|
438
|
+
if key in self.components:
|
|
439
|
+
# Format: 'CHP': 'red' (component -> color)
|
|
440
|
+
component, color = key, value
|
|
441
|
+
|
|
442
|
+
configured_components.add(component)
|
|
443
|
+
component_colors[component] = color
|
|
444
|
+
else:
|
|
445
|
+
raise ValueError(f"Component '{key}' not found")
|
|
446
|
+
else:
|
|
447
|
+
raise TypeError(f'Config value must be str or list, got {type(value)}')
|
|
448
|
+
|
|
449
|
+
# Step 2: Assign colors to remaining unconfigured components
|
|
450
|
+
remaining_components = list(set(self.components.keys()) - configured_components)
|
|
451
|
+
if remaining_components:
|
|
452
|
+
# Use default colorscale to assign one color per remaining component
|
|
453
|
+
default_colors = process_colors(default_colorscale, remaining_components)
|
|
454
|
+
component_colors.update(default_colors)
|
|
455
|
+
|
|
456
|
+
# Step 3: Build variable-to-color mapping
|
|
457
|
+
# Clear existing colors to avoid stale keys
|
|
458
|
+
self.colors = {}
|
|
459
|
+
# Each component's variables all get the same color as the component
|
|
460
|
+
for component, color in component_colors.items():
|
|
461
|
+
variable_names = get_all_variable_names(component)
|
|
462
|
+
for var_name in variable_names:
|
|
463
|
+
self.colors[var_name] = color
|
|
464
|
+
|
|
465
|
+
return self.colors
|
|
466
|
+
|
|
309
467
|
def filter_solution(
|
|
310
468
|
self,
|
|
311
469
|
variable_dims: Literal['scalar', 'time', 'scenario', 'timeonly', 'scenarioonly'] | None = None,
|
|
@@ -705,13 +863,13 @@ class CalculationResults:
|
|
|
705
863
|
self,
|
|
706
864
|
variable_name: str | list[str],
|
|
707
865
|
save: bool | pathlib.Path = False,
|
|
708
|
-
show: bool =
|
|
709
|
-
colors: plotting.ColorType =
|
|
866
|
+
show: bool | None = None,
|
|
867
|
+
colors: plotting.ColorType | None = None,
|
|
710
868
|
engine: plotting.PlottingEngine = 'plotly',
|
|
711
869
|
select: dict[FlowSystemDimensions, Any] | None = None,
|
|
712
870
|
facet_by: str | list[str] | None = 'scenario',
|
|
713
871
|
animate_by: str | None = 'period',
|
|
714
|
-
facet_cols: int =
|
|
872
|
+
facet_cols: int | None = None,
|
|
715
873
|
reshape_time: tuple[Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'], Literal['W', 'D', 'h', '15min', 'min']]
|
|
716
874
|
| Literal['auto']
|
|
717
875
|
| None = 'auto',
|
|
@@ -721,6 +879,7 @@ class CalculationResults:
|
|
|
721
879
|
heatmap_timeframes: Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'] | None = None,
|
|
722
880
|
heatmap_timesteps_per_frame: Literal['W', 'D', 'h', '15min', 'min'] | None = None,
|
|
723
881
|
color_map: str | None = None,
|
|
882
|
+
**plot_kwargs: Any,
|
|
724
883
|
) -> plotly.graph_objs.Figure | tuple[plt.Figure, plt.Axes]:
|
|
725
884
|
"""
|
|
726
885
|
Plots a heatmap visualization of a variable using imshow or time-based reshaping.
|
|
@@ -754,6 +913,20 @@ class CalculationResults:
|
|
|
754
913
|
Supported timeframes: 'YS', 'MS', 'W', 'D', 'h', '15min', 'min'
|
|
755
914
|
fill: Method to fill missing values after reshape: 'ffill' (forward fill) or 'bfill' (backward fill).
|
|
756
915
|
Default is 'ffill'.
|
|
916
|
+
**plot_kwargs: Additional plotting customization options.
|
|
917
|
+
Common options:
|
|
918
|
+
|
|
919
|
+
- **dpi** (int): Export resolution for saved plots. Default: 300.
|
|
920
|
+
|
|
921
|
+
For heatmaps specifically:
|
|
922
|
+
|
|
923
|
+
- **vmin** (float): Minimum value for color scale (both engines).
|
|
924
|
+
- **vmax** (float): Maximum value for color scale (both engines).
|
|
925
|
+
|
|
926
|
+
For Matplotlib heatmaps:
|
|
927
|
+
|
|
928
|
+
- **imshow_kwargs** (dict): Additional kwargs for matplotlib's imshow (e.g., interpolation, aspect).
|
|
929
|
+
- **cbar_kwargs** (dict): Additional kwargs for colorbar customization.
|
|
757
930
|
|
|
758
931
|
Examples:
|
|
759
932
|
Direct imshow mode (default):
|
|
@@ -794,6 +967,18 @@ class CalculationResults:
|
|
|
794
967
|
... animate_by='period',
|
|
795
968
|
... reshape_time=('D', 'h'),
|
|
796
969
|
... )
|
|
970
|
+
|
|
971
|
+
High-resolution export with custom color range:
|
|
972
|
+
|
|
973
|
+
>>> results.plot_heatmap('Battery|charge_state', save=True, dpi=600, vmin=0, vmax=100)
|
|
974
|
+
|
|
975
|
+
Matplotlib heatmap with custom imshow settings:
|
|
976
|
+
|
|
977
|
+
>>> results.plot_heatmap(
|
|
978
|
+
... 'Boiler(Q_th)|flow_rate',
|
|
979
|
+
... engine='matplotlib',
|
|
980
|
+
... imshow_kwargs={'interpolation': 'bilinear', 'aspect': 'auto'},
|
|
981
|
+
... )
|
|
797
982
|
"""
|
|
798
983
|
# Delegate to module-level plot_heatmap function
|
|
799
984
|
return plot_heatmap(
|
|
@@ -814,6 +999,7 @@ class CalculationResults:
|
|
|
814
999
|
heatmap_timeframes=heatmap_timeframes,
|
|
815
1000
|
heatmap_timesteps_per_frame=heatmap_timesteps_per_frame,
|
|
816
1001
|
color_map=color_map,
|
|
1002
|
+
**plot_kwargs,
|
|
817
1003
|
)
|
|
818
1004
|
|
|
819
1005
|
def plot_network(
|
|
@@ -870,14 +1056,13 @@ class CalculationResults:
|
|
|
870
1056
|
fx_io.save_dataset_to_netcdf(self.solution, paths.solution, compression=compression)
|
|
871
1057
|
fx_io.save_dataset_to_netcdf(self.flow_system_data, paths.flow_system, compression=compression)
|
|
872
1058
|
|
|
873
|
-
|
|
874
|
-
yaml.dump(self.summary, f, allow_unicode=True, sort_keys=False, indent=4, width=1000)
|
|
1059
|
+
fx_io.save_yaml(self.summary, paths.summary)
|
|
875
1060
|
|
|
876
1061
|
if save_linopy_model:
|
|
877
1062
|
if self.model is None:
|
|
878
1063
|
logger.critical('No model in the CalculationResults. Saving the model is not possible.')
|
|
879
1064
|
else:
|
|
880
|
-
self.model.to_netcdf(paths.linopy_model, engine='
|
|
1065
|
+
self.model.to_netcdf(paths.linopy_model, engine='netcdf4')
|
|
881
1066
|
|
|
882
1067
|
if document_model:
|
|
883
1068
|
if self.model is None:
|
|
@@ -982,8 +1167,8 @@ class _NodeResults(_ElementResults):
|
|
|
982
1167
|
def plot_node_balance(
|
|
983
1168
|
self,
|
|
984
1169
|
save: bool | pathlib.Path = False,
|
|
985
|
-
show: bool =
|
|
986
|
-
colors: plotting.ColorType =
|
|
1170
|
+
show: bool | None = None,
|
|
1171
|
+
colors: plotting.ColorType | None = None,
|
|
987
1172
|
engine: plotting.PlottingEngine = 'plotly',
|
|
988
1173
|
select: dict[FlowSystemDimensions, Any] | None = None,
|
|
989
1174
|
unit_type: Literal['flow_rate', 'flow_hours'] = 'flow_rate',
|
|
@@ -991,9 +1176,10 @@ class _NodeResults(_ElementResults):
|
|
|
991
1176
|
drop_suffix: bool = True,
|
|
992
1177
|
facet_by: str | list[str] | None = 'scenario',
|
|
993
1178
|
animate_by: str | None = 'period',
|
|
994
|
-
facet_cols: int =
|
|
1179
|
+
facet_cols: int | None = None,
|
|
995
1180
|
# Deprecated parameter (kept for backwards compatibility)
|
|
996
1181
|
indexer: dict[FlowSystemDimensions, Any] | None = None,
|
|
1182
|
+
**plot_kwargs: Any,
|
|
997
1183
|
) -> plotly.graph_objs.Figure | tuple[plt.Figure, plt.Axes]:
|
|
998
1184
|
"""
|
|
999
1185
|
Plots the node balance of the Component or Bus with optional faceting and animation.
|
|
@@ -1021,6 +1207,27 @@ class _NodeResults(_ElementResults):
|
|
|
1021
1207
|
animate_by: Dimension to animate over (Plotly only). Creates animation frames that cycle through
|
|
1022
1208
|
dimension values. Only one dimension can be animated. Ignored if not found.
|
|
1023
1209
|
facet_cols: Number of columns in the facet grid layout (default: 3).
|
|
1210
|
+
**plot_kwargs: Additional plotting customization options passed to underlying plotting functions.
|
|
1211
|
+
|
|
1212
|
+
Common options:
|
|
1213
|
+
|
|
1214
|
+
- **dpi** (int): Export resolution in dots per inch. Default: 300.
|
|
1215
|
+
|
|
1216
|
+
**For Plotly engine** (`engine='plotly'`):
|
|
1217
|
+
|
|
1218
|
+
- Any Plotly Express parameter for px.bar()/px.line()/px.area()
|
|
1219
|
+
Example: `range_y=[0, 100]`, `line_shape='linear'`
|
|
1220
|
+
|
|
1221
|
+
**For Matplotlib engine** (`engine='matplotlib'`):
|
|
1222
|
+
|
|
1223
|
+
- **plot_kwargs** (dict): Customize plot via `ax.bar()` or `ax.step()`.
|
|
1224
|
+
Example: `plot_kwargs={'linewidth': 3, 'alpha': 0.7, 'edgecolor': 'black'}`
|
|
1225
|
+
|
|
1226
|
+
See :func:`flixopt.plotting.with_plotly` and :func:`flixopt.plotting.with_matplotlib`
|
|
1227
|
+
for complete parameter reference.
|
|
1228
|
+
|
|
1229
|
+
Note: For Plotly, you can further customize the returned figure using `fig.update_traces()`
|
|
1230
|
+
and `fig.update_layout()` after calling this method.
|
|
1024
1231
|
|
|
1025
1232
|
Examples:
|
|
1026
1233
|
Basic plot (current behavior):
|
|
@@ -1052,6 +1259,25 @@ class _NodeResults(_ElementResults):
|
|
|
1052
1259
|
Time range selection (summer months only):
|
|
1053
1260
|
|
|
1054
1261
|
>>> results['Boiler'].plot_node_balance(select={'time': slice('2024-06', '2024-08')}, facet_by='scenario')
|
|
1262
|
+
|
|
1263
|
+
High-resolution export for publication:
|
|
1264
|
+
|
|
1265
|
+
>>> results['Boiler'].plot_node_balance(engine='matplotlib', save='figure.png', dpi=600)
|
|
1266
|
+
|
|
1267
|
+
Plotly Express customization (e.g., set y-axis range):
|
|
1268
|
+
|
|
1269
|
+
>>> results['Boiler'].plot_node_balance(range_y=[0, 100])
|
|
1270
|
+
|
|
1271
|
+
Custom matplotlib appearance:
|
|
1272
|
+
|
|
1273
|
+
>>> results['Boiler'].plot_node_balance(engine='matplotlib', plot_kwargs={'linewidth': 3, 'alpha': 0.7})
|
|
1274
|
+
|
|
1275
|
+
Further customize Plotly figure after creation:
|
|
1276
|
+
|
|
1277
|
+
>>> fig = results['Boiler'].plot_node_balance(mode='line', show=False)
|
|
1278
|
+
>>> fig.update_traces(line={'width': 5, 'dash': 'dot'})
|
|
1279
|
+
>>> fig.update_layout(template='plotly_dark', width=1200, height=600)
|
|
1280
|
+
>>> fig.show()
|
|
1055
1281
|
"""
|
|
1056
1282
|
# Handle deprecated indexer parameter
|
|
1057
1283
|
if indexer is not None:
|
|
@@ -1073,8 +1299,11 @@ class _NodeResults(_ElementResults):
|
|
|
1073
1299
|
if engine not in {'plotly', 'matplotlib'}:
|
|
1074
1300
|
raise ValueError(f'Engine "{engine}" not supported. Use one of ["plotly", "matplotlib"]')
|
|
1075
1301
|
|
|
1302
|
+
# Extract dpi for export_figure
|
|
1303
|
+
dpi = plot_kwargs.pop('dpi', None) # None uses CONFIG.Plotting.default_dpi
|
|
1304
|
+
|
|
1076
1305
|
# Don't pass select/indexer to node_balance - we'll apply it afterwards
|
|
1077
|
-
ds = self.node_balance(with_last_timestep=
|
|
1306
|
+
ds = self.node_balance(with_last_timestep=False, unit_type=unit_type, drop_suffix=drop_suffix)
|
|
1078
1307
|
|
|
1079
1308
|
ds, suffix_parts = _apply_selection_to_data(ds, select=select, drop=True)
|
|
1080
1309
|
|
|
@@ -1097,18 +1326,21 @@ class _NodeResults(_ElementResults):
|
|
|
1097
1326
|
ds,
|
|
1098
1327
|
facet_by=facet_by,
|
|
1099
1328
|
animate_by=animate_by,
|
|
1100
|
-
colors=colors,
|
|
1329
|
+
colors=colors if colors is not None else self._calculation_results.colors,
|
|
1101
1330
|
mode=mode,
|
|
1102
1331
|
title=title,
|
|
1103
1332
|
facet_cols=facet_cols,
|
|
1333
|
+
xlabel='Time in h',
|
|
1334
|
+
**plot_kwargs,
|
|
1104
1335
|
)
|
|
1105
1336
|
default_filetype = '.html'
|
|
1106
1337
|
else:
|
|
1107
1338
|
figure_like = plotting.with_matplotlib(
|
|
1108
|
-
ds
|
|
1109
|
-
colors=colors,
|
|
1339
|
+
ds,
|
|
1340
|
+
colors=colors if colors is not None else self._calculation_results.colors,
|
|
1110
1341
|
mode=mode,
|
|
1111
1342
|
title=title,
|
|
1343
|
+
**plot_kwargs,
|
|
1112
1344
|
)
|
|
1113
1345
|
default_filetype = '.png'
|
|
1114
1346
|
|
|
@@ -1119,19 +1351,21 @@ class _NodeResults(_ElementResults):
|
|
|
1119
1351
|
user_path=None if isinstance(save, bool) else pathlib.Path(save),
|
|
1120
1352
|
show=show,
|
|
1121
1353
|
save=True if save else False,
|
|
1354
|
+
dpi=dpi,
|
|
1122
1355
|
)
|
|
1123
1356
|
|
|
1124
1357
|
def plot_node_balance_pie(
|
|
1125
1358
|
self,
|
|
1126
1359
|
lower_percentage_group: float = 5,
|
|
1127
|
-
colors: plotting.ColorType =
|
|
1360
|
+
colors: plotting.ColorType | None = None,
|
|
1128
1361
|
text_info: str = 'percent+label+value',
|
|
1129
1362
|
save: bool | pathlib.Path = False,
|
|
1130
|
-
show: bool =
|
|
1363
|
+
show: bool | None = None,
|
|
1131
1364
|
engine: plotting.PlottingEngine = 'plotly',
|
|
1132
1365
|
select: dict[FlowSystemDimensions, Any] | None = None,
|
|
1133
1366
|
# Deprecated parameter (kept for backwards compatibility)
|
|
1134
1367
|
indexer: dict[FlowSystemDimensions, Any] | None = None,
|
|
1368
|
+
**plot_kwargs: Any,
|
|
1135
1369
|
) -> plotly.graph_objs.Figure | tuple[plt.Figure, list[plt.Axes]]:
|
|
1136
1370
|
"""Plot pie chart of flow hours distribution.
|
|
1137
1371
|
|
|
@@ -1151,6 +1385,17 @@ class _NodeResults(_ElementResults):
|
|
|
1151
1385
|
engine: Plotting engine ('plotly' or 'matplotlib').
|
|
1152
1386
|
select: Optional data selection dict. Supports single values, lists, slices, and index arrays.
|
|
1153
1387
|
Use this to select specific scenario/period before creating the pie chart.
|
|
1388
|
+
**plot_kwargs: Additional plotting customization options.
|
|
1389
|
+
|
|
1390
|
+
Common options:
|
|
1391
|
+
|
|
1392
|
+
- **dpi** (int): Export resolution in dots per inch. Default: 300.
|
|
1393
|
+
- **hover_template** (str): Hover text template (Plotly only).
|
|
1394
|
+
Example: `hover_template='%{label}: %{value} (%{percent})'`
|
|
1395
|
+
- **text_position** (str): Text position ('inside', 'outside', 'auto').
|
|
1396
|
+
- **hole** (float): Size of donut hole (0.0 to 1.0).
|
|
1397
|
+
|
|
1398
|
+
See :func:`flixopt.plotting.dual_pie_with_plotly` for complete reference.
|
|
1154
1399
|
|
|
1155
1400
|
Examples:
|
|
1156
1401
|
Basic usage (auto-selects first scenario/period if present):
|
|
@@ -1160,6 +1405,14 @@ class _NodeResults(_ElementResults):
|
|
|
1160
1405
|
Explicitly select a scenario and period:
|
|
1161
1406
|
|
|
1162
1407
|
>>> results['Bus'].plot_node_balance_pie(select={'scenario': 'high_demand', 'period': 2030})
|
|
1408
|
+
|
|
1409
|
+
Create a donut chart with custom hover text:
|
|
1410
|
+
|
|
1411
|
+
>>> results['Bus'].plot_node_balance_pie(hole=0.4, hover_template='%{label}: %{value:.2f} (%{percent})')
|
|
1412
|
+
|
|
1413
|
+
High-resolution export:
|
|
1414
|
+
|
|
1415
|
+
>>> results['Bus'].plot_node_balance_pie(save='figure.png', dpi=600)
|
|
1163
1416
|
"""
|
|
1164
1417
|
# Handle deprecated indexer parameter
|
|
1165
1418
|
if indexer is not None:
|
|
@@ -1178,6 +1431,9 @@ class _NodeResults(_ElementResults):
|
|
|
1178
1431
|
)
|
|
1179
1432
|
select = indexer
|
|
1180
1433
|
|
|
1434
|
+
# Extract dpi for export_figure
|
|
1435
|
+
dpi = plot_kwargs.pop('dpi', None) # None uses CONFIG.Plotting.default_dpi
|
|
1436
|
+
|
|
1181
1437
|
inputs = sanitize_dataset(
|
|
1182
1438
|
ds=self.solution[self.inputs] * self._calculation_results.hours_per_timestep,
|
|
1183
1439
|
threshold=1e-5,
|
|
@@ -1193,8 +1449,9 @@ class _NodeResults(_ElementResults):
|
|
|
1193
1449
|
drop_suffix='|',
|
|
1194
1450
|
)
|
|
1195
1451
|
|
|
1196
|
-
inputs,
|
|
1197
|
-
outputs,
|
|
1452
|
+
inputs, suffix_parts_in = _apply_selection_to_data(inputs, select=select, drop=True)
|
|
1453
|
+
outputs, suffix_parts_out = _apply_selection_to_data(outputs, select=select, drop=True)
|
|
1454
|
+
suffix_parts = suffix_parts_in + suffix_parts_out
|
|
1198
1455
|
|
|
1199
1456
|
# Sum over time dimension
|
|
1200
1457
|
inputs = inputs.sum('time')
|
|
@@ -1204,7 +1461,7 @@ class _NodeResults(_ElementResults):
|
|
|
1204
1461
|
# Pie charts need scalar data, so we automatically reduce extra dimensions
|
|
1205
1462
|
extra_dims_inputs = [dim for dim in inputs.dims if dim != 'time']
|
|
1206
1463
|
extra_dims_outputs = [dim for dim in outputs.dims if dim != 'time']
|
|
1207
|
-
extra_dims =
|
|
1464
|
+
extra_dims = sorted(set(extra_dims_inputs + extra_dims_outputs))
|
|
1208
1465
|
|
|
1209
1466
|
if extra_dims:
|
|
1210
1467
|
auto_select = {}
|
|
@@ -1222,27 +1479,28 @@ class _NodeResults(_ElementResults):
|
|
|
1222
1479
|
f'Use select={{"{dim}": value}} to choose a different value.'
|
|
1223
1480
|
)
|
|
1224
1481
|
|
|
1225
|
-
# Apply auto-selection
|
|
1226
|
-
inputs = inputs.sel(auto_select)
|
|
1227
|
-
outputs = outputs.sel(auto_select)
|
|
1482
|
+
# Apply auto-selection only for coords present in each dataset
|
|
1483
|
+
inputs = inputs.sel({k: v for k, v in auto_select.items() if k in inputs.coords})
|
|
1484
|
+
outputs = outputs.sel({k: v for k, v in auto_select.items() if k in outputs.coords})
|
|
1228
1485
|
|
|
1229
1486
|
# Update suffix with auto-selected values
|
|
1230
1487
|
auto_suffix_parts = [f'{dim}={val}' for dim, val in auto_select.items()]
|
|
1231
1488
|
suffix_parts.extend(auto_suffix_parts)
|
|
1232
1489
|
|
|
1233
|
-
suffix = '--' + '-'.join(suffix_parts) if suffix_parts else ''
|
|
1490
|
+
suffix = '--' + '-'.join(sorted(set(suffix_parts))) if suffix_parts else ''
|
|
1234
1491
|
title = f'{self.label} (total flow hours){suffix}'
|
|
1235
1492
|
|
|
1236
1493
|
if engine == 'plotly':
|
|
1237
1494
|
figure_like = plotting.dual_pie_with_plotly(
|
|
1238
|
-
data_left=inputs
|
|
1239
|
-
data_right=outputs
|
|
1240
|
-
colors=colors,
|
|
1495
|
+
data_left=inputs,
|
|
1496
|
+
data_right=outputs,
|
|
1497
|
+
colors=colors if colors is not None else self._calculation_results.colors,
|
|
1241
1498
|
title=title,
|
|
1242
1499
|
text_info=text_info,
|
|
1243
1500
|
subtitles=('Inputs', 'Outputs'),
|
|
1244
1501
|
legend_title='Flows',
|
|
1245
1502
|
lower_percentage_group=lower_percentage_group,
|
|
1503
|
+
**plot_kwargs,
|
|
1246
1504
|
)
|
|
1247
1505
|
default_filetype = '.html'
|
|
1248
1506
|
elif engine == 'matplotlib':
|
|
@@ -1250,11 +1508,12 @@ class _NodeResults(_ElementResults):
|
|
|
1250
1508
|
figure_like = plotting.dual_pie_with_matplotlib(
|
|
1251
1509
|
data_left=inputs.to_pandas(),
|
|
1252
1510
|
data_right=outputs.to_pandas(),
|
|
1253
|
-
colors=colors,
|
|
1511
|
+
colors=colors if colors is not None else self._calculation_results.colors,
|
|
1254
1512
|
title=title,
|
|
1255
1513
|
subtitles=('Inputs', 'Outputs'),
|
|
1256
1514
|
legend_title='Flows',
|
|
1257
1515
|
lower_percentage_group=lower_percentage_group,
|
|
1516
|
+
**plot_kwargs,
|
|
1258
1517
|
)
|
|
1259
1518
|
default_filetype = '.png'
|
|
1260
1519
|
else:
|
|
@@ -1267,6 +1526,7 @@ class _NodeResults(_ElementResults):
|
|
|
1267
1526
|
user_path=None if isinstance(save, bool) else pathlib.Path(save),
|
|
1268
1527
|
show=show,
|
|
1269
1528
|
save=True if save else False,
|
|
1529
|
+
dpi=dpi,
|
|
1270
1530
|
)
|
|
1271
1531
|
|
|
1272
1532
|
def node_balance(
|
|
@@ -1363,16 +1623,17 @@ class ComponentResults(_NodeResults):
|
|
|
1363
1623
|
def plot_charge_state(
|
|
1364
1624
|
self,
|
|
1365
1625
|
save: bool | pathlib.Path = False,
|
|
1366
|
-
show: bool =
|
|
1367
|
-
colors: plotting.ColorType =
|
|
1626
|
+
show: bool | None = None,
|
|
1627
|
+
colors: plotting.ColorType | None = None,
|
|
1368
1628
|
engine: plotting.PlottingEngine = 'plotly',
|
|
1369
1629
|
mode: Literal['area', 'stacked_bar', 'line'] = 'area',
|
|
1370
1630
|
select: dict[FlowSystemDimensions, Any] | None = None,
|
|
1371
1631
|
facet_by: str | list[str] | None = 'scenario',
|
|
1372
1632
|
animate_by: str | None = 'period',
|
|
1373
|
-
facet_cols: int =
|
|
1633
|
+
facet_cols: int | None = None,
|
|
1374
1634
|
# Deprecated parameter (kept for backwards compatibility)
|
|
1375
1635
|
indexer: dict[FlowSystemDimensions, Any] | None = None,
|
|
1636
|
+
**plot_kwargs: Any,
|
|
1376
1637
|
) -> plotly.graph_objs.Figure:
|
|
1377
1638
|
"""Plot storage charge state over time, combined with the node balance with optional faceting and animation.
|
|
1378
1639
|
|
|
@@ -1389,6 +1650,26 @@ class ComponentResults(_NodeResults):
|
|
|
1389
1650
|
animate_by: Dimension to animate over (Plotly only). Creates animation frames that cycle through
|
|
1390
1651
|
dimension values. Only one dimension can be animated. Ignored if not found.
|
|
1391
1652
|
facet_cols: Number of columns in the facet grid layout (default: 3).
|
|
1653
|
+
**plot_kwargs: Additional plotting customization options passed to underlying plotting functions.
|
|
1654
|
+
|
|
1655
|
+
Common options:
|
|
1656
|
+
|
|
1657
|
+
- **dpi** (int): Export resolution in dots per inch. Default: 300.
|
|
1658
|
+
|
|
1659
|
+
**For Plotly engine:**
|
|
1660
|
+
|
|
1661
|
+
- Any Plotly Express parameter for px.bar()/px.line()/px.area()
|
|
1662
|
+
Example: `range_y=[0, 100]`, `line_shape='linear'`
|
|
1663
|
+
|
|
1664
|
+
**For Matplotlib engine:**
|
|
1665
|
+
|
|
1666
|
+
- **plot_kwargs** (dict): Customize plot via `ax.bar()` or `ax.step()`.
|
|
1667
|
+
|
|
1668
|
+
See :func:`flixopt.plotting.with_plotly` and :func:`flixopt.plotting.with_matplotlib`
|
|
1669
|
+
for complete parameter reference.
|
|
1670
|
+
|
|
1671
|
+
Note: For Plotly, you can further customize the returned figure using `fig.update_traces()`
|
|
1672
|
+
and `fig.update_layout()` after calling this method.
|
|
1392
1673
|
|
|
1393
1674
|
Raises:
|
|
1394
1675
|
ValueError: If component is not a storage.
|
|
@@ -1409,6 +1690,16 @@ class ComponentResults(_NodeResults):
|
|
|
1409
1690
|
Facet by scenario AND animate by period:
|
|
1410
1691
|
|
|
1411
1692
|
>>> results['Storage'].plot_charge_state(facet_by='scenario', animate_by='period')
|
|
1693
|
+
|
|
1694
|
+
Custom layout after creation:
|
|
1695
|
+
|
|
1696
|
+
>>> fig = results['Storage'].plot_charge_state(show=False)
|
|
1697
|
+
>>> fig.update_layout(template='plotly_dark', height=800)
|
|
1698
|
+
>>> fig.show()
|
|
1699
|
+
|
|
1700
|
+
High-resolution export:
|
|
1701
|
+
|
|
1702
|
+
>>> results['Storage'].plot_charge_state(save='storage.png', dpi=600)
|
|
1412
1703
|
"""
|
|
1413
1704
|
# Handle deprecated indexer parameter
|
|
1414
1705
|
if indexer is not None:
|
|
@@ -1427,11 +1718,17 @@ class ComponentResults(_NodeResults):
|
|
|
1427
1718
|
)
|
|
1428
1719
|
select = indexer
|
|
1429
1720
|
|
|
1721
|
+
# Extract dpi for export_figure
|
|
1722
|
+
dpi = plot_kwargs.pop('dpi', None) # None uses CONFIG.Plotting.default_dpi
|
|
1723
|
+
|
|
1724
|
+
# Extract charge state line color (for overlay customization)
|
|
1725
|
+
overlay_color = plot_kwargs.pop('charge_state_line_color', 'black')
|
|
1726
|
+
|
|
1430
1727
|
if not self.is_storage:
|
|
1431
1728
|
raise ValueError(f'Cant plot charge_state. "{self.label}" is not a storage')
|
|
1432
1729
|
|
|
1433
1730
|
# Get node balance and charge state
|
|
1434
|
-
ds = self.node_balance(with_last_timestep=True)
|
|
1731
|
+
ds = self.node_balance(with_last_timestep=True).fillna(0)
|
|
1435
1732
|
charge_state_da = self.charge_state
|
|
1436
1733
|
|
|
1437
1734
|
# Apply select filtering
|
|
@@ -1447,25 +1744,28 @@ class ComponentResults(_NodeResults):
|
|
|
1447
1744
|
ds,
|
|
1448
1745
|
facet_by=facet_by,
|
|
1449
1746
|
animate_by=animate_by,
|
|
1450
|
-
colors=colors,
|
|
1747
|
+
colors=colors if colors is not None else self._calculation_results.colors,
|
|
1451
1748
|
mode=mode,
|
|
1452
1749
|
title=title,
|
|
1453
1750
|
facet_cols=facet_cols,
|
|
1751
|
+
xlabel='Time in h',
|
|
1752
|
+
**plot_kwargs,
|
|
1454
1753
|
)
|
|
1455
1754
|
|
|
1456
|
-
#
|
|
1457
|
-
|
|
1458
|
-
charge_state_ds = charge_state_da.to_dataset(name=self._charge_state)
|
|
1755
|
+
# Prepare charge_state as Dataset for plotting
|
|
1756
|
+
charge_state_ds = xr.Dataset({self._charge_state: charge_state_da})
|
|
1459
1757
|
|
|
1460
1758
|
# Plot charge_state with mode='line' to get Scatter traces
|
|
1461
1759
|
charge_state_fig = plotting.with_plotly(
|
|
1462
1760
|
charge_state_ds,
|
|
1463
1761
|
facet_by=facet_by,
|
|
1464
1762
|
animate_by=animate_by,
|
|
1465
|
-
colors=colors,
|
|
1763
|
+
colors=colors if colors is not None else self._calculation_results.colors,
|
|
1466
1764
|
mode='line', # Always line for charge_state
|
|
1467
1765
|
title='', # No title needed for this temp figure
|
|
1468
1766
|
facet_cols=facet_cols,
|
|
1767
|
+
xlabel='Time in h',
|
|
1768
|
+
**plot_kwargs,
|
|
1469
1769
|
)
|
|
1470
1770
|
|
|
1471
1771
|
# Add charge_state traces to the main figure
|
|
@@ -1473,6 +1773,7 @@ class ComponentResults(_NodeResults):
|
|
|
1473
1773
|
for trace in charge_state_fig.data:
|
|
1474
1774
|
trace.line.width = 2 # Make charge_state line more prominent
|
|
1475
1775
|
trace.line.shape = 'linear' # Smooth line for charge state (not stepped like flows)
|
|
1776
|
+
trace.line.color = overlay_color
|
|
1476
1777
|
figure_like.add_trace(trace)
|
|
1477
1778
|
|
|
1478
1779
|
# Also add traces from animation frames if they exist
|
|
@@ -1484,6 +1785,7 @@ class ComponentResults(_NodeResults):
|
|
|
1484
1785
|
for trace in frame.data:
|
|
1485
1786
|
trace.line.width = 2
|
|
1486
1787
|
trace.line.shape = 'linear' # Smooth line for charge state
|
|
1788
|
+
trace.line.color = overlay_color
|
|
1487
1789
|
figure_like.frames[i].data = figure_like.frames[i].data + (trace,)
|
|
1488
1790
|
|
|
1489
1791
|
default_filetype = '.html'
|
|
@@ -1497,10 +1799,11 @@ class ComponentResults(_NodeResults):
|
|
|
1497
1799
|
)
|
|
1498
1800
|
# For matplotlib, plot flows (node balance), then add charge_state as line
|
|
1499
1801
|
fig, ax = plotting.with_matplotlib(
|
|
1500
|
-
ds
|
|
1501
|
-
colors=colors,
|
|
1802
|
+
ds,
|
|
1803
|
+
colors=colors if colors is not None else self._calculation_results.colors,
|
|
1502
1804
|
mode=mode,
|
|
1503
1805
|
title=title,
|
|
1806
|
+
**plot_kwargs,
|
|
1504
1807
|
)
|
|
1505
1808
|
|
|
1506
1809
|
# Add charge_state as a line overlay
|
|
@@ -1510,9 +1813,18 @@ class ComponentResults(_NodeResults):
|
|
|
1510
1813
|
charge_state_df.values.flatten(),
|
|
1511
1814
|
label=self._charge_state,
|
|
1512
1815
|
linewidth=2,
|
|
1513
|
-
color=
|
|
1816
|
+
color=overlay_color,
|
|
1817
|
+
)
|
|
1818
|
+
# Recreate legend with the same styling as with_matplotlib
|
|
1819
|
+
handles, labels = ax.get_legend_handles_labels()
|
|
1820
|
+
ax.legend(
|
|
1821
|
+
handles,
|
|
1822
|
+
labels,
|
|
1823
|
+
loc='upper center',
|
|
1824
|
+
bbox_to_anchor=(0.5, -0.15),
|
|
1825
|
+
ncol=5,
|
|
1826
|
+
frameon=False,
|
|
1514
1827
|
)
|
|
1515
|
-
ax.legend()
|
|
1516
1828
|
fig.tight_layout()
|
|
1517
1829
|
|
|
1518
1830
|
figure_like = fig, ax
|
|
@@ -1525,6 +1837,7 @@ class ComponentResults(_NodeResults):
|
|
|
1525
1837
|
user_path=None if isinstance(save, bool) else pathlib.Path(save),
|
|
1526
1838
|
show=show,
|
|
1527
1839
|
save=True if save else False,
|
|
1840
|
+
dpi=dpi,
|
|
1528
1841
|
)
|
|
1529
1842
|
|
|
1530
1843
|
def node_balance_with_charge_state(
|
|
@@ -1734,8 +2047,7 @@ class SegmentedCalculationResults:
|
|
|
1734
2047
|
folder = pathlib.Path(folder)
|
|
1735
2048
|
path = folder / name
|
|
1736
2049
|
logger.info(f'loading calculation "{name}" from file ("{path.with_suffix(".nc4")}")')
|
|
1737
|
-
|
|
1738
|
-
meta_data = json.load(f)
|
|
2050
|
+
meta_data = fx_io.load_json(path.with_suffix('.json'))
|
|
1739
2051
|
return cls(
|
|
1740
2052
|
[CalculationResults.from_file(folder, sub_name) for sub_name in meta_data['sub_calculations']],
|
|
1741
2053
|
all_timesteps=pd.DatetimeIndex(
|
|
@@ -1763,6 +2075,7 @@ class SegmentedCalculationResults:
|
|
|
1763
2075
|
self.name = name
|
|
1764
2076
|
self.folder = pathlib.Path(folder) if folder is not None else pathlib.Path.cwd() / 'results'
|
|
1765
2077
|
self.hours_per_timestep = FlowSystem.calculate_hours_per_timestep(self.all_timesteps)
|
|
2078
|
+
self._colors = {}
|
|
1766
2079
|
|
|
1767
2080
|
@property
|
|
1768
2081
|
def meta_data(self) -> dict[str, int | list[str]]:
|
|
@@ -1777,6 +2090,64 @@ class SegmentedCalculationResults:
|
|
|
1777
2090
|
def segment_names(self) -> list[str]:
|
|
1778
2091
|
return [segment.name for segment in self.segment_results]
|
|
1779
2092
|
|
|
2093
|
+
@property
|
|
2094
|
+
def colors(self) -> dict[str, str]:
|
|
2095
|
+
return self._colors
|
|
2096
|
+
|
|
2097
|
+
@colors.setter
|
|
2098
|
+
def colors(self, colors: dict[str, str]):
|
|
2099
|
+
"""Applies colors to all segments"""
|
|
2100
|
+
self._colors = colors
|
|
2101
|
+
for segment in self.segment_results:
|
|
2102
|
+
segment.colors = copy.deepcopy(colors)
|
|
2103
|
+
|
|
2104
|
+
def setup_colors(
|
|
2105
|
+
self,
|
|
2106
|
+
config: dict[str, str | list[str]] | str | pathlib.Path | None = None,
|
|
2107
|
+
default_colorscale: str | None = None,
|
|
2108
|
+
) -> dict[str, str]:
|
|
2109
|
+
"""
|
|
2110
|
+
Setup colors for all variables across all segment results.
|
|
2111
|
+
|
|
2112
|
+
This method applies the same color configuration to all segments, ensuring
|
|
2113
|
+
consistent visualization across the entire segmented calculation. The color
|
|
2114
|
+
mapping is propagated to each segment's CalculationResults instance.
|
|
2115
|
+
|
|
2116
|
+
Args:
|
|
2117
|
+
config: Configuration for color assignment. Can be:
|
|
2118
|
+
- dict: Maps components to colors/colorscales:
|
|
2119
|
+
* 'component1': 'red' # Single component to single color
|
|
2120
|
+
* 'component1': '#FF0000' # Single component to hex color
|
|
2121
|
+
- OR maps colorscales to multiple components:
|
|
2122
|
+
* 'colorscale_name': ['component1', 'component2'] # Colorscale across components
|
|
2123
|
+
- str: Path to a JSON/YAML config file or a colorscale name to apply to all
|
|
2124
|
+
- Path: Path to a JSON/YAML config file
|
|
2125
|
+
- None: Use default_colorscale for all components
|
|
2126
|
+
default_colorscale: Default colorscale for unconfigured components (default: 'turbo')
|
|
2127
|
+
|
|
2128
|
+
Examples:
|
|
2129
|
+
```python
|
|
2130
|
+
# Apply colors to all segments
|
|
2131
|
+
segmented_results.setup_colors(
|
|
2132
|
+
{
|
|
2133
|
+
'CHP': 'red',
|
|
2134
|
+
'Blues': ['Storage1', 'Storage2'],
|
|
2135
|
+
'Oranges': ['Solar1', 'Solar2'],
|
|
2136
|
+
}
|
|
2137
|
+
)
|
|
2138
|
+
|
|
2139
|
+
# Use a single colorscale for all components in all segments
|
|
2140
|
+
segmented_results.setup_colors('portland')
|
|
2141
|
+
```
|
|
2142
|
+
|
|
2143
|
+
Returns:
|
|
2144
|
+
Complete variable-to-color mapping dictionary from the first segment
|
|
2145
|
+
(all segments will have the same mapping)
|
|
2146
|
+
"""
|
|
2147
|
+
self.colors = self.segment_results[0].setup_colors(config=config, default_colorscale=default_colorscale)
|
|
2148
|
+
|
|
2149
|
+
return self.colors
|
|
2150
|
+
|
|
1780
2151
|
def solution_without_overlap(self, variable_name: str) -> xr.DataArray:
|
|
1781
2152
|
"""Get variable solution removing segment overlaps.
|
|
1782
2153
|
|
|
@@ -1798,18 +2169,19 @@ class SegmentedCalculationResults:
|
|
|
1798
2169
|
reshape_time: tuple[Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'], Literal['W', 'D', 'h', '15min', 'min']]
|
|
1799
2170
|
| Literal['auto']
|
|
1800
2171
|
| None = 'auto',
|
|
1801
|
-
colors:
|
|
2172
|
+
colors: plotting.ColorType | None = None,
|
|
1802
2173
|
save: bool | pathlib.Path = False,
|
|
1803
|
-
show: bool =
|
|
2174
|
+
show: bool | None = None,
|
|
1804
2175
|
engine: plotting.PlottingEngine = 'plotly',
|
|
1805
2176
|
facet_by: str | list[str] | None = None,
|
|
1806
2177
|
animate_by: str | None = None,
|
|
1807
|
-
facet_cols: int =
|
|
2178
|
+
facet_cols: int | None = None,
|
|
1808
2179
|
fill: Literal['ffill', 'bfill'] | None = 'ffill',
|
|
1809
2180
|
# Deprecated parameters (kept for backwards compatibility)
|
|
1810
2181
|
heatmap_timeframes: Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'] | None = None,
|
|
1811
2182
|
heatmap_timesteps_per_frame: Literal['W', 'D', 'h', '15min', 'min'] | None = None,
|
|
1812
2183
|
color_map: str | None = None,
|
|
2184
|
+
**plot_kwargs: Any,
|
|
1813
2185
|
) -> plotly.graph_objs.Figure | tuple[plt.Figure, plt.Axes]:
|
|
1814
2186
|
"""Plot heatmap of variable solution across segments.
|
|
1815
2187
|
|
|
@@ -1830,6 +2202,17 @@ class SegmentedCalculationResults:
|
|
|
1830
2202
|
heatmap_timeframes: (Deprecated) Use reshape_time instead.
|
|
1831
2203
|
heatmap_timesteps_per_frame: (Deprecated) Use reshape_time instead.
|
|
1832
2204
|
color_map: (Deprecated) Use colors instead.
|
|
2205
|
+
**plot_kwargs: Additional plotting customization options.
|
|
2206
|
+
Common options:
|
|
2207
|
+
|
|
2208
|
+
- **dpi** (int): Export resolution for saved plots. Default: 300.
|
|
2209
|
+
- **vmin** (float): Minimum value for color scale.
|
|
2210
|
+
- **vmax** (float): Maximum value for color scale.
|
|
2211
|
+
|
|
2212
|
+
For Matplotlib heatmaps:
|
|
2213
|
+
|
|
2214
|
+
- **imshow_kwargs** (dict): Additional kwargs for matplotlib's imshow.
|
|
2215
|
+
- **cbar_kwargs** (dict): Additional kwargs for colorbar customization.
|
|
1833
2216
|
|
|
1834
2217
|
Returns:
|
|
1835
2218
|
Figure object.
|
|
@@ -1857,7 +2240,7 @@ class SegmentedCalculationResults:
|
|
|
1857
2240
|
|
|
1858
2241
|
if color_map is not None:
|
|
1859
2242
|
# Check for conflict with new parameter
|
|
1860
|
-
if colors
|
|
2243
|
+
if colors is not None: # Check if user explicitly set colors
|
|
1861
2244
|
raise ValueError(
|
|
1862
2245
|
"Cannot use both deprecated parameter 'color_map' and new parameter 'colors'. Use only 'colors'."
|
|
1863
2246
|
)
|
|
@@ -1884,6 +2267,7 @@ class SegmentedCalculationResults:
|
|
|
1884
2267
|
animate_by=animate_by,
|
|
1885
2268
|
facet_cols=facet_cols,
|
|
1886
2269
|
fill=fill,
|
|
2270
|
+
**plot_kwargs,
|
|
1887
2271
|
)
|
|
1888
2272
|
|
|
1889
2273
|
def to_file(self, folder: str | pathlib.Path | None = None, name: str | None = None, compression: int = 5):
|
|
@@ -1907,8 +2291,7 @@ class SegmentedCalculationResults:
|
|
|
1907
2291
|
for segment in self.segment_results:
|
|
1908
2292
|
segment.to_file(folder=folder, name=segment.name, compression=compression)
|
|
1909
2293
|
|
|
1910
|
-
|
|
1911
|
-
json.dump(self.meta_data, f, indent=4, ensure_ascii=False)
|
|
2294
|
+
fx_io.save_json(self.meta_data, path.with_suffix('.json'))
|
|
1912
2295
|
logger.info(f'Saved calculation "{name}" to {path}')
|
|
1913
2296
|
|
|
1914
2297
|
|
|
@@ -1916,14 +2299,14 @@ def plot_heatmap(
|
|
|
1916
2299
|
data: xr.DataArray | xr.Dataset,
|
|
1917
2300
|
name: str | None = None,
|
|
1918
2301
|
folder: pathlib.Path | None = None,
|
|
1919
|
-
colors: plotting.ColorType =
|
|
2302
|
+
colors: plotting.ColorType | None = None,
|
|
1920
2303
|
save: bool | pathlib.Path = False,
|
|
1921
|
-
show: bool =
|
|
2304
|
+
show: bool | None = None,
|
|
1922
2305
|
engine: plotting.PlottingEngine = 'plotly',
|
|
1923
2306
|
select: dict[str, Any] | None = None,
|
|
1924
2307
|
facet_by: str | list[str] | None = None,
|
|
1925
2308
|
animate_by: str | None = None,
|
|
1926
|
-
facet_cols: int =
|
|
2309
|
+
facet_cols: int | None = None,
|
|
1927
2310
|
reshape_time: tuple[Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'], Literal['W', 'D', 'h', '15min', 'min']]
|
|
1928
2311
|
| Literal['auto']
|
|
1929
2312
|
| None = 'auto',
|
|
@@ -1933,6 +2316,7 @@ def plot_heatmap(
|
|
|
1933
2316
|
heatmap_timeframes: Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'] | None = None,
|
|
1934
2317
|
heatmap_timesteps_per_frame: Literal['W', 'D', 'h', '15min', 'min'] | None = None,
|
|
1935
2318
|
color_map: str | None = None,
|
|
2319
|
+
**plot_kwargs: Any,
|
|
1936
2320
|
):
|
|
1937
2321
|
"""Plot heatmap visualization with support for multi-variable, faceting, and animation.
|
|
1938
2322
|
|
|
@@ -2003,8 +2387,7 @@ def plot_heatmap(
|
|
|
2003
2387
|
|
|
2004
2388
|
# Handle deprecated color_map parameter
|
|
2005
2389
|
if color_map is not None:
|
|
2006
|
-
|
|
2007
|
-
if colors != 'viridis': # User explicitly set colors
|
|
2390
|
+
if colors is not None: # User explicitly set colors
|
|
2008
2391
|
raise ValueError(
|
|
2009
2392
|
"Cannot use both deprecated parameter 'color_map' and new parameter 'colors'. Use only 'colors'."
|
|
2010
2393
|
)
|
|
@@ -2087,6 +2470,9 @@ def plot_heatmap(
|
|
|
2087
2470
|
timeframes, timesteps_per_frame = reshape_time
|
|
2088
2471
|
title += f' ({timeframes} vs {timesteps_per_frame})'
|
|
2089
2472
|
|
|
2473
|
+
# Extract dpi before passing to plotting functions
|
|
2474
|
+
dpi = plot_kwargs.pop('dpi', None) # None uses CONFIG.Plotting.default_dpi
|
|
2475
|
+
|
|
2090
2476
|
# Plot with appropriate engine
|
|
2091
2477
|
if engine == 'plotly':
|
|
2092
2478
|
figure_like = plotting.heatmap_with_plotly(
|
|
@@ -2098,6 +2484,7 @@ def plot_heatmap(
|
|
|
2098
2484
|
facet_cols=facet_cols,
|
|
2099
2485
|
reshape_time=reshape_time,
|
|
2100
2486
|
fill=fill,
|
|
2487
|
+
**plot_kwargs,
|
|
2101
2488
|
)
|
|
2102
2489
|
default_filetype = '.html'
|
|
2103
2490
|
elif engine == 'matplotlib':
|
|
@@ -2107,6 +2494,7 @@ def plot_heatmap(
|
|
|
2107
2494
|
title=title,
|
|
2108
2495
|
reshape_time=reshape_time,
|
|
2109
2496
|
fill=fill,
|
|
2497
|
+
**plot_kwargs,
|
|
2110
2498
|
)
|
|
2111
2499
|
default_filetype = '.png'
|
|
2112
2500
|
else:
|
|
@@ -2123,6 +2511,7 @@ def plot_heatmap(
|
|
|
2123
2511
|
user_path=None if isinstance(save, bool) else pathlib.Path(save),
|
|
2124
2512
|
show=show,
|
|
2125
2513
|
save=True if save else False,
|
|
2514
|
+
dpi=dpi,
|
|
2126
2515
|
)
|
|
2127
2516
|
|
|
2128
2517
|
|