Skip to content

Commit 4f31aaf

Browse files
authored
Feature/speed up plotting (#586)
* _style_area_as_bar() helper: - Classifies traces by analyzing y-values: positive, negative, mixed, zero - Sets stackgroup='positive' or stackgroup='negative' for proper separate stacking - Mixed values shown as dashed lines (no fill) - Opaque fills, no line borders, hv line shape Performance: ┌────────────────────────┬───────┐ │ Method │ Time │ ├────────────────────────┼───────┤ │ .plotly.bar() + update │ 0.14s │ ├────────────────────────┼───────┤ │ .plotly.area() + style │ 0.10s │ ├────────────────────────┼───────┤ │ Speedup │ ~1.4x │ └────────────────────────┴───────┘ * New Helper Functions # Iterate over all traces (main + animation frames) def _iter_all_traces(fig: go.Figure): yield from fig.data for frame in getattr(fig, 'frames', []) or []: yield from frame.data # Apply unified hover styling (works with any plot type) def _apply_unified_hover(fig: go.Figure, unit: str = '', decimals: int = 1): # Sets: <b>name</b>: value unit # + hovermode='x unified' + spike lines Updated Methods ┌───────────────────┬──────────────────────────────────────────────┐ │ Method │ Changes │ ├───────────────────┼──────────────────────────────────────────────┤ │ balance() │ + _apply_unified_hover(fig, unit=unit_label) │ ├───────────────────┼──────────────────────────────────────────────┤ │ carrier_balance() │ + _apply_unified_hover(fig, unit=unit_label) │ ├───────────────────┼──────────────────────────────────────────────┤ │ storage() │ + _apply_unified_hover(fig, unit=unit_label) │ └───────────────────┴──────────────────────────────────────────────┘ Result - Hover format: <b>Solar</b>: 45.3 kW - Hovermode: x unified (single tooltip for all traces) - Spikes: Gray vertical line at cursor * 1. _style_area_as_bar (lines 183-221): The class_map is now built by aggregating sign info across all traces returned by _iter_all_traces(fig), including animation frames. The color_map is still derived from fig.data. The implementation uses a sign_flags dictionary to incrementally update has_pos/has_neg flags for each trace.name, then computes class_map from those aggregated flags. 2. _apply_unified_hover (lines 271-274): Replaced the fig.update_layout(xaxis_showspikes=..., ...) with a single fig.update_xaxes(showspikes=True, spikecolor='gray', spikethickness=1) call so spike settings apply to all x-axes (xaxis, xaxis2, xaxis3, ...) in faceted plots. * Update plotting deps * Update plotting deps and use xarray-plotly for fast_bar * Removed both redundant line_shape='hv' parameters - fast_bar() handles that internally * Update CHANGELOG.md
1 parent b39ab03 commit 4f31aaf

3 files changed

Lines changed: 8 additions & 115 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ fs.transform.cluster(
301301

302302
- `FlowSystem.weights` returns `dict[str, xr.DataArray]` (unit weights instead of `1.0` float fallback)
303303
- `FlowSystemDimensions` type now includes `'cluster'`
304+
- `statistics.plot.balance()`, `carrier_balance()`, and `storage()` now use `xarray_plotly.fast_bar()` internally (styled stacked areas for better performance)
304305

305306
### 🗑️ Deprecated
306307

flixopt/statistics_accessor.py

Lines changed: 6 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +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
3031

3132
from .color_processing import ColorType, hex_to_rgba, process_colors
3233
from .config import CONFIG
@@ -146,108 +147,6 @@ def _reshape_time_for_heatmap(
146147
return result.transpose('timestep', 'timeframe', *other_dims)
147148

148149

149-
def _iter_all_traces(fig: go.Figure):
150-
"""Iterate over all traces in a figure, including animation frames.
151-
152-
Yields traces from fig.data first, then from each frame in fig.frames.
153-
Useful for applying styling to all traces including those in animations.
154-
155-
Args:
156-
fig: Plotly Figure.
157-
158-
Yields:
159-
Each trace object from the figure.
160-
"""
161-
yield from fig.data
162-
for frame in getattr(fig, 'frames', []) or []:
163-
yield from frame.data
164-
165-
166-
def _style_area_as_bar(fig: go.Figure) -> None:
167-
"""Style area chart traces to look like bar charts with proper pos/neg stacking.
168-
169-
Iterates over all traces in fig.data and fig.frames (for animations),
170-
setting stepped line shape, removing line borders, making fills opaque,
171-
and assigning stackgroups based on whether values are positive or negative.
172-
173-
Handles faceting + animation combinations by building color and classification
174-
maps from trace names in the base figure.
175-
176-
Args:
177-
fig: Plotly Figure with area chart traces.
178-
"""
179-
import plotly.express as px
180-
181-
default_colors = px.colors.qualitative.Plotly
182-
183-
# Build color map from base figure traces
184-
# trace.name -> color
185-
color_map: dict[str, str] = {}
186-
for i, trace in enumerate(fig.data):
187-
if hasattr(trace, 'line') and trace.line and trace.line.color:
188-
color_map[trace.name] = trace.line.color
189-
else:
190-
color_map[trace.name] = default_colors[i % len(default_colors)]
191-
192-
# Classify traces by aggregating sign info across ALL traces (including animation frames)
193-
# trace.name -> 'positive'|'negative'|'mixed'|'zero'
194-
class_map: dict[str, str] = {}
195-
sign_flags: dict[str, dict[str, bool]] = {} # trace.name -> {'has_pos': bool, 'has_neg': bool}
196-
197-
for trace in _iter_all_traces(fig):
198-
if trace.name not in sign_flags:
199-
sign_flags[trace.name] = {'has_pos': False, 'has_neg': False}
200-
201-
y_vals = trace.y
202-
if y_vals is not None and len(y_vals) > 0:
203-
y_arr = np.asarray(y_vals)
204-
y_clean = y_arr[np.abs(y_arr) > 1e-9]
205-
if len(y_clean) > 0:
206-
if np.any(y_clean > 0):
207-
sign_flags[trace.name]['has_pos'] = True
208-
if np.any(y_clean < 0):
209-
sign_flags[trace.name]['has_neg'] = True
210-
211-
# Compute class_map from aggregated sign flags
212-
for name, flags in sign_flags.items():
213-
has_pos, has_neg = flags['has_pos'], flags['has_neg']
214-
if has_pos and has_neg:
215-
class_map[name] = 'mixed'
216-
elif has_neg:
217-
class_map[name] = 'negative'
218-
elif has_pos:
219-
class_map[name] = 'positive'
220-
else:
221-
class_map[name] = 'zero'
222-
223-
def style_trace(trace: go.Scatter) -> None:
224-
"""Apply bar-like styling to a single trace."""
225-
# Look up color by trace name
226-
color = color_map.get(trace.name, default_colors[0])
227-
228-
# Look up classification
229-
cls = class_map.get(trace.name, 'positive')
230-
231-
# Set stackgroup based on classification (positive and negative stack separately)
232-
if cls in ('positive', 'negative'):
233-
trace.stackgroup = cls
234-
trace.fillcolor = color
235-
trace.line = dict(width=0, color=color, shape='hv')
236-
elif cls == 'mixed':
237-
# Mixed: show as dashed line, no stacking
238-
trace.stackgroup = None
239-
trace.fill = None
240-
trace.line = dict(width=2, color=color, shape='hv', dash='dash')
241-
else: # zero
242-
trace.stackgroup = None
243-
trace.fill = None
244-
trace.line = dict(width=0, color=color, shape='hv')
245-
246-
# Style all traces (main + animation frames)
247-
for trace in _iter_all_traces(fig):
248-
style_trace(trace)
249-
250-
251150
def _apply_unified_hover(fig: go.Figure, unit: str = '', decimals: int = 1) -> None:
252151
"""Apply unified hover mode with clean formatting to any Plotly figure.
253152
@@ -264,9 +163,8 @@ def _apply_unified_hover(fig: go.Figure, unit: str = '', decimals: int = 1) -> N
264163
unit_suffix = f' {unit}' if unit else ''
265164
hover_template = f'<b>%{{fullData.name}}</b>: %{{y:.{decimals}f}}{unit_suffix}<extra></extra>'
266165

267-
# Apply to all traces (main + animation frames)
268-
for trace in _iter_all_traces(fig):
269-
trace.hovertemplate = hover_template
166+
# Apply to all traces (main + animation frames) using xarray_plotly helper
167+
update_traces(fig, hovertemplate=hover_template)
270168

271169
# Layout settings for unified hover
272170
fig.update_layout(hovermode='x unified')
@@ -1650,13 +1548,11 @@ def balance(
16501548
unit_label = ds[first_var].attrs.get('unit', '')
16511549

16521550
_apply_slot_defaults(plotly_kwargs, 'balance')
1653-
fig = ds.plotly.area(
1551+
fig = ds.plotly.fast_bar(
16541552
title=f'{node} [{unit_label}]' if unit_label else node,
1655-
line_shape='hv',
16561553
**color_kwargs,
16571554
**plotly_kwargs,
16581555
)
1659-
_style_area_as_bar(fig)
16601556
_apply_unified_hover(fig, unit=unit_label)
16611557

16621558
if show is None:
@@ -1775,13 +1671,11 @@ def carrier_balance(
17751671
unit_label = ds[first_var].attrs.get('unit', '')
17761672

17771673
_apply_slot_defaults(plotly_kwargs, 'carrier_balance')
1778-
fig = ds.plotly.area(
1674+
fig = ds.plotly.fast_bar(
17791675
title=f'{carrier.capitalize()} Balance [{unit_label}]' if unit_label else f'{carrier.capitalize()} Balance',
1780-
line_shape='hv',
17811676
**color_kwargs,
17821677
**plotly_kwargs,
17831678
)
1784-
_style_area_as_bar(fig)
17851679
_apply_unified_hover(fig, unit=unit_label)
17861680

17871681
if show is None:
@@ -2380,13 +2274,11 @@ def storage(
23802274

23812275
# Create stacked area chart for flows (styled as bar)
23822276
_apply_slot_defaults(plotly_kwargs, 'storage')
2383-
fig = flow_ds.plotly.area(
2277+
fig = flow_ds.plotly.fast_bar(
23842278
title=f'{storage} Operation [{unit_label}]' if unit_label else f'{storage} Operation',
2385-
line_shape='hv',
23862279
**color_kwargs,
23872280
**plotly_kwargs,
23882281
)
2389-
_style_area_as_bar(fig)
23902282
_apply_unified_hover(fig, unit=unit_label)
23912283

23922284
# Add charge state as line on secondary y-axis

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ dependencies = [
4747
# Visualization
4848
"matplotlib >= 3.5.2, < 4",
4949
"plotly >= 5.15.0, < 7",
50-
"xarray_plotly >= 0.0.3, < 1",
50+
"xarray_plotly >= 0.0.10, < 1",
5151
]
5252

5353
[project.optional-dependencies]

0 commit comments

Comments
 (0)