flixopt 3.1.1__tar.gz → 3.2.0__tar.gz

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.

Files changed (34) hide show
  1. {flixopt-3.1.1 → flixopt-3.2.0}/CHANGELOG.md +59 -1
  2. {flixopt-3.1.1/flixopt.egg-info → flixopt-3.2.0}/PKG-INFO +2 -2
  3. {flixopt-3.1.1 → flixopt-3.2.0}/flixopt/aggregation.py +13 -4
  4. {flixopt-3.1.1 → flixopt-3.2.0}/flixopt/calculation.py +2 -3
  5. flixopt-3.2.0/flixopt/color_processing.py +261 -0
  6. {flixopt-3.1.1 → flixopt-3.2.0}/flixopt/config.py +59 -4
  7. {flixopt-3.1.1 → flixopt-3.2.0}/flixopt/flow_system.py +5 -3
  8. {flixopt-3.1.1 → flixopt-3.2.0}/flixopt/interface.py +2 -1
  9. {flixopt-3.1.1 → flixopt-3.2.0}/flixopt/io.py +239 -22
  10. {flixopt-3.1.1 → flixopt-3.2.0}/flixopt/plotting.py +583 -789
  11. {flixopt-3.1.1 → flixopt-3.2.0}/flixopt/results.py +445 -56
  12. {flixopt-3.1.1 → flixopt-3.2.0}/flixopt/structure.py +1 -3
  13. {flixopt-3.1.1 → flixopt-3.2.0/flixopt.egg-info}/PKG-INFO +2 -2
  14. {flixopt-3.1.1 → flixopt-3.2.0}/flixopt.egg-info/SOURCES.txt +1 -1
  15. {flixopt-3.1.1 → flixopt-3.2.0}/flixopt.egg-info/requires.txt +1 -1
  16. {flixopt-3.1.1 → flixopt-3.2.0}/pyproject.toml +1 -1
  17. flixopt-3.1.1/flixopt/utils.py +0 -86
  18. {flixopt-3.1.1 → flixopt-3.2.0}/LICENSE +0 -0
  19. {flixopt-3.1.1 → flixopt-3.2.0}/MANIFEST.in +0 -0
  20. {flixopt-3.1.1 → flixopt-3.2.0}/README.md +0 -0
  21. {flixopt-3.1.1 → flixopt-3.2.0}/flixopt/__init__.py +0 -0
  22. {flixopt-3.1.1 → flixopt-3.2.0}/flixopt/commons.py +0 -0
  23. {flixopt-3.1.1 → flixopt-3.2.0}/flixopt/components.py +0 -0
  24. {flixopt-3.1.1 → flixopt-3.2.0}/flixopt/core.py +0 -0
  25. {flixopt-3.1.1 → flixopt-3.2.0}/flixopt/effects.py +0 -0
  26. {flixopt-3.1.1 → flixopt-3.2.0}/flixopt/elements.py +0 -0
  27. {flixopt-3.1.1 → flixopt-3.2.0}/flixopt/features.py +0 -0
  28. {flixopt-3.1.1 → flixopt-3.2.0}/flixopt/linear_converters.py +0 -0
  29. {flixopt-3.1.1 → flixopt-3.2.0}/flixopt/modeling.py +0 -0
  30. {flixopt-3.1.1 → flixopt-3.2.0}/flixopt/network_app.py +0 -0
  31. {flixopt-3.1.1 → flixopt-3.2.0}/flixopt/solvers.py +0 -0
  32. {flixopt-3.1.1 → flixopt-3.2.0}/flixopt.egg-info/dependency_links.txt +0 -0
  33. {flixopt-3.1.1 → flixopt-3.2.0}/flixopt.egg-info/top_level.txt +0 -0
  34. {flixopt-3.1.1 → flixopt-3.2.0}/setup.cfg +0 -0
@@ -21,8 +21,9 @@ Please keep the format of the changelog consistent with the other releases, so t
21
21
 
22
22
  ## [Template] - ????-??-??
23
23
 
24
- If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/).
24
+ **Summary**:
25
25
 
26
+ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/).
26
27
 
27
28
  ### ✨ Added
28
29
 
@@ -50,6 +51,8 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp
50
51
 
51
52
  ## [Unreleased] - ????-??-??
52
53
 
54
+ **Summary**:
55
+
53
56
  If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/).
54
57
 
55
58
  ### ✨ Added
@@ -78,6 +81,61 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp
78
81
 
79
82
  Until here -->
80
83
 
84
+ ## [3.2.0] - 2025-10-26
85
+
86
+ **Summary**: Enhanced plotting capabilities with consistent color management, custom plotting kwargs support, and centralized I/O handling.
87
+
88
+ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/).
89
+
90
+ ### ✨ Added
91
+
92
+ **Color management:**
93
+ - **`setup_colors()` method** for `CalculationResults` and `SegmentedCalculationResults` to configure consistent colors across all plots
94
+ - Group components by colorscales: `results.setup_colors({'CHP': 'reds', 'Storage': 'blues', 'Greys': ['Grid', 'Demand']})`
95
+ - Automatically propagates to all segments in segmented calculations
96
+ - Colors persist across all plot calls unless explicitly overridden
97
+ - **Flexible color inputs**: Supports colorscale names (e.g., 'turbo', 'plasma'), color lists, or label-to-color dictionaries
98
+ - **Cross-backend compatibility**: Seamless color handling for both Plotly and Matplotlib
99
+
100
+ **Plotting customization:**
101
+ - **Plotting kwargs support**: Pass additional arguments to plotting backends via `px_kwargs`, `plot_kwargs`, and `backend_kwargs` parameters
102
+ - **New `CONFIG.Plotting` configuration section**:
103
+ - `default_show`: Control default plot visibility
104
+ - `default_engine`: Choose 'plotly' or 'matplotlib'
105
+ - `default_dpi`: Set resolution for saved plots
106
+ - `default_facet_cols`: Configure default faceting columns
107
+ - `default_sequential_colorscale`: Default for heatmaps (now 'turbo')
108
+ - `default_qualitative_colorscale`: Default for categorical plots (now 'plotly')
109
+
110
+ **I/O improvements:**
111
+ - Centralized JSON/YAML I/O with auto-format detection
112
+ - Enhanced NetCDF handling with consistent engine usage
113
+ - Better numeric formatting in YAML exports
114
+
115
+ ### ♻️ Changed
116
+ - **Default colorscale**: Changed from 'viridis' to 'turbo' for better perceptual uniformity
117
+ - **Color terminology**: Standardized from "colormap" to "colorscale" throughout for Plotly consistency
118
+ - **Plotting internals**: Now use `xr.Dataset` as primary data type (DataFrames automatically converted)
119
+ - **NetCDF engine**: Switched back to netcdf4 engine following xarray updates and performance benchmarks
120
+
121
+ ### 🔥 Removed
122
+ - Removed unused `plotting.pie_with_plotly()` method
123
+
124
+ ### 🐛 Fixed
125
+ - Improved error messages when using `engine='matplotlib'` with multidimensional data
126
+ - Better dimension validation in `results.plot_heatmap()`
127
+
128
+ ### 📝 Docs
129
+ - Enhanced examples demonstrating `setup_colors()` usage
130
+ - Updated terminology from "colormap" to "colorscale" in docstrings
131
+
132
+ ### 👷 Development
133
+ - Fixed concurrency issue in CI
134
+ - Centralized color processing logic into dedicated module
135
+ - Refactored to function-based color handling for simpler API
136
+
137
+ ---
138
+
81
139
  ## [3.1.1] - 2025-10-20
82
140
  **Summary**: Fixed a bug when acessing the `effects_per_component` dataset in results without periodic effects.
83
141
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flixopt
3
- Version: 3.1.1
3
+ Version: 3.2.0
4
4
  Summary: Vector based energy and material flow optimization framework in Python.
5
5
  Author-email: "Chair of Building Energy Systems and Heat Supply, TU Dresden" <peter.stange@tu-dresden.de>, Felix Bumann <felixbumann387@gmail.com>, Felix Panitz <baumbude@googlemail.com>, Peter Stange <peter.stange@tu-dresden.de>
6
6
  Maintainer-email: Felix Bumann <felixbumann387@gmail.com>, Peter Stange <peter.stange@tu-dresden.de>
@@ -24,7 +24,7 @@ Requires-Dist: numpy<3,>=1.21.5
24
24
  Requires-Dist: pandas<3,>=2.0.0
25
25
  Requires-Dist: xarray<2026.0,>=2024.2.0
26
26
  Requires-Dist: linopy<0.6,>=0.5.1
27
- Requires-Dist: h5netcdf<2,>=1.0.0
27
+ Requires-Dist: netcdf4<2,>=1.6.1
28
28
  Requires-Dist: pyyaml<7,>=6.0.0
29
29
  Requires-Dist: rich<15,>=13.0.0
30
30
  Requires-Dist: tomli<3,>=2.0.1; python_version < "3.11"
@@ -20,7 +20,9 @@ try:
20
20
  except ImportError:
21
21
  TSAM_AVAILABLE = False
22
22
 
23
+ from .color_processing import process_colors
23
24
  from .components import Storage
25
+ from .config import CONFIG
24
26
  from .structure import (
25
27
  FlowSystemModel,
26
28
  Submodel,
@@ -141,7 +143,7 @@ class Aggregation:
141
143
  def use_extreme_periods(self):
142
144
  return self.time_series_for_high_peaks or self.time_series_for_low_peaks
143
145
 
144
- def plot(self, colormap: str = 'viridis', show: bool = True, save: pathlib.Path | None = None) -> go.Figure:
146
+ def plot(self, colormap: str | None = None, show: bool = True, save: pathlib.Path | None = None) -> go.Figure:
145
147
  from . import plotting
146
148
 
147
149
  df_org = self.original_data.copy().rename(
@@ -150,13 +152,20 @@ class Aggregation:
150
152
  df_agg = self.aggregated_data.copy().rename(
151
153
  columns={col: f'Aggregated - {col}' for col in self.aggregated_data.columns}
152
154
  )
153
- fig = plotting.with_plotly(df_org, 'line', colors=colormap)
155
+ colors = list(
156
+ process_colors(colormap or CONFIG.Plotting.default_qualitative_colorscale, list(df_org.columns)).values()
157
+ )
158
+ fig = plotting.with_plotly(df_org.to_xarray(), 'line', colors=colors, xlabel='Time in h')
154
159
  for trace in fig.data:
155
160
  trace.update(dict(line=dict(dash='dash')))
156
- fig = plotting.with_plotly(df_agg, 'line', colors=colormap, fig=fig)
161
+ fig2 = plotting.with_plotly(df_agg.to_xarray(), 'line', colors=colors, xlabel='Time in h')
162
+ for trace in fig2.data:
163
+ fig.add_trace(trace)
157
164
 
158
165
  fig.update_layout(
159
- title='Original vs Aggregated Data (original = ---)', xaxis_title='Index', yaxis_title='Value'
166
+ title='Original vs Aggregated Data (original = ---)',
167
+ xaxis_title='Time in h',
168
+ yaxis_title='Value',
160
169
  )
161
170
 
162
171
  plotting.export_figure(
@@ -22,7 +22,6 @@ import numpy as np
22
22
  import yaml
23
23
 
24
24
  from . import io as fx_io
25
- from . import utils as utils
26
25
  from .aggregation import Aggregation, AggregationModel, AggregationParameters
27
26
  from .components import Storage
28
27
  from .config import CONFIG
@@ -144,7 +143,7 @@ class Calculation:
144
143
  ],
145
144
  }
146
145
 
147
- return utils.round_nested_floats(main_results)
146
+ return fx_io.round_nested_floats(main_results)
148
147
 
149
148
  @property
150
149
  def summary(self):
@@ -253,7 +252,7 @@ class FullCalculation(Calculation):
253
252
  logger.info(
254
253
  f'{" Main Results ":#^80}\n'
255
254
  + yaml.dump(
256
- utils.round_nested_floats(self.main_results),
255
+ self.main_results,
257
256
  default_flow_style=False,
258
257
  sort_keys=False,
259
258
  allow_unicode=True,
@@ -0,0 +1,261 @@
1
+ """Simplified color handling for visualization.
2
+
3
+ This module provides clean color processing that transforms various input formats
4
+ into a label-to-color mapping dictionary, without needing to know about the plotting engine.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+
11
+ import matplotlib.colors as mcolors
12
+ import matplotlib.pyplot as plt
13
+ import plotly.express as px
14
+ from plotly.exceptions import PlotlyError
15
+
16
+ logger = logging.getLogger('flixopt')
17
+
18
+
19
+ def _rgb_string_to_hex(color: str) -> str:
20
+ """Convert Plotly RGB/RGBA string format to hex.
21
+
22
+ Args:
23
+ color: Color in format 'rgb(R, G, B)', 'rgba(R, G, B, A)' or already in hex
24
+
25
+ Returns:
26
+ Color in hex format '#RRGGBB'
27
+ """
28
+ color = color.strip()
29
+
30
+ # If already hex, return as-is
31
+ if color.startswith('#'):
32
+ return color
33
+
34
+ # Try to parse rgb() or rgba()
35
+ try:
36
+ if color.startswith('rgb('):
37
+ # Extract RGB values from 'rgb(R, G, B)' format
38
+ rgb_str = color[4:-1] # Remove 'rgb(' and ')'
39
+ elif color.startswith('rgba('):
40
+ # Extract RGBA values from 'rgba(R, G, B, A)' format
41
+ rgb_str = color[5:-1] # Remove 'rgba(' and ')'
42
+ else:
43
+ return color
44
+
45
+ # Split on commas and parse first three components
46
+ components = rgb_str.split(',')
47
+ if len(components) < 3:
48
+ return color
49
+
50
+ # Parse and clamp the first three components
51
+ r = max(0, min(255, int(round(float(components[0].strip())))))
52
+ g = max(0, min(255, int(round(float(components[1].strip())))))
53
+ b = max(0, min(255, int(round(float(components[2].strip())))))
54
+
55
+ return f'#{r:02x}{g:02x}{b:02x}'
56
+ except (ValueError, IndexError):
57
+ # If parsing fails, return original
58
+ return color
59
+
60
+
61
+ def process_colors(
62
+ colors: None | str | list[str] | dict[str, str],
63
+ labels: list[str],
64
+ default_colorscale: str = 'turbo',
65
+ ) -> dict[str, str]:
66
+ """Process color input and return a label-to-color mapping.
67
+
68
+ This function takes flexible color input and always returns a dictionary
69
+ mapping each label to a specific color string. The plotting engine can then
70
+ use this mapping as needed.
71
+
72
+ Args:
73
+ colors: Color specification in one of four formats:
74
+ - None: Use the default colorscale
75
+ - str: Name of a colorscale (e.g., 'turbo', 'plasma', 'Set1', 'portland')
76
+ - list[str]: List of color strings (hex, named colors, etc.)
77
+ - dict[str, str]: Direct label-to-color mapping
78
+ labels: List of labels that need colors assigned
79
+ default_colorscale: Fallback colorscale name if requested scale not found
80
+
81
+ Returns:
82
+ Dictionary mapping each label to a color string
83
+
84
+ Examples:
85
+ >>> # Using None - applies default colorscale
86
+ >>> process_colors(None, ['A', 'B', 'C'])
87
+ {'A': '#0d0887', 'B': '#7e03a8', 'C': '#cc4778'}
88
+
89
+ >>> # Using a colorscale name
90
+ >>> process_colors('plasma', ['A', 'B', 'C'])
91
+ {'A': '#0d0887', 'B': '#7e03a8', 'C': '#cc4778'}
92
+
93
+ >>> # Using a list of colors
94
+ >>> process_colors(['red', 'blue', 'green'], ['A', 'B', 'C'])
95
+ {'A': 'red', 'B': 'blue', 'C': 'green'}
96
+
97
+ >>> # Using a pre-made mapping
98
+ >>> process_colors({'A': 'red', 'B': 'blue'}, ['A', 'B', 'C'])
99
+ {'A': 'red', 'B': 'blue', 'C': '#0d0887'} # C gets color from default scale
100
+ """
101
+ if not labels:
102
+ return {}
103
+
104
+ # Case 1: Already a mapping dictionary
105
+ if isinstance(colors, dict):
106
+ return _fill_missing_colors(colors, labels, default_colorscale)
107
+
108
+ # Case 2: None or colorscale name (string)
109
+ if colors is None or isinstance(colors, str):
110
+ colorscale_name = colors if colors is not None else default_colorscale
111
+ color_list = _get_colors_from_scale(colorscale_name, len(labels), default_colorscale)
112
+ return dict(zip(labels, color_list, strict=False))
113
+
114
+ # Case 3: List of colors
115
+ if isinstance(colors, list):
116
+ if len(colors) == 0:
117
+ logger.warning(f'Empty color list provided. Using {default_colorscale} instead.')
118
+ color_list = _get_colors_from_scale(default_colorscale, len(labels), default_colorscale)
119
+ return dict(zip(labels, color_list, strict=False))
120
+
121
+ if len(colors) < len(labels):
122
+ logger.debug(
123
+ f'Not enough colors provided ({len(colors)}) for all labels ({len(labels)}). Colors will cycle.'
124
+ )
125
+
126
+ # Cycle through colors if we don't have enough
127
+ return {label: colors[i % len(colors)] for i, label in enumerate(labels)}
128
+
129
+ raise TypeError(f'colors must be None, str, list, or dict, got {type(colors)}')
130
+
131
+
132
+ def _fill_missing_colors(
133
+ color_mapping: dict[str, str],
134
+ labels: list[str],
135
+ default_colorscale: str,
136
+ ) -> dict[str, str]:
137
+ """Fill in missing labels in a color mapping using a colorscale.
138
+
139
+ Args:
140
+ color_mapping: Partial label-to-color mapping
141
+ labels: All labels that need colors
142
+ default_colorscale: Colorscale to use for missing labels
143
+
144
+ Returns:
145
+ Complete label-to-color mapping
146
+ """
147
+ missing_labels = [label for label in labels if label not in color_mapping]
148
+
149
+ if not missing_labels:
150
+ return color_mapping.copy()
151
+
152
+ # Log warning about missing labels
153
+ logger.debug(f'Labels missing colors: {missing_labels}. Using {default_colorscale} for these.')
154
+
155
+ # Get colors for missing labels
156
+ missing_colors = _get_colors_from_scale(default_colorscale, len(missing_labels), default_colorscale)
157
+
158
+ # Combine existing and new colors
159
+ result = color_mapping.copy()
160
+ result.update(dict(zip(missing_labels, missing_colors, strict=False)))
161
+ return result
162
+
163
+
164
+ def _get_colors_from_scale(
165
+ colorscale_name: str,
166
+ num_colors: int,
167
+ fallback_scale: str,
168
+ ) -> list[str]:
169
+ """Extract a list of colors from a named colorscale.
170
+
171
+ Tries to get colors from the named scale (Plotly first, then Matplotlib),
172
+ falls back to the fallback scale if not found.
173
+
174
+ Args:
175
+ colorscale_name: Name of the colorscale to try
176
+ num_colors: Number of colors needed
177
+ fallback_scale: Fallback colorscale name if first fails
178
+
179
+ Returns:
180
+ List of color strings (hex format)
181
+ """
182
+ # Try to get the requested colorscale
183
+ colors = _try_get_colorscale(colorscale_name, num_colors)
184
+
185
+ if colors is not None:
186
+ return colors
187
+
188
+ # Fallback to default
189
+ logger.warning(f"Colorscale '{colorscale_name}' not found. Using '{fallback_scale}' instead.")
190
+
191
+ colors = _try_get_colorscale(fallback_scale, num_colors)
192
+
193
+ if colors is not None:
194
+ return colors
195
+
196
+ # Ultimate fallback: just use basic colors
197
+ logger.warning(f"Fallback colorscale '{fallback_scale}' also not found. Using basic colors.")
198
+ basic_colors = [
199
+ '#1f77b4',
200
+ '#ff7f0e',
201
+ '#2ca02c',
202
+ '#d62728',
203
+ '#9467bd',
204
+ '#8c564b',
205
+ '#e377c2',
206
+ '#7f7f7f',
207
+ '#bcbd22',
208
+ '#17becf',
209
+ ]
210
+ return [basic_colors[i % len(basic_colors)] for i in range(num_colors)]
211
+
212
+
213
+ def _try_get_colorscale(colorscale_name: str, num_colors: int) -> list[str] | None:
214
+ """Try to get colors from Plotly or Matplotlib colorscales.
215
+
216
+ Tries Plotly colorscales first (both qualitative and sequential),
217
+ then falls back to Matplotlib colorscales.
218
+
219
+ Args:
220
+ colorscale_name: Name of the colorscale
221
+ num_colors: Number of colors needed
222
+
223
+ Returns:
224
+ List of color strings (hex format) if successful, None if colorscale not found
225
+ """
226
+ # First try Plotly qualitative (discrete) color sequences
227
+ colorscale_title = colorscale_name.title()
228
+ if hasattr(px.colors.qualitative, colorscale_title):
229
+ color_list = getattr(px.colors.qualitative, colorscale_title)
230
+ # Convert to hex format for matplotlib compatibility
231
+ return [_rgb_string_to_hex(color_list[i % len(color_list)]) for i in range(num_colors)]
232
+
233
+ # Then try Plotly sequential/continuous colorscales
234
+ try:
235
+ colorscale = px.colors.get_colorscale(colorscale_name)
236
+ # Sample evenly from the colorscale
237
+ if num_colors == 1:
238
+ sample_points = [0.5]
239
+ else:
240
+ sample_points = [i / (num_colors - 1) for i in range(num_colors)]
241
+ colors = px.colors.sample_colorscale(colorscale, sample_points)
242
+ # Convert to hex format for matplotlib compatibility
243
+ return [_rgb_string_to_hex(c) for c in colors]
244
+ except (PlotlyError, ValueError):
245
+ pass
246
+
247
+ # Finally try Matplotlib colorscales
248
+ try:
249
+ cmap = plt.get_cmap(colorscale_name)
250
+
251
+ # Sample evenly from the colorscale
252
+ if num_colors == 1:
253
+ colors = [cmap(0.5)]
254
+ else:
255
+ colors = [cmap(i / (num_colors - 1)) for i in range(num_colors)]
256
+
257
+ # Convert RGBA tuples to hex strings
258
+ return [mcolors.rgb2hex(color[:3]) for color in colors]
259
+
260
+ except (ValueError, KeyError):
261
+ return None
@@ -8,7 +8,6 @@ from pathlib import Path
8
8
  from types import MappingProxyType
9
9
  from typing import Literal
10
10
 
11
- import yaml
12
11
  from rich.console import Console
13
12
  from rich.logging import RichHandler
14
13
  from rich.style import Style
@@ -54,6 +53,16 @@ _DEFAULTS = MappingProxyType(
54
53
  'big_binary_bound': 100_000,
55
54
  }
56
55
  ),
56
+ 'plotting': MappingProxyType(
57
+ {
58
+ 'default_show': True,
59
+ 'default_engine': 'plotly',
60
+ 'default_dpi': 300,
61
+ 'default_facet_cols': 3,
62
+ 'default_sequential_colorscale': 'turbo',
63
+ 'default_qualitative_colorscale': 'plotly',
64
+ }
65
+ ),
57
66
  }
58
67
  )
59
68
 
@@ -185,6 +194,42 @@ class CONFIG:
185
194
  epsilon: float = _DEFAULTS['modeling']['epsilon']
186
195
  big_binary_bound: int = _DEFAULTS['modeling']['big_binary_bound']
187
196
 
197
+ class Plotting:
198
+ """Plotting configuration.
199
+
200
+ Configure backends via environment variables:
201
+ - Matplotlib: Set `MPLBACKEND` environment variable (e.g., 'Agg', 'TkAgg')
202
+ - Plotly: Set `PLOTLY_RENDERER` or use `plotly.io.renderers.default`
203
+
204
+ Attributes:
205
+ default_show: Default value for the `show` parameter in plot methods.
206
+ default_engine: Default plotting engine.
207
+ default_dpi: Default DPI for saved plots.
208
+ default_facet_cols: Default number of columns for faceted plots.
209
+ default_sequential_colorscale: Default colorscale for heatmaps and continuous data.
210
+ default_qualitative_colorscale: Default colormap for categorical plots (bar/line/area charts).
211
+
212
+ Examples:
213
+ ```python
214
+ # Set consistent theming
215
+ CONFIG.Plotting.plotly_template = 'plotly_dark'
216
+ CONFIG.apply()
217
+
218
+ # Configure default export and color settings
219
+ CONFIG.Plotting.default_dpi = 600
220
+ CONFIG.Plotting.default_sequential_colorscale = 'plasma'
221
+ CONFIG.Plotting.default_qualitative_colorscale = 'Dark24'
222
+ CONFIG.apply()
223
+ ```
224
+ """
225
+
226
+ default_show: bool = _DEFAULTS['plotting']['default_show']
227
+ default_engine: Literal['plotly', 'matplotlib'] = _DEFAULTS['plotting']['default_engine']
228
+ default_dpi: int = _DEFAULTS['plotting']['default_dpi']
229
+ default_facet_cols: int = _DEFAULTS['plotting']['default_facet_cols']
230
+ default_sequential_colorscale: str = _DEFAULTS['plotting']['default_sequential_colorscale']
231
+ default_qualitative_colorscale: str = _DEFAULTS['plotting']['default_qualitative_colorscale']
232
+
188
233
  config_name: str = _DEFAULTS['config_name']
189
234
 
190
235
  @classmethod
@@ -253,13 +298,15 @@ class CONFIG:
253
298
  Raises:
254
299
  FileNotFoundError: If the config file does not exist.
255
300
  """
301
+ # Import here to avoid circular import
302
+ from . import io as fx_io
303
+
256
304
  config_path = Path(config_file)
257
305
  if not config_path.exists():
258
306
  raise FileNotFoundError(f'Config file not found: {config_file}')
259
307
 
260
- with config_path.open() as file:
261
- config_dict = yaml.safe_load(file) or {}
262
- cls._apply_config_dict(config_dict)
308
+ config_dict = fx_io.load_yaml(config_path)
309
+ cls._apply_config_dict(config_dict)
263
310
 
264
311
  cls.apply()
265
312
 
@@ -319,6 +366,14 @@ class CONFIG:
319
366
  'epsilon': cls.Modeling.epsilon,
320
367
  'big_binary_bound': cls.Modeling.big_binary_bound,
321
368
  },
369
+ 'plotting': {
370
+ 'default_show': cls.Plotting.default_show,
371
+ 'default_engine': cls.Plotting.default_engine,
372
+ 'default_dpi': cls.Plotting.default_dpi,
373
+ 'default_facet_cols': cls.Plotting.default_facet_cols,
374
+ 'default_sequential_colorscale': cls.Plotting.default_sequential_colorscale,
375
+ 'default_qualitative_colorscale': cls.Plotting.default_qualitative_colorscale,
376
+ },
322
377
  }
323
378
 
324
379
 
@@ -4,7 +4,6 @@ This module contains the FlowSystem class, which is used to collect instances of
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
- import json
8
7
  import logging
9
8
  import warnings
10
9
  from typing import TYPE_CHECKING, Any, Literal, Optional
@@ -13,6 +12,7 @@ import numpy as np
13
12
  import pandas as pd
14
13
  import xarray as xr
15
14
 
15
+ from .config import CONFIG
16
16
  from .core import (
17
17
  ConversionError,
18
18
  DataConverter,
@@ -484,7 +484,7 @@ class FlowSystem(Interface):
484
484
  | list[
485
485
  Literal['nodes', 'edges', 'layout', 'interaction', 'manipulation', 'physics', 'selection', 'renderer']
486
486
  ] = True,
487
- show: bool = False,
487
+ show: bool | None = None,
488
488
  ) -> pyvis.network.Network | None:
489
489
  """
490
490
  Visualizes the network structure of a FlowSystem using PyVis, saving it as an interactive HTML file.
@@ -514,7 +514,9 @@ class FlowSystem(Interface):
514
514
  from . import plotting
515
515
 
516
516
  node_infos, edge_infos = self.network_infos()
517
- return plotting.plot_network(node_infos, edge_infos, path, controls, show)
517
+ return plotting.plot_network(
518
+ node_infos, edge_infos, path, controls, show if show is not None else CONFIG.Plotting.default_show
519
+ )
518
520
 
519
521
  def start_network_app(self):
520
522
  """Visualizes the network structure of a FlowSystem using Dash, Cytoscape, and networkx.
@@ -712,6 +712,8 @@ class InvestParameters(Interface):
712
712
  Combinable with effects_of_investment and effects_of_investment_per_size.
713
713
  effects_of_retirement: Costs incurred if NOT investing (demolition, penalties).
714
714
  Dict: {'effect_name': value}.
715
+ linked_periods: Describes which periods are linked. 1 means linked, 0 means size=0. None means no linked periods.
716
+ For convenience, pass a tuple containing the first and last period (2025, 2039), linking them and those in between
715
717
 
716
718
  Deprecated Args:
717
719
  fix_effects: **Deprecated**. Use `effects_of_investment` instead.
@@ -724,7 +726,6 @@ class InvestParameters(Interface):
724
726
  Will be removed in version 4.0.
725
727
  optional: DEPRECATED. Use `mandatory` instead. Opposite of `mandatory`.
726
728
  Will be removed in version 4.0.
727
- linked_periods: Describes which periods are linked. 1 means linked, 0 means size=0. None means no linked periods.
728
729
 
729
730
  Cost Annualization Requirements:
730
731
  All cost values must be properly weighted to match the optimization model's time horizon.