Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 17 additions & 13 deletions flixopt/clustering.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import copy
import logging
import pathlib
import timeit
from typing import TYPE_CHECKING

Expand All @@ -29,6 +28,8 @@
)

if TYPE_CHECKING:
import pathlib
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard Clustering.plot against unset aggregated_data and consider more robust color mapping

Clustering.__init__ sets self.aggregated_data = None, and cluster() is the only method that populates it. In plot(), you call self.aggregated_data.copy() unconditionally, so calling plot() before cluster() will fail with an AttributeError instead of a clear error message.

You can make this failure mode explicit and user-friendly:

     def plot(self, colormap: str | None = None, show: bool = True, save: pathlib.Path | None = None) -> go.Figure:
-        import plotly.express as px
+        import plotly.express as px
+
+        if self.aggregated_data is None:
+            raise RuntimeError("Clustering.plot() requires aggregated data. Call cluster() before plot().")

Separately, you derive colors via:

colors = list(
    process_colors(colormap or CONFIG.Plotting.default_qualitative_colorscale, list(df_org.columns)).values()
)

and pass that as color_discrete_sequence to both the original and aggregated plots. This assumes the aggregated dataframe has the same column order as the original to keep color pairing between “Original - X” and “Aggregated - X”, which is a bit fragile.

If you want stronger guarantees that each original/aggregated pair shares a color regardless of column ordering, consider building a color_discrete_map keyed by the full series names (or by a shared base label) and passing that into px.line instead of relying solely on sequence position.

Also applies to: 148-183

🤖 Prompt for AI Agents
In flixopt/clustering.py around lines 31 and 148-183, plot() currently calls
self.aggregated_data.copy() unguarded and derives colors as a sequence which
assumes identical column ordering; first add an explicit guard at the top of
plot() that checks if self.aggregated_data is None and raise a clear ValueError
(or return) telling the user to call cluster() first; second, replace the
positional color_discrete_sequence approach with a deterministic
color_discrete_map: generate a mapping from the actual full series names used in
the plot (e.g., original column names and their corresponding aggregated labels
like "Aggregated - X") to colors produced by process_colors (using the provided
colormap or default), and pass this map into px.line via color_discrete_map so
each original/aggregated pair always shares the same color regardless of column
ordering.


import linopy
import pandas as pd
import plotly.graph_objects as go
Expand Down Expand Up @@ -145,7 +146,7 @@ def use_extreme_periods(self):
return self.time_series_for_high_peaks or self.time_series_for_low_peaks

def plot(self, colormap: str | None = None, show: bool = True, save: pathlib.Path | None = None) -> go.Figure:
from . import plotting
import plotly.express as px

df_org = self.original_data.copy().rename(
columns={col: f'Original - {col}' for col in self.original_data.columns}
Expand All @@ -156,10 +157,17 @@ def plot(self, colormap: str | None = None, show: bool = True, save: pathlib.Pat
colors = list(
process_colors(colormap or CONFIG.Plotting.default_qualitative_colorscale, list(df_org.columns)).values()
)
fig = plotting.with_plotly(df_org.to_xarray(), 'line', colors=colors, xlabel='Time in h')

# Create line plot for original data (dashed)
index_name = df_org.index.name or 'index'
df_org_long = df_org.reset_index().melt(id_vars=index_name, var_name='variable', value_name='value')
fig = px.line(df_org_long, x=index_name, y='value', color='variable', color_discrete_sequence=colors)
for trace in fig.data:
trace.update(dict(line=dict(dash='dash')))
fig2 = plotting.with_plotly(df_agg.to_xarray(), 'line', colors=colors, xlabel='Time in h')
trace.update(line=dict(dash='dash'))

# Add aggregated data (solid lines)
df_agg_long = df_agg.reset_index().melt(id_vars=index_name, var_name='variable', value_name='value')
fig2 = px.line(df_agg_long, x=index_name, y='value', color='variable', color_discrete_sequence=colors)
for trace in fig2.data:
fig.add_trace(trace)

Expand All @@ -169,14 +177,10 @@ def plot(self, colormap: str | None = None, show: bool = True, save: pathlib.Pat
yaxis_title='Value',
)

plotting.export_figure(
figure_like=fig,
default_path=pathlib.Path('aggregated data.html'),
default_filetype='.html',
user_path=save,
show=show,
save=save is not None,
)
if save is not None:
fig.write_html(str(save))
if show:
fig.show()

return fig

Expand Down
51 changes: 51 additions & 0 deletions flixopt/color_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,57 @@

logger = logging.getLogger('flixopt')

# Type alias for flexible color input
ColorType = str | list[str] | dict[str, str]
"""Flexible color specification type supporting multiple input formats for visualization.

Color specifications can take several forms to accommodate different use cases:

**Named colorscales** (str):
- Standard colorscales: 'turbo', 'plasma', 'cividis', 'tab10', 'Set1'
- Energy-focused: 'portland' (custom flixopt colorscale for energy systems)
- Backend-specific maps available in Plotly and Matplotlib

**Color Lists** (list[str]):
- Explicit color sequences: ['red', 'blue', 'green', 'orange']
- HEX codes: ['#FF0000', '#0000FF', '#00FF00', '#FFA500']
- Mixed formats: ['red', '#0000FF', 'green', 'orange']

**Label-to-Color Mapping** (dict[str, str]):
- Explicit associations: {'Wind': 'skyblue', 'Solar': 'gold', 'Gas': 'brown'}
- Ensures consistent colors across different plots and datasets
- Ideal for energy system components with semantic meaning

Examples:
```python
# Named colorscale
colors = 'turbo' # Automatic color generation

# Explicit color list
colors = ['red', 'blue', 'green', '#FFD700']

# Component-specific mapping
colors = {
'Wind_Turbine': 'skyblue',
'Solar_Panel': 'gold',
'Natural_Gas': 'brown',
'Battery': 'green',
'Electric_Load': 'darkred'
}
```

Color Format Support:
- **Named Colors**: 'red', 'blue', 'forestgreen', 'darkorange'
- **HEX Codes**: '#FF0000', '#0000FF', '#228B22', '#FF8C00'
- **RGB Tuples**: (255, 0, 0), (0, 0, 255) [Matplotlib only]
- **RGBA**: 'rgba(255,0,0,0.8)' [Plotly only]

References:
- HTML Color Names: https://htmlcolorcodes.com/color-names/
- Matplotlib colorscales: https://matplotlib.org/stable/tutorials/colors/colorscales.html
- Plotly Built-in Colorscales: https://plotly.com/python/builtin-colorscales/
"""


def _rgb_string_to_hex(color: str) -> str:
"""Convert Plotly RGB/RGBA string format to hex.
Expand Down
52 changes: 1 addition & 51 deletions flixopt/plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
import plotly.offline
import xarray as xr

from .color_processing import process_colors
from .color_processing import ColorType, process_colors
from .config import CONFIG

if TYPE_CHECKING:
Expand All @@ -66,56 +66,6 @@
plt.register_cmap(name='portland', cmap=mcolors.LinearSegmentedColormap.from_list('portland', _portland_colors))


ColorType = str | list[str] | dict[str, str]
"""Flexible color specification type supporting multiple input formats for visualization.

Color specifications can take several forms to accommodate different use cases:

**Named colorscales** (str):
- Standard colorscales: 'turbo', 'plasma', 'cividis', 'tab10', 'Set1'
- Energy-focused: 'portland' (custom flixopt colorscale for energy systems)
- Backend-specific maps available in Plotly and Matplotlib

**Color Lists** (list[str]):
- Explicit color sequences: ['red', 'blue', 'green', 'orange']
- HEX codes: ['#FF0000', '#0000FF', '#00FF00', '#FFA500']
- Mixed formats: ['red', '#0000FF', 'green', 'orange']

**Label-to-Color Mapping** (dict[str, str]):
- Explicit associations: {'Wind': 'skyblue', 'Solar': 'gold', 'Gas': 'brown'}
- Ensures consistent colors across different plots and datasets
- Ideal for energy system components with semantic meaning

Examples:
```python
# Named colorscale
colors = 'turbo' # Automatic color generation

# Explicit color list
colors = ['red', 'blue', 'green', '#FFD700']

# Component-specific mapping
colors = {
'Wind_Turbine': 'skyblue',
'Solar_Panel': 'gold',
'Natural_Gas': 'brown',
'Battery': 'green',
'Electric_Load': 'darkred'
}
```

Color Format Support:
- **Named Colors**: 'red', 'blue', 'forestgreen', 'darkorange'
- **HEX Codes**: '#FF0000', '#0000FF', '#228B22', '#FF8C00'
- **RGB Tuples**: (255, 0, 0), (0, 0, 255) [Matplotlib only]
- **RGBA**: 'rgba(255,0,0,0.8)' [Plotly only]

References:
- HTML Color Names: https://htmlcolorcodes.com/color-names/
- Matplotlib colorscales: https://matplotlib.org/stable/tutorials/colors/colorscales.html
- Plotly Built-in Colorscales: https://plotly.com/python/builtin-colorscales/
"""

PlottingEngine = Literal['plotly', 'matplotlib']
"""Identifier for the plotting engine to use."""

Expand Down
Loading