flixopt 2.1.7__py3-none-any.whl → 2.1.9__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/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
- This module contains the plotting functionality of the flixopt framework.
3
- It provides high level functions to plot data with plotly and matplotlib.
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, Dict, List, Literal, Optional, Tuple, Union
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 'portland' not in plt.colormaps:
37
- plt.colormaps.register(mcolors.LinearSegmentedColormap.from_list('portland', _portland_colors))
38
-
39
-
40
- ColorType = Union[str, List[str], Dict[str, str]]
41
- """Identifier for the colors to use.
42
- Use the name of a colorscale, a list of colors or a dictionary of labels to colors.
43
- The colors must be valid color strings (HEX or names). Depending on the Engine used, other formats are possible.
44
- See also:
45
- - https://htmlcolorcodes.com/color-names/
46
- - https://matplotlib.org/stable/tutorials/colors/colormaps.html
47
- - https://plotly.com/python/builtin-colorscales/
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
- """Class to handle color processing for different visualization engines."""
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
- def __init__(self, engine: PlottingEngine = 'plotly', default_colormap: str = 'viridis'):
58
- """
59
- Initialize the color processor.
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
- Args:
62
- engine: The plotting engine to use ('plotly' or 'matplotlib')
63
- default_colormap: Default colormap to use if none is specified
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) -> List[Any]:
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
- List of colors in the format appropriate for the engine
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: List[str], num_labels: int) -> List[str]:
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: List of color strings
226
+ colors: list of color strings
109
227
  num_labels: Number of labels that need colors
110
228
 
111
229
  Returns:
112
- List of colors matching the number of labels
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: Dict[str, str], labels: List[str]) -> List[str]:
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: List of labels that need colors
257
+ labels: list of labels that need colors
140
258
 
141
259
  Returns:
142
- List of colors in the same order as labels
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: List[str],
289
+ labels: list[str],
172
290
  return_mapping: bool = False,
173
- ) -> Union[List[Any], Dict[str, Any]]:
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: List of data labels that need colors assigned
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: Optional[go.Figure] = None,
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
- assert mode in ['bar', 'line', 'area'], f"'mode' must be one of {['bar', 'line', 'area']}"
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: Tuple[int, int] = (12, 6),
354
- fig: Optional[plt.Figure] = None,
355
- ax: Optional[plt.Axes] = None,
356
- ) -> Tuple[plt.Figure, plt.Axes]:
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
- assert mode in ['bar', 'line'], f"'mode' must be one of {['bar', 'line']} for matplotlib"
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: Tuple[float, float] = (12, 6),
449
- ) -> Tuple[plt.Figure, plt.Axes]:
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._A = []
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: Optional[Literal['ffill', 'bfill']] = None,
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
- If a non-valid combination of periods and steps per period is used, falls back to numerical indices
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 Dataframe must be datetime to transfrom it properly for a heatmap plot'
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:%MM'), # Day and hour
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
- minimum_time_diff_in_min = df.index.to_series().diff().min().total_seconds() / 60 # Smallest time_diff in minutes
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
- assert format_pair in formats, f'{format_pair} is not a valid format. Choose from {list(formats.keys())}'
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: # SHift index of strings to ensure proper sorting
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: Optional[Union[str, pathlib.Path]] = None,
693
- controls: Union[
694
- bool,
695
- List[Literal['nodes', 'edges', 'layout', 'interaction', 'manipulation', 'physics', 'selection', 'renderer']],
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
- ) -> Optional['pyvis.network.Network']:
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. `List`: Specify controls, e.g., ['nodes', 'layout'].
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: Optional[go.Figure] = None,
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, list(data.columns))
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: Tuple[int, int] = (10, 8),
868
- fig: Optional[plt.Figure] = None,
869
- ax: Optional[plt.Axes] = None,
870
- ) -> Tuple[plt.Figure, plt.Axes]:
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: Tuple[str, str] = ('Left Chart', 'Right Chart'),
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 100).
1001
- lower_percentage_group: Whether to group small segments (below percentage (0...1)) into an "Other" category.
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
- marker_colors=trace_colors,
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: Tuple[str, str] = ('Left Chart', 'Right Chart'),
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: Tuple[int, int] = (14, 7),
1138
- fig: Optional[plt.Figure] = None,
1139
- axes: Optional[List[plt.Axes]] = None,
1140
- ) -> Tuple[plt.Figure, List[plt.Axes]]:
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: Union[plotly.graph_objs.Figure, Tuple[plt.Figure, plt.Axes]],
1418
+ figure_like: go.Figure | tuple[plt.Figure, plt.Axes],
1292
1419
  default_path: pathlib.Path,
1293
- default_filetype: Optional[str] = None,
1294
- user_path: Optional[pathlib.Path] = None,
1420
+ default_filetype: str | None = None,
1421
+ user_path: pathlib.Path | None = None,
1295
1422
  show: bool = True,
1296
1423
  save: bool = False,
1297
- ) -> Union[plotly.graph_objs.Figure, Tuple[plt.Figure, plt.Axes]]:
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 not filename.suffix == '.html':
1323
- logger.debug(f'To save a plotly figure, the filename should end with ".html". Got {filename}')
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):