flixopt 2.1.6__py3-none-any.whl → 2.1.8__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.
- docs/examples/00-Minimal Example.md +1 -1
- docs/examples/01-Basic Example.md +1 -1
- docs/examples/02-Complex Example.md +1 -1
- docs/examples/index.md +1 -1
- docs/faq/contribute.md +26 -14
- docs/faq/index.md +1 -1
- docs/javascripts/mathjax.js +1 -1
- docs/user-guide/Mathematical Notation/Bus.md +1 -1
- docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +21 -21
- docs/user-guide/Mathematical Notation/Flow.md +3 -3
- docs/user-guide/Mathematical Notation/InvestParameters.md +3 -0
- docs/user-guide/Mathematical Notation/LinearConverter.md +5 -5
- docs/user-guide/Mathematical Notation/OnOffParameters.md +3 -0
- docs/user-guide/Mathematical Notation/Piecewise.md +1 -1
- docs/user-guide/Mathematical Notation/Storage.md +2 -2
- docs/user-guide/Mathematical Notation/index.md +1 -1
- docs/user-guide/Mathematical Notation/others.md +1 -1
- docs/user-guide/index.md +2 -2
- flixopt/__init__.py +4 -0
- flixopt/aggregation.py +33 -32
- flixopt/calculation.py +161 -65
- flixopt/components.py +687 -154
- flixopt/config.py +17 -8
- flixopt/core.py +69 -60
- flixopt/effects.py +146 -64
- flixopt/elements.py +297 -110
- flixopt/features.py +78 -71
- flixopt/flow_system.py +72 -50
- flixopt/interface.py +952 -113
- flixopt/io.py +15 -10
- flixopt/linear_converters.py +373 -81
- flixopt/network_app.py +445 -266
- flixopt/plotting.py +215 -87
- flixopt/results.py +382 -209
- flixopt/solvers.py +25 -21
- flixopt/structure.py +41 -39
- flixopt/utils.py +10 -7
- {flixopt-2.1.6.dist-info → flixopt-2.1.8.dist-info}/METADATA +64 -53
- flixopt-2.1.8.dist-info/RECORD +56 -0
- scripts/extract_release_notes.py +5 -5
- scripts/gen_ref_pages.py +1 -1
- flixopt-2.1.6.dist-info/RECORD +0 -54
- {flixopt-2.1.6.dist-info → flixopt-2.1.8.dist-info}/WHEEL +0 -0
- {flixopt-2.1.6.dist-info → flixopt-2.1.8.dist-info}/licenses/LICENSE +0 -0
- {flixopt-2.1.6.dist-info → flixopt-2.1.8.dist-info}/top_level.txt +0 -0
flixopt/plotting.py
CHANGED
|
@@ -1,13 +1,34 @@
|
|
|
1
|
+
"""Comprehensive visualization toolkit for flixopt optimization results and data analysis.
|
|
2
|
+
|
|
3
|
+
This module provides a unified plotting interface supporting both Plotly (interactive)
|
|
4
|
+
and Matplotlib (static) backends for visualizing energy system optimization results.
|
|
5
|
+
It offers specialized plotting functions for time series, heatmaps, network diagrams,
|
|
6
|
+
and statistical analyses commonly needed in energy system modeling.
|
|
7
|
+
|
|
8
|
+
Key Features:
|
|
9
|
+
**Dual Backend Support**: Seamless switching between Plotly and Matplotlib
|
|
10
|
+
**Energy System Focus**: Specialized plots for power flows, storage states, emissions
|
|
11
|
+
**Color Management**: Intelligent color processing and palette management
|
|
12
|
+
**Export Capabilities**: High-quality export for reports and publications
|
|
13
|
+
**Integration Ready**: Designed for use with CalculationResults and standalone analysis
|
|
14
|
+
|
|
15
|
+
Main Plot Types:
|
|
16
|
+
- **Time Series**: Flow rates, power profiles, storage states over time
|
|
17
|
+
- **Heatmaps**: High-resolution temporal data visualization with customizable aggregation
|
|
18
|
+
- **Network Diagrams**: System topology with flow visualization
|
|
19
|
+
- **Statistical Plots**: Distribution analysis, correlation studies, performance metrics
|
|
20
|
+
- **Comparative Analysis**: Multi-scenario and sensitivity study visualizations
|
|
21
|
+
|
|
22
|
+
The module integrates seamlessly with flixopt's result classes while remaining
|
|
23
|
+
accessible for standalone data visualization tasks.
|
|
1
24
|
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
It's meant to be used in results.py, but is designed to be used by the end user as well.
|
|
5
|
-
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
6
27
|
|
|
7
28
|
import itertools
|
|
8
29
|
import logging
|
|
9
30
|
import pathlib
|
|
10
|
-
from typing import TYPE_CHECKING, Any,
|
|
31
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
11
32
|
|
|
12
33
|
import matplotlib.colors as mcolors
|
|
13
34
|
import matplotlib.pyplot as plt
|
|
@@ -33,18 +54,63 @@ _portland_colors = [
|
|
|
33
54
|
]
|
|
34
55
|
|
|
35
56
|
# Check if the colormap already exists before registering it
|
|
36
|
-
if '
|
|
37
|
-
plt.colormaps
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
if hasattr(plt, 'colormaps'): # Matplotlib >= 3.7
|
|
58
|
+
registry = plt.colormaps
|
|
59
|
+
if 'portland' not in registry:
|
|
60
|
+
registry.register(mcolors.LinearSegmentedColormap.from_list('portland', _portland_colors))
|
|
61
|
+
else: # Matplotlib < 3.7
|
|
62
|
+
if 'portland' not in [c for c in plt.colormaps()]:
|
|
63
|
+
plt.register_cmap(name='portland', cmap=mcolors.LinearSegmentedColormap.from_list('portland', _portland_colors))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
ColorType = str | list[str] | dict[str, str]
|
|
67
|
+
"""Flexible color specification type supporting multiple input formats for visualization.
|
|
68
|
+
|
|
69
|
+
Color specifications can take several forms to accommodate different use cases:
|
|
70
|
+
|
|
71
|
+
**Named Colormaps** (str):
|
|
72
|
+
- Standard colormaps: 'viridis', 'plasma', 'cividis', 'tab10', 'Set1'
|
|
73
|
+
- Energy-focused: 'portland' (custom flixopt colormap for energy systems)
|
|
74
|
+
- Backend-specific maps available in Plotly and Matplotlib
|
|
75
|
+
|
|
76
|
+
**Color Lists** (list[str]):
|
|
77
|
+
- Explicit color sequences: ['red', 'blue', 'green', 'orange']
|
|
78
|
+
- HEX codes: ['#FF0000', '#0000FF', '#00FF00', '#FFA500']
|
|
79
|
+
- Mixed formats: ['red', '#0000FF', 'green', 'orange']
|
|
80
|
+
|
|
81
|
+
**Label-to-Color Mapping** (dict[str, str]):
|
|
82
|
+
- Explicit associations: {'Wind': 'skyblue', 'Solar': 'gold', 'Gas': 'brown'}
|
|
83
|
+
- Ensures consistent colors across different plots and datasets
|
|
84
|
+
- Ideal for energy system components with semantic meaning
|
|
85
|
+
|
|
86
|
+
Examples:
|
|
87
|
+
```python
|
|
88
|
+
# Named colormap
|
|
89
|
+
colors = 'viridis' # Automatic color generation
|
|
90
|
+
|
|
91
|
+
# Explicit color list
|
|
92
|
+
colors = ['red', 'blue', 'green', '#FFD700']
|
|
93
|
+
|
|
94
|
+
# Component-specific mapping
|
|
95
|
+
colors = {
|
|
96
|
+
'Wind_Turbine': 'skyblue',
|
|
97
|
+
'Solar_Panel': 'gold',
|
|
98
|
+
'Natural_Gas': 'brown',
|
|
99
|
+
'Battery': 'green',
|
|
100
|
+
'Electric_Load': 'darkred'
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Color Format Support:
|
|
105
|
+
- **Named Colors**: 'red', 'blue', 'forestgreen', 'darkorange'
|
|
106
|
+
- **HEX Codes**: '#FF0000', '#0000FF', '#228B22', '#FF8C00'
|
|
107
|
+
- **RGB Tuples**: (255, 0, 0), (0, 0, 255) [Matplotlib only]
|
|
108
|
+
- **RGBA**: 'rgba(255,0,0,0.8)' [Plotly only]
|
|
109
|
+
|
|
110
|
+
References:
|
|
111
|
+
- HTML Color Names: https://htmlcolorcodes.com/color-names/
|
|
112
|
+
- Matplotlib Colormaps: https://matplotlib.org/stable/tutorials/colors/colormaps.html
|
|
113
|
+
- Plotly Built-in Colorscales: https://plotly.com/python/builtin-colorscales/
|
|
48
114
|
"""
|
|
49
115
|
|
|
50
116
|
PlottingEngine = Literal['plotly', 'matplotlib']
|
|
@@ -52,22 +118,74 @@ PlottingEngine = Literal['plotly', 'matplotlib']
|
|
|
52
118
|
|
|
53
119
|
|
|
54
120
|
class ColorProcessor:
|
|
55
|
-
"""
|
|
121
|
+
"""Intelligent color management system for consistent multi-backend visualization.
|
|
122
|
+
|
|
123
|
+
This class provides unified color processing across Plotly and Matplotlib backends,
|
|
124
|
+
ensuring consistent visual appearance regardless of the plotting engine used.
|
|
125
|
+
It handles color palette generation, named colormap translation, and intelligent
|
|
126
|
+
color cycling for complex datasets with many categories.
|
|
127
|
+
|
|
128
|
+
Key Features:
|
|
129
|
+
**Backend Agnostic**: Automatic color format conversion between engines
|
|
130
|
+
**Palette Management**: Support for named colormaps, custom palettes, and color lists
|
|
131
|
+
**Intelligent Cycling**: Smart color assignment for datasets with many categories
|
|
132
|
+
**Fallback Handling**: Graceful degradation when requested colormaps are unavailable
|
|
133
|
+
**Energy System Colors**: Built-in palettes optimized for energy system visualization
|
|
134
|
+
|
|
135
|
+
Color Input Types:
|
|
136
|
+
- **Named Colormaps**: 'viridis', 'plasma', 'portland', 'tab10', etc.
|
|
137
|
+
- **Color Lists**: ['red', 'blue', 'green'] or ['#FF0000', '#0000FF', '#00FF00']
|
|
138
|
+
- **Label Dictionaries**: {'Generator': 'red', 'Storage': 'blue', 'Load': 'green'}
|
|
139
|
+
|
|
140
|
+
Examples:
|
|
141
|
+
Basic color processing:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
# Initialize for Plotly backend
|
|
145
|
+
processor = ColorProcessor(engine='plotly', default_colormap='viridis')
|
|
146
|
+
|
|
147
|
+
# Process different color specifications
|
|
148
|
+
colors = processor.process_colors('plasma', ['Gen1', 'Gen2', 'Storage'])
|
|
149
|
+
colors = processor.process_colors(['red', 'blue', 'green'], ['A', 'B', 'C'])
|
|
150
|
+
colors = processor.process_colors({'Wind': 'skyblue', 'Solar': 'gold'}, ['Wind', 'Solar', 'Gas'])
|
|
151
|
+
|
|
152
|
+
# Switch to Matplotlib
|
|
153
|
+
processor = ColorProcessor(engine='matplotlib')
|
|
154
|
+
mpl_colors = processor.process_colors('tab10', component_labels)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Energy system visualization:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
# Specialized energy system palette
|
|
161
|
+
energy_colors = {
|
|
162
|
+
'Natural_Gas': '#8B4513', # Brown
|
|
163
|
+
'Electricity': '#FFD700', # Gold
|
|
164
|
+
'Heat': '#FF4500', # Red-orange
|
|
165
|
+
'Cooling': '#87CEEB', # Sky blue
|
|
166
|
+
'Hydrogen': '#E6E6FA', # Lavender
|
|
167
|
+
'Battery': '#32CD32', # Lime green
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
processor = ColorProcessor('plotly')
|
|
171
|
+
flow_colors = processor.process_colors(energy_colors, flow_labels)
|
|
172
|
+
```
|
|
56
173
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
174
|
+
Args:
|
|
175
|
+
engine: Plotting backend ('plotly' or 'matplotlib'). Determines output color format.
|
|
176
|
+
default_colormap: Fallback colormap when requested palettes are unavailable.
|
|
177
|
+
Common options: 'viridis', 'plasma', 'tab10', 'portland'.
|
|
60
178
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
"""
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
def __init__(self, engine: PlottingEngine = 'plotly', default_colormap: str = 'viridis'):
|
|
182
|
+
"""Initialize the color processor with specified backend and defaults."""
|
|
65
183
|
if engine not in ['plotly', 'matplotlib']:
|
|
66
184
|
raise TypeError(f'engine must be "plotly" or "matplotlib", but is {engine}')
|
|
67
185
|
self.engine = engine
|
|
68
186
|
self.default_colormap = default_colormap
|
|
69
187
|
|
|
70
|
-
def _generate_colors_from_colormap(self, colormap_name: str, num_colors: int) ->
|
|
188
|
+
def _generate_colors_from_colormap(self, colormap_name: str, num_colors: int) -> list[Any]:
|
|
71
189
|
"""
|
|
72
190
|
Generate colors from a named colormap.
|
|
73
191
|
|
|
@@ -76,7 +194,7 @@ class ColorProcessor:
|
|
|
76
194
|
num_colors: Number of colors to generate
|
|
77
195
|
|
|
78
196
|
Returns:
|
|
79
|
-
|
|
197
|
+
list of colors in the format appropriate for the engine
|
|
80
198
|
"""
|
|
81
199
|
if self.engine == 'plotly':
|
|
82
200
|
try:
|
|
@@ -100,16 +218,16 @@ class ColorProcessor:
|
|
|
100
218
|
|
|
101
219
|
return [cmap(i) for i in range(num_colors)]
|
|
102
220
|
|
|
103
|
-
def _handle_color_list(self, colors:
|
|
221
|
+
def _handle_color_list(self, colors: list[str], num_labels: int) -> list[str]:
|
|
104
222
|
"""
|
|
105
223
|
Handle a list of colors, cycling if necessary.
|
|
106
224
|
|
|
107
225
|
Args:
|
|
108
|
-
colors:
|
|
226
|
+
colors: list of color strings
|
|
109
227
|
num_labels: Number of labels that need colors
|
|
110
228
|
|
|
111
229
|
Returns:
|
|
112
|
-
|
|
230
|
+
list of colors matching the number of labels
|
|
113
231
|
"""
|
|
114
232
|
if len(colors) == 0:
|
|
115
233
|
logger.warning(f'Empty color list provided. Using {self.default_colormap} instead.')
|
|
@@ -130,23 +248,23 @@ class ColorProcessor:
|
|
|
130
248
|
)
|
|
131
249
|
return colors[:num_labels]
|
|
132
250
|
|
|
133
|
-
def _handle_color_dict(self, colors:
|
|
251
|
+
def _handle_color_dict(self, colors: dict[str, str], labels: list[str]) -> list[str]:
|
|
134
252
|
"""
|
|
135
253
|
Handle a dictionary mapping labels to colors.
|
|
136
254
|
|
|
137
255
|
Args:
|
|
138
256
|
colors: Dictionary mapping labels to colors
|
|
139
|
-
labels:
|
|
257
|
+
labels: list of labels that need colors
|
|
140
258
|
|
|
141
259
|
Returns:
|
|
142
|
-
|
|
260
|
+
list of colors in the same order as labels
|
|
143
261
|
"""
|
|
144
262
|
if len(colors) == 0:
|
|
145
263
|
logger.warning(f'Empty color dictionary provided. Using {self.default_colormap} instead.')
|
|
146
264
|
return self._generate_colors_from_colormap(self.default_colormap, len(labels))
|
|
147
265
|
|
|
148
266
|
# Find missing labels
|
|
149
|
-
missing_labels = set(labels) - set(colors.keys())
|
|
267
|
+
missing_labels = sorted(set(labels) - set(colors.keys()))
|
|
150
268
|
if missing_labels:
|
|
151
269
|
logger.warning(
|
|
152
270
|
f'Some labels have no color specified: {missing_labels}. Using {self.default_colormap} for these.'
|
|
@@ -168,15 +286,15 @@ class ColorProcessor:
|
|
|
168
286
|
def process_colors(
|
|
169
287
|
self,
|
|
170
288
|
colors: ColorType,
|
|
171
|
-
labels:
|
|
289
|
+
labels: list[str],
|
|
172
290
|
return_mapping: bool = False,
|
|
173
|
-
) ->
|
|
291
|
+
) -> list[Any] | dict[str, Any]:
|
|
174
292
|
"""
|
|
175
293
|
Process colors for the specified labels.
|
|
176
294
|
|
|
177
295
|
Args:
|
|
178
296
|
colors: Color specification (colormap name, list of colors, or label-to-color mapping)
|
|
179
|
-
labels:
|
|
297
|
+
labels: list of data labels that need colors assigned
|
|
180
298
|
return_mapping: If True, returns a dictionary mapping labels to colors;
|
|
181
299
|
if False, returns a list of colors in the same order as labels
|
|
182
300
|
|
|
@@ -214,7 +332,7 @@ def with_plotly(
|
|
|
214
332
|
title: str = '',
|
|
215
333
|
ylabel: str = '',
|
|
216
334
|
xlabel: str = 'Time in h',
|
|
217
|
-
fig:
|
|
335
|
+
fig: go.Figure | None = None,
|
|
218
336
|
) -> go.Figure:
|
|
219
337
|
"""
|
|
220
338
|
Plot a DataFrame with Plotly, using either stacked bars or stepped lines.
|
|
@@ -230,12 +348,14 @@ def with_plotly(
|
|
|
230
348
|
- A dictionary mapping column names to colors (e.g., {'Column1': '#ff0000'})
|
|
231
349
|
title: The title of the plot.
|
|
232
350
|
ylabel: The label for the y-axis.
|
|
351
|
+
xlabel: The label for the x-axis.
|
|
233
352
|
fig: A Plotly figure object to plot on. If not provided, a new figure will be created.
|
|
234
353
|
|
|
235
354
|
Returns:
|
|
236
355
|
A Plotly figure object containing the generated plot.
|
|
237
356
|
"""
|
|
238
|
-
|
|
357
|
+
if mode not in ('bar', 'line', 'area'):
|
|
358
|
+
raise ValueError(f"'mode' must be one of {{'bar','line','area'}}, got {mode!r}")
|
|
239
359
|
if data.empty:
|
|
240
360
|
return go.Figure()
|
|
241
361
|
|
|
@@ -350,10 +470,10 @@ def with_matplotlib(
|
|
|
350
470
|
title: str = '',
|
|
351
471
|
ylabel: str = '',
|
|
352
472
|
xlabel: str = 'Time in h',
|
|
353
|
-
figsize:
|
|
354
|
-
fig:
|
|
355
|
-
ax:
|
|
356
|
-
) ->
|
|
473
|
+
figsize: tuple[int, int] = (12, 6),
|
|
474
|
+
fig: plt.Figure | None = None,
|
|
475
|
+
ax: plt.Axes | None = None,
|
|
476
|
+
) -> tuple[plt.Figure, plt.Axes]:
|
|
357
477
|
"""
|
|
358
478
|
Plot a DataFrame with Matplotlib using stacked bars or stepped lines.
|
|
359
479
|
|
|
@@ -381,7 +501,8 @@ def with_matplotlib(
|
|
|
381
501
|
- If `mode` is 'line', stepped lines are drawn for each data series.
|
|
382
502
|
- The legend is placed below the plot to accommodate multiple data series.
|
|
383
503
|
"""
|
|
384
|
-
|
|
504
|
+
if mode not in ('bar', 'line'):
|
|
505
|
+
raise ValueError(f"'mode' must be one of {{'bar','line'}} for matplotlib, got {mode!r}")
|
|
385
506
|
|
|
386
507
|
if fig is None or ax is None:
|
|
387
508
|
fig, ax = plt.subplots(figsize=figsize)
|
|
@@ -445,8 +566,8 @@ def heat_map_matplotlib(
|
|
|
445
566
|
title: str = '',
|
|
446
567
|
xlabel: str = 'Period',
|
|
447
568
|
ylabel: str = 'Step',
|
|
448
|
-
figsize:
|
|
449
|
-
) ->
|
|
569
|
+
figsize: tuple[float, float] = (12, 6),
|
|
570
|
+
) -> tuple[plt.Figure, plt.Axes]:
|
|
450
571
|
"""
|
|
451
572
|
Plots a DataFrame as a heatmap using Matplotlib. The columns of the DataFrame will be displayed on the x-axis,
|
|
452
573
|
the index will be displayed on the y-axis, and the values will represent the 'heat' intensity in the plot.
|
|
@@ -455,6 +576,9 @@ def heat_map_matplotlib(
|
|
|
455
576
|
data: A DataFrame containing the data to be visualized. The index will be used for the y-axis, and columns will be used for the x-axis.
|
|
456
577
|
The values in the DataFrame will be represented as colors in the heatmap.
|
|
457
578
|
color_map: The colormap to use for the heatmap. Default is 'viridis'. Matplotlib supports various colormaps like 'plasma', 'inferno', 'cividis', etc.
|
|
579
|
+
title: The title of the plot.
|
|
580
|
+
xlabel: The label for the x-axis.
|
|
581
|
+
ylabel: The label for the y-axis.
|
|
458
582
|
figsize: The size of the figure to create. Default is (12, 6), which results in a width of 12 inches and a height of 6 inches.
|
|
459
583
|
|
|
460
584
|
Returns:
|
|
@@ -473,7 +597,7 @@ def heat_map_matplotlib(
|
|
|
473
597
|
|
|
474
598
|
# Create the heatmap plot
|
|
475
599
|
fig, ax = plt.subplots(figsize=figsize)
|
|
476
|
-
ax.pcolormesh(data.values, cmap=color_map)
|
|
600
|
+
ax.pcolormesh(data.values, cmap=color_map, shading='auto')
|
|
477
601
|
ax.invert_yaxis() # Flip the y-axis to start at the top
|
|
478
602
|
|
|
479
603
|
# Adjust ticks and labels for x and y axes
|
|
@@ -493,7 +617,7 @@ def heat_map_matplotlib(
|
|
|
493
617
|
|
|
494
618
|
# Add the colorbar
|
|
495
619
|
sm1 = plt.cm.ScalarMappable(cmap=color_map, norm=plt.Normalize(vmin=color_bar_min, vmax=color_bar_max))
|
|
496
|
-
sm1.
|
|
620
|
+
sm1.set_array([])
|
|
497
621
|
fig.colorbar(sm1, ax=ax, pad=0.12, aspect=15, fraction=0.2, orientation='horizontal')
|
|
498
622
|
|
|
499
623
|
fig.tight_layout()
|
|
@@ -517,11 +641,11 @@ def heat_map_plotly(
|
|
|
517
641
|
data: A DataFrame with the data to be visualized. The index will be used for the y-axis, and columns will be used for the x-axis.
|
|
518
642
|
The values in the DataFrame will be represented as colors in the heatmap.
|
|
519
643
|
color_map: The color scale to use for the heatmap. Default is 'viridis'. Plotly supports various color scales like 'Cividis', 'Inferno', etc.
|
|
644
|
+
title: The title of the heatmap. Default is an empty string.
|
|
645
|
+
xlabel: The label for the x-axis. Default is 'Period'.
|
|
646
|
+
ylabel: The label for the y-axis. Default is 'Step'.
|
|
520
647
|
categorical_labels: If True, the x and y axes are treated as categorical data (i.e., the index and columns will not be interpreted as continuous data).
|
|
521
648
|
Default is True. If False, the axes are treated as continuous, which may be useful for time series or numeric data.
|
|
522
|
-
show: Wether to show the figure after creation. (This includes saving the figure)
|
|
523
|
-
save: Wether to save the figure after creation (without showing)
|
|
524
|
-
path: Path to save the figure.
|
|
525
649
|
|
|
526
650
|
Returns:
|
|
527
651
|
A Plotly figure object containing the heatmap. This can be further customized and saved
|
|
@@ -612,12 +736,12 @@ def heat_map_data_from_df(
|
|
|
612
736
|
df: pd.DataFrame,
|
|
613
737
|
periods: Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'],
|
|
614
738
|
steps_per_period: Literal['W', 'D', 'h', '15min', 'min'],
|
|
615
|
-
fill:
|
|
739
|
+
fill: Literal['ffill', 'bfill'] | None = None,
|
|
616
740
|
) -> pd.DataFrame:
|
|
617
741
|
"""
|
|
618
742
|
Reshapes a DataFrame with a DateTime index into a 2D array for heatmap plotting,
|
|
619
743
|
based on a specified sample rate.
|
|
620
|
-
|
|
744
|
+
Only specific combinations of `periods` and `steps_per_period` are supported; invalid combinations raise an assertion.
|
|
621
745
|
|
|
622
746
|
Args:
|
|
623
747
|
df: A DataFrame with a DateTime index containing the data to reshape.
|
|
@@ -632,7 +756,7 @@ def heat_map_data_from_df(
|
|
|
632
756
|
and columns representing each period.
|
|
633
757
|
"""
|
|
634
758
|
assert pd.api.types.is_datetime64_any_dtype(df.index), (
|
|
635
|
-
'The index of the
|
|
759
|
+
'The index of the DataFrame must be datetime to transform it properly for a heatmap plot'
|
|
636
760
|
)
|
|
637
761
|
|
|
638
762
|
# Define formats for different combinations of `periods` and `steps_per_period`
|
|
@@ -645,15 +769,17 @@ def heat_map_data_from_df(
|
|
|
645
769
|
('W', 'D'): ('%Y-w%W', '%w_%A'), # week and day of week (with prefix for proper sorting)
|
|
646
770
|
('W', 'h'): ('%Y-w%W', '%w_%A %H:00'),
|
|
647
771
|
('D', 'h'): ('%Y-%m-%d', '%H:00'), # Day and hour
|
|
648
|
-
('D', '15min'): ('%Y-%m-%d', '%H:%
|
|
772
|
+
('D', '15min'): ('%Y-%m-%d', '%H:%M'), # Day and minute
|
|
649
773
|
('h', '15min'): ('%Y-%m-%d %H:00', '%M'), # minute of hour
|
|
650
774
|
('h', 'min'): ('%Y-%m-%d %H:00', '%M'), # minute of hour
|
|
651
775
|
}
|
|
652
776
|
|
|
653
|
-
|
|
777
|
+
if df.empty:
|
|
778
|
+
raise ValueError('DataFrame is empty.')
|
|
779
|
+
diffs = df.index.to_series().diff().dropna()
|
|
780
|
+
minimum_time_diff_in_min = diffs.min().total_seconds() / 60
|
|
654
781
|
time_intervals = {'min': 1, '15min': 15, 'h': 60, 'D': 24 * 60, 'W': 7 * 24 * 60}
|
|
655
782
|
if time_intervals[steps_per_period] > minimum_time_diff_in_min:
|
|
656
|
-
time_intervals[steps_per_period]
|
|
657
783
|
logger.warning(
|
|
658
784
|
f'To compute the heatmap, the data was aggregated from {minimum_time_diff_in_min:.2f} min to '
|
|
659
785
|
f'{time_intervals[steps_per_period]:.2f} min. Mean values are displayed.'
|
|
@@ -661,7 +787,8 @@ def heat_map_data_from_df(
|
|
|
661
787
|
|
|
662
788
|
# Select the format based on the `periods` and `steps_per_period` combination
|
|
663
789
|
format_pair = (periods, steps_per_period)
|
|
664
|
-
|
|
790
|
+
if format_pair not in formats:
|
|
791
|
+
raise ValueError(f'{format_pair} is not a valid format. Choose from {list(formats.keys())}')
|
|
665
792
|
period_format, step_format = formats[format_pair]
|
|
666
793
|
|
|
667
794
|
df = df.sort_index() # Ensure DataFrame is sorted by time index
|
|
@@ -675,7 +802,7 @@ def heat_map_data_from_df(
|
|
|
675
802
|
|
|
676
803
|
resampled_data['period'] = resampled_data.index.strftime(period_format)
|
|
677
804
|
resampled_data['step'] = resampled_data.index.strftime(step_format)
|
|
678
|
-
if '%w_%A' in step_format: #
|
|
805
|
+
if '%w_%A' in step_format: # Shift index of strings to ensure proper sorting
|
|
679
806
|
resampled_data['step'] = resampled_data['step'].apply(
|
|
680
807
|
lambda x: x.replace('0_Sunday', '7_Sunday') if '0_Sunday' in x else x
|
|
681
808
|
)
|
|
@@ -689,19 +816,19 @@ def heat_map_data_from_df(
|
|
|
689
816
|
def plot_network(
|
|
690
817
|
node_infos: dict,
|
|
691
818
|
edge_infos: dict,
|
|
692
|
-
path:
|
|
693
|
-
controls:
|
|
694
|
-
|
|
695
|
-
|
|
819
|
+
path: str | pathlib.Path | None = None,
|
|
820
|
+
controls: bool
|
|
821
|
+
| list[
|
|
822
|
+
Literal['nodes', 'edges', 'layout', 'interaction', 'manipulation', 'physics', 'selection', 'renderer']
|
|
696
823
|
] = True,
|
|
697
824
|
show: bool = False,
|
|
698
|
-
) ->
|
|
825
|
+
) -> pyvis.network.Network | None:
|
|
699
826
|
"""
|
|
700
827
|
Visualizes the network structure of a FlowSystem using PyVis, using info-dictionaries.
|
|
701
828
|
|
|
702
829
|
Args:
|
|
703
830
|
path: Path to save the HTML visualization. `False`: Visualization is created but not saved. `str` or `Path`: Specifies file path (default: 'results/network.html').
|
|
704
|
-
controls: UI controls to add to the visualization. `True`: Enables all available controls. `
|
|
831
|
+
controls: UI controls to add to the visualization. `True`: Enables all available controls. `list`: Specify controls, e.g., ['nodes', 'layout'].
|
|
705
832
|
Options: 'nodes', 'edges', 'layout', 'interaction', 'manipulation', 'physics', 'selection', 'renderer'.
|
|
706
833
|
You can play with these and generate a Dictionary from it that can be applied to the network returned by this function.
|
|
707
834
|
network.set_options()
|
|
@@ -778,7 +905,7 @@ def pie_with_plotly(
|
|
|
778
905
|
title: str = '',
|
|
779
906
|
legend_title: str = '',
|
|
780
907
|
hole: float = 0.0,
|
|
781
|
-
fig:
|
|
908
|
+
fig: go.Figure | None = None,
|
|
782
909
|
) -> go.Figure:
|
|
783
910
|
"""
|
|
784
911
|
Create a pie chart with Plotly to visualize the proportion of values in a DataFrame.
|
|
@@ -828,7 +955,7 @@ def pie_with_plotly(
|
|
|
828
955
|
values = data_sum.values.tolist()
|
|
829
956
|
|
|
830
957
|
# Apply color mapping using the unified color processor
|
|
831
|
-
processed_colors = ColorProcessor(engine='plotly').process_colors(colors,
|
|
958
|
+
processed_colors = ColorProcessor(engine='plotly').process_colors(colors, labels)
|
|
832
959
|
|
|
833
960
|
# Create figure if not provided
|
|
834
961
|
fig = fig if fig is not None else go.Figure()
|
|
@@ -864,10 +991,10 @@ def pie_with_matplotlib(
|
|
|
864
991
|
title: str = '',
|
|
865
992
|
legend_title: str = 'Categories',
|
|
866
993
|
hole: float = 0.0,
|
|
867
|
-
figsize:
|
|
868
|
-
fig:
|
|
869
|
-
ax:
|
|
870
|
-
) ->
|
|
994
|
+
figsize: tuple[int, int] = (10, 8),
|
|
995
|
+
fig: plt.Figure | None = None,
|
|
996
|
+
ax: plt.Axes | None = None,
|
|
997
|
+
) -> tuple[plt.Figure, plt.Axes]:
|
|
871
998
|
"""
|
|
872
999
|
Create a pie chart with Matplotlib to visualize the proportion of values in a DataFrame.
|
|
873
1000
|
|
|
@@ -976,7 +1103,7 @@ def dual_pie_with_plotly(
|
|
|
976
1103
|
data_right: pd.Series,
|
|
977
1104
|
colors: ColorType = 'viridis',
|
|
978
1105
|
title: str = '',
|
|
979
|
-
subtitles:
|
|
1106
|
+
subtitles: tuple[str, str] = ('Left Chart', 'Right Chart'),
|
|
980
1107
|
legend_title: str = '',
|
|
981
1108
|
hole: float = 0.2,
|
|
982
1109
|
lower_percentage_group: float = 5.0,
|
|
@@ -997,8 +1124,8 @@ def dual_pie_with_plotly(
|
|
|
997
1124
|
title: The main title of the plot.
|
|
998
1125
|
subtitles: Tuple containing the subtitles for (left, right) charts.
|
|
999
1126
|
legend_title: The title for the legend.
|
|
1000
|
-
hole: Size of the hole in the center for creating donut charts (0.0 to
|
|
1001
|
-
lower_percentage_group:
|
|
1127
|
+
hole: Size of the hole in the center for creating donut charts (0.0 to 1.0).
|
|
1128
|
+
lower_percentage_group: Group segments whose cumulative share is below this percentage (0–100) into "Other".
|
|
1002
1129
|
hover_template: Template for hover text. Use %{label}, %{value}, %{percent}.
|
|
1003
1130
|
text_info: What to show on pie segments: 'label', 'percent', 'value', 'label+percent',
|
|
1004
1131
|
'label+value', 'percent+value', 'label+percent+value', or 'none'.
|
|
@@ -1090,7 +1217,7 @@ def dual_pie_with_plotly(
|
|
|
1090
1217
|
labels=labels,
|
|
1091
1218
|
values=values,
|
|
1092
1219
|
name=side,
|
|
1093
|
-
|
|
1220
|
+
marker=dict(colors=trace_colors),
|
|
1094
1221
|
hole=hole,
|
|
1095
1222
|
textinfo=text_info,
|
|
1096
1223
|
textposition=text_position,
|
|
@@ -1130,14 +1257,14 @@ def dual_pie_with_matplotlib(
|
|
|
1130
1257
|
data_right: pd.Series,
|
|
1131
1258
|
colors: ColorType = 'viridis',
|
|
1132
1259
|
title: str = '',
|
|
1133
|
-
subtitles:
|
|
1260
|
+
subtitles: tuple[str, str] = ('Left Chart', 'Right Chart'),
|
|
1134
1261
|
legend_title: str = '',
|
|
1135
1262
|
hole: float = 0.2,
|
|
1136
1263
|
lower_percentage_group: float = 5.0,
|
|
1137
|
-
figsize:
|
|
1138
|
-
fig:
|
|
1139
|
-
axes:
|
|
1140
|
-
) ->
|
|
1264
|
+
figsize: tuple[int, int] = (14, 7),
|
|
1265
|
+
fig: plt.Figure | None = None,
|
|
1266
|
+
axes: list[plt.Axes] | None = None,
|
|
1267
|
+
) -> tuple[plt.Figure, list[plt.Axes]]:
|
|
1141
1268
|
"""
|
|
1142
1269
|
Create two pie charts side by side with Matplotlib, with consistent coloring across both charts.
|
|
1143
1270
|
Leverages the existing pie_with_matplotlib function.
|
|
@@ -1288,13 +1415,13 @@ def dual_pie_with_matplotlib(
|
|
|
1288
1415
|
|
|
1289
1416
|
|
|
1290
1417
|
def export_figure(
|
|
1291
|
-
figure_like:
|
|
1418
|
+
figure_like: go.Figure | tuple[plt.Figure, plt.Axes],
|
|
1292
1419
|
default_path: pathlib.Path,
|
|
1293
|
-
default_filetype:
|
|
1294
|
-
user_path:
|
|
1420
|
+
default_filetype: str | None = None,
|
|
1421
|
+
user_path: pathlib.Path | None = None,
|
|
1295
1422
|
show: bool = True,
|
|
1296
1423
|
save: bool = False,
|
|
1297
|
-
) ->
|
|
1424
|
+
) -> go.Figure | tuple[plt.Figure, plt.Axes]:
|
|
1298
1425
|
"""
|
|
1299
1426
|
Export a figure to a file and or show it.
|
|
1300
1427
|
|
|
@@ -1319,14 +1446,15 @@ def export_figure(
|
|
|
1319
1446
|
|
|
1320
1447
|
if isinstance(figure_like, plotly.graph_objs.Figure):
|
|
1321
1448
|
fig = figure_like
|
|
1322
|
-
if
|
|
1323
|
-
logger.
|
|
1449
|
+
if filename.suffix != '.html':
|
|
1450
|
+
logger.warning(f'To save a Plotly figure, using .html. Adjusting suffix for {filename}')
|
|
1451
|
+
filename = filename.with_suffix('.html')
|
|
1324
1452
|
if show and not save:
|
|
1325
1453
|
fig.show()
|
|
1326
1454
|
elif save and show:
|
|
1327
1455
|
plotly.offline.plot(fig, filename=str(filename))
|
|
1328
1456
|
elif save and not show:
|
|
1329
|
-
fig.write_html(filename)
|
|
1457
|
+
fig.write_html(str(filename))
|
|
1330
1458
|
return figure_like
|
|
1331
1459
|
|
|
1332
1460
|
elif isinstance(figure_like, tuple):
|