Skip to content

Commit 379fe95

Browse files
committed
Changes made:
1. statistics_accessor.py: - Added add_secondary_y to imports from xarray_plotly.figures - Replaced add_line_overlay call in storage() method with add_secondary_y - Removed the 94-line add_line_overlay function entirely 2. comparison.py: - Added import for add_secondary_y from xarray_plotly.figures - Removed add_line_overlay from imports - Replaced add_line_overlay call in storage() method with add_secondary_y Why this works better: The xarray_plotly.figures.add_secondary_y function: - Validates that both figures have the same facet structure - Correctly maps traces to secondary y-axes based on actual axis assignments (not trace index) - Properly handles animation frames - Uses proper axis numbering that doesn't conflict with existing axes The old add_line_overlay was broken because it assumed trace index i corresponded to subplot i+1, which isn't how Plotly Express generates faceted plots.
1 parent 8921fd4 commit 379fe95

2 files changed

Lines changed: 21 additions & 120 deletions

File tree

flixopt/comparison.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77

88
import xarray as xr
99
from xarray_plotly import SLOT_ORDERS
10+
from xarray_plotly.figures import add_secondary_y
1011

1112
from .config import CONFIG
1213
from .plot_result import PlotResult
1314
from .statistics_accessor import (
1415
ColorType,
1516
SelectType,
1617
_build_color_kwargs,
17-
add_line_overlay,
1818
)
1919

2020
if TYPE_CHECKING:
@@ -615,19 +615,16 @@ def storage(
615615

616616
# Add charge state as line overlay on secondary y-axis
617617
if 'charge_state' in ds:
618-
# Only pass faceting kwargs that add_line_overlay accepts
619-
overlay_kwargs = {
618+
# Build line figure with same faceting kwargs, colored by case
619+
line_kwargs = {
620620
k: v for k, v in plotly_kwargs.items() if k in ('x', 'facet_col', 'facet_row', 'animation_frame')
621621
}
622-
add_line_overlay(
623-
fig,
624-
ds['charge_state'],
625-
color='case',
626-
name='charge_state',
627-
secondary_y=True,
628-
y_title='Charge State',
629-
**overlay_kwargs,
630-
)
622+
line_fig = ds['charge_state'].plotly.line(color='case', **line_kwargs)
623+
# Update legend group for charge_state traces
624+
for trace in line_fig.data:
625+
trace.legendgroup = f'charge_state_{trace.name}'
626+
# Combine using xarray_plotly's add_secondary_y which handles facets correctly
627+
fig = add_secondary_y(fig, line_fig, secondary_y_title='Charge State')
631628

632629
return self._finalize(ds, fig, show)
633630

flixopt/statistics_accessor.py

Lines changed: 12 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
import pandas as pd
2828
import plotly.graph_objects as go
2929
import xarray as xr
30-
from xarray_plotly.figures import update_traces
30+
from xarray_plotly.figures import add_secondary_y, update_traces
3131

3232
from .color_processing import ColorType, hex_to_rgba, process_colors
3333
from .config import CONFIG
@@ -298,102 +298,6 @@ def _filter_small_variables(ds: xr.Dataset, threshold: float | None) -> xr.Datas
298298
return ds[keep] if keep else ds
299299

300300

301-
def add_line_overlay(
302-
fig: go.Figure,
303-
da: xr.DataArray,
304-
*,
305-
x: str | None = None,
306-
facet_col: str | None = None,
307-
facet_row: str | None = None,
308-
animation_frame: str | None = None,
309-
color: str | None = None,
310-
line_color: str = 'black',
311-
name: str | None = None,
312-
secondary_y: bool = False,
313-
y_title: str | None = None,
314-
showlegend: bool = True,
315-
) -> None:
316-
"""Add line traces on top of existing figure, optionally on secondary y-axis.
317-
318-
This function creates line traces from a DataArray and adds them to an existing
319-
figure. When using secondary_y=True, it correctly handles faceted figures by
320-
creating matching secondary axes for each primary axis.
321-
322-
Args:
323-
fig: Plotly figure to add traces to.
324-
da: DataArray to plot as lines.
325-
x: Dimension to use for x-axis. If None, auto-detects 'time' or first dim.
326-
facet_col: Dimension for column facets (must match primary figure).
327-
facet_row: Dimension for row facets (must match primary figure).
328-
animation_frame: Dimension for animation slider (must match primary figure).
329-
color: Dimension to color by (creates multiple lines).
330-
line_color: Color for lines when color is None.
331-
name: Legend name for the traces.
332-
secondary_y: If True, plot on secondary y-axis.
333-
y_title: Title for the y-axis (secondary if secondary_y=True).
334-
showlegend: Whether to show legend entries.
335-
"""
336-
if da.size == 0:
337-
return
338-
339-
# Auto-detect x dimension if not specified
340-
if x is None:
341-
x = 'time' if 'time' in da.dims else da.dims[0]
342-
343-
# Build kwargs for line plot, only passing facet params if specified
344-
line_kwargs: dict[str, Any] = {'x': x}
345-
if color is not None:
346-
line_kwargs['color'] = color
347-
if facet_col is not None:
348-
line_kwargs['facet_col'] = facet_col
349-
if facet_row is not None:
350-
line_kwargs['facet_row'] = facet_row
351-
if animation_frame is not None:
352-
line_kwargs['animation_frame'] = animation_frame
353-
354-
# Create line figure with same facets
355-
line_fig = da.plotly.line(**line_kwargs)
356-
357-
if secondary_y:
358-
# Get the primary y-axes from the bar figure to create matching secondary axes
359-
primary_yaxes = [key for key in fig.layout if key.startswith('yaxis')]
360-
361-
# For each primary y-axis, create a secondary y-axis.
362-
# Secondary axis numbering strategy:
363-
# - Primary axes are named 'yaxis', 'yaxis2', 'yaxis3', etc.
364-
# - We use +100 offset (yaxis101, yaxis102, ...) to avoid conflicts
365-
# - Each secondary axis 'overlays' its corresponding primary axis
366-
for i, primary_key in enumerate(sorted(primary_yaxes, key=lambda x: int(x[5:]) if x[5:] else 0)):
367-
primary_num = primary_key[5:] if primary_key[5:] else '1'
368-
secondary_num = int(primary_num) + 100
369-
secondary_key = f'yaxis{secondary_num}'
370-
secondary_anchor = f'x{primary_num}' if primary_num != '1' else 'x'
371-
372-
fig.layout[secondary_key] = dict(
373-
overlaying=f'y{primary_num}' if primary_num != '1' else 'y',
374-
side='right',
375-
showgrid=False,
376-
title=y_title if i == len(primary_yaxes) - 1 else None,
377-
anchor=secondary_anchor,
378-
)
379-
380-
# Add line traces with correct axis assignments
381-
for i, trace in enumerate(line_fig.data):
382-
if name is not None:
383-
trace.name = name
384-
if color is None:
385-
trace.line = dict(color=line_color, width=2)
386-
387-
if secondary_y:
388-
primary_num = i + 1 if i > 0 else 1
389-
trace.yaxis = f'y{primary_num + 100}'
390-
391-
trace.showlegend = showlegend and (i == 0)
392-
if name is not None:
393-
trace.legendgroup = name
394-
fig.add_trace(trace)
395-
396-
397301
def _filter_by_carrier(ds: xr.Dataset, carrier: str | list[str] | None) -> xr.Dataset:
398302
"""Filter dataset variables by carrier attribute.
399303
@@ -2489,19 +2393,19 @@ def storage(
24892393
_apply_unified_hover(fig, unit=unit_label)
24902394

24912395
# Add charge state as line on secondary y-axis
2492-
# Only pass faceting kwargs that add_line_overlay accepts
2493-
overlay_kwargs = {
2396+
# Build line figure with same faceting kwargs
2397+
line_kwargs = {
24942398
k: v for k, v in plotly_kwargs.items() if k in ('x', 'facet_col', 'facet_row', 'animation_frame')
24952399
}
2496-
add_line_overlay(
2497-
fig,
2498-
charge_da,
2499-
line_color=charge_state_color,
2500-
name='charge_state',
2501-
secondary_y=True,
2502-
y_title='Charge State',
2503-
**overlay_kwargs,
2504-
)
2400+
line_fig = charge_da.plotly.line(color=None, **line_kwargs)
2401+
# Style the line traces
2402+
for trace in line_fig.data:
2403+
trace.name = 'charge_state'
2404+
trace.line = dict(color=charge_state_color, width=2)
2405+
trace.showlegend = trace == line_fig.data[0] # Only first trace in legend
2406+
trace.legendgroup = 'charge_state'
2407+
# Combine using xarray_plotly's add_secondary_y which handles facets correctly
2408+
fig = add_secondary_y(fig, line_fig, secondary_y_title='Charge State')
25052409

25062410
if show is None:
25072411
show = CONFIG.Plotting.default_show

0 commit comments

Comments
 (0)