-
Notifications
You must be signed in to change notification settings - Fork 9
Use area chart instead of bar chart for better performance #577
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -145,6 +145,128 @@ def _reshape_time_for_heatmap( | |
| return result.transpose('timestep', 'timeframe', *other_dims) | ||
|
|
||
|
|
||
| def _iter_all_traces(fig: go.Figure): | ||
| """Iterate over all traces in a figure, including animation frames. | ||
|
|
||
| Yields traces from fig.data first, then from each frame in fig.frames. | ||
| Useful for applying styling to all traces including those in animations. | ||
|
|
||
| Args: | ||
| fig: Plotly Figure. | ||
|
|
||
| Yields: | ||
| Each trace object from the figure. | ||
| """ | ||
| yield from fig.data | ||
| for frame in getattr(fig, 'frames', []) or []: | ||
| yield from frame.data | ||
|
|
||
|
|
||
| def _style_area_as_bar(fig: go.Figure) -> None: | ||
| """Style area chart traces to look like bar charts with proper pos/neg stacking. | ||
|
|
||
| Iterates over all traces in fig.data and fig.frames (for animations), | ||
| setting stepped line shape, removing line borders, making fills opaque, | ||
| and assigning stackgroups based on whether values are positive or negative. | ||
|
|
||
| Handles faceting + animation combinations by building color and classification | ||
| maps from trace names in the base figure. | ||
|
|
||
| Args: | ||
| fig: Plotly Figure with area chart traces. | ||
| """ | ||
| import plotly.express as px | ||
|
|
||
| default_colors = px.colors.qualitative.Plotly | ||
|
|
||
| # Build color map and classify traces from base figure | ||
| # trace.name -> color, trace.name -> 'positive'|'negative'|'mixed'|'zero' | ||
| color_map: dict[str, str] = {} | ||
| class_map: dict[str, str] = {} | ||
|
|
||
| for i, trace in enumerate(fig.data): | ||
| # Get color | ||
| if hasattr(trace, 'line') and trace.line and trace.line.color: | ||
| color_map[trace.name] = trace.line.color | ||
| else: | ||
| color_map[trace.name] = default_colors[i % len(default_colors)] | ||
|
|
||
| # Classify based on y values | ||
| y_vals = trace.y | ||
| if y_vals is None or len(y_vals) == 0: | ||
| class_map[trace.name] = 'zero' | ||
| else: | ||
| y_arr = np.asarray(y_vals) | ||
| y_clean = y_arr[np.abs(y_arr) > 1e-9] | ||
| if len(y_clean) == 0: | ||
| class_map[trace.name] = 'zero' | ||
| else: | ||
| has_pos = np.any(y_clean > 0) | ||
| has_neg = np.any(y_clean < 0) | ||
| if has_pos and has_neg: | ||
| class_map[trace.name] = 'mixed' | ||
| elif has_neg: | ||
| class_map[trace.name] = 'negative' | ||
| else: | ||
| class_map[trace.name] = 'positive' | ||
|
|
||
| def style_trace(trace: go.Scatter) -> None: | ||
| """Apply bar-like styling to a single trace.""" | ||
| # Look up color by trace name | ||
| color = color_map.get(trace.name, default_colors[0]) | ||
|
|
||
| # Look up classification | ||
| cls = class_map.get(trace.name, 'positive') | ||
|
|
||
| # Set stackgroup based on classification (positive and negative stack separately) | ||
| if cls in ('positive', 'negative'): | ||
| trace.stackgroup = cls | ||
| trace.fillcolor = color | ||
| trace.line = dict(width=0, color=color, shape='hv') | ||
| elif cls == 'mixed': | ||
| # Mixed: show as dashed line, no stacking | ||
| trace.stackgroup = None | ||
| trace.fill = None | ||
| trace.line = dict(width=2, color=color, shape='hv', dash='dash') | ||
| else: # zero | ||
| trace.stackgroup = None | ||
| trace.fill = None | ||
| trace.line = dict(width=0, color=color, shape='hv') | ||
|
|
||
| # Style all traces (main + animation frames) | ||
| for trace in _iter_all_traces(fig): | ||
| style_trace(trace) | ||
|
|
||
|
|
||
| def _apply_unified_hover(fig: go.Figure, unit: str = '', decimals: int = 1) -> None: | ||
| """Apply unified hover mode with clean formatting to any Plotly figure. | ||
|
|
||
| Sets up 'x unified' hovermode with spike lines and formats hover labels | ||
| as '<b>name</b>: value unit'. | ||
|
|
||
| Works with any plot type (area, bar, line, scatter). | ||
|
|
||
| Args: | ||
| fig: Plotly Figure to style. | ||
| unit: Unit string to append (e.g., 'kW', 'MWh'). Empty for no unit. | ||
| decimals: Number of decimal places for values. | ||
| """ | ||
| unit_suffix = f' {unit}' if unit else '' | ||
| hover_template = f'<b>%{{fullData.name}}</b>: %{{y:.{decimals}f}}{unit_suffix}<extra></extra>' | ||
|
|
||
| # Apply to all traces (main + animation frames) | ||
| for trace in _iter_all_traces(fig): | ||
| trace.hovertemplate = hover_template | ||
|
|
||
| # Layout settings for unified hover | ||
| fig.update_layout( | ||
| hovermode='x unified', | ||
| xaxis_showspikes=True, | ||
| xaxis_spikecolor='gray', | ||
| xaxis_spikethickness=1, | ||
| ) | ||
|
Comment on lines
+261
to
+267
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, find the file and examine the context around lines 261-267
cat -n flixopt/statistics_accessor.py | sed -n '240,290p'Repository: flixOpt/flixopt Length of output: 174 🏁 Script executed: # Search for any subplot or facet configuration in the file
rg -n "make_subplots|facet_row|facet_col|secondary_y" flixopt/statistics_accessor.pyRepository: flixOpt/flixopt Length of output: 174 🏁 Script executed: # Look for the function containing this code to understand its full context
ast-grep --pattern $'def $_($_) {
$$$
fig.update_layout(
hovermode=$_,
$$$
)
$$$
}'Repository: flixOpt/flixopt Length of output: 41 🌐 Web query:
💡 Result: Short answer — two ways:
Notes / gotchas:
References:
🌐 Web query:
💡 Result: Short answer — use update_xaxes to change x-axis spike properties for one or many x-axes programmatically; use update_layout (or the xaxis/xaxis2/xaxis3 dicts) to set properties for a specific named axis. Note: spike drawing depends on hovermode and Plotly’s subplot behavior (spikes across separate subplot axes can be limited). How to apply spikes
Important notes / gotchas
References
If you want, tell me your exact code (make_subplots vs single plot, which axes to target) and I’ll give the precise update_xaxes / update_layout snippet. Apply spike settings consistently across all x-axes using In faceted plots, Suggested adjustment- fig.update_layout(
- hovermode='x unified',
- xaxis_showspikes=True,
- xaxis_spikecolor='gray',
- xaxis_spikethickness=1,
- )
+ fig.update_layout(hovermode='x unified')
+ fig.update_xaxes(showspikes=True, spikecolor='gray', spikethickness=1)🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| # --- Helper functions --- | ||
|
|
||
|
|
||
|
|
@@ -1529,13 +1651,14 @@ def balance( | |
| unit_label = ds[first_var].attrs.get('unit', '') | ||
|
|
||
| _apply_slot_defaults(plotly_kwargs, 'balance') | ||
| fig = ds.plotly.bar( | ||
| fig = ds.plotly.area( | ||
| title=f'{node} [{unit_label}]' if unit_label else node, | ||
| line_shape='hv', | ||
| **color_kwargs, | ||
| **plotly_kwargs, | ||
| ) | ||
| fig.update_layout(barmode='relative', bargap=0, bargroupgap=0) | ||
| fig.update_traces(marker_line_width=0) | ||
| _style_area_as_bar(fig) | ||
| _apply_unified_hover(fig, unit=unit_label) | ||
|
|
||
| if show is None: | ||
| show = CONFIG.Plotting.default_show | ||
|
|
@@ -1653,13 +1776,14 @@ def carrier_balance( | |
| unit_label = ds[first_var].attrs.get('unit', '') | ||
|
|
||
| _apply_slot_defaults(plotly_kwargs, 'carrier_balance') | ||
| fig = ds.plotly.bar( | ||
| fig = ds.plotly.area( | ||
| title=f'{carrier.capitalize()} Balance [{unit_label}]' if unit_label else f'{carrier.capitalize()} Balance', | ||
| line_shape='hv', | ||
| **color_kwargs, | ||
| **plotly_kwargs, | ||
| ) | ||
| fig.update_layout(barmode='relative', bargap=0, bargroupgap=0) | ||
| fig.update_traces(marker_line_width=0) | ||
| _style_area_as_bar(fig) | ||
| _apply_unified_hover(fig, unit=unit_label) | ||
|
|
||
| if show is None: | ||
| show = CONFIG.Plotting.default_show | ||
|
|
@@ -2249,15 +2373,22 @@ def storage( | |
| else: | ||
| color_kwargs = _build_color_kwargs(colors, flow_labels) | ||
|
|
||
| # Create stacked bar chart for flows | ||
| # Get unit label from flow data | ||
| unit_label = '' | ||
| if flow_ds.data_vars: | ||
| first_var = next(iter(flow_ds.data_vars)) | ||
| unit_label = flow_ds[first_var].attrs.get('unit', '') | ||
|
|
||
| # Create stacked area chart for flows (styled as bar) | ||
| _apply_slot_defaults(plotly_kwargs, 'storage') | ||
| fig = flow_ds.plotly.bar( | ||
| title=f'{storage} Operation ({unit})', | ||
| fig = flow_ds.plotly.area( | ||
| title=f'{storage} Operation [{unit_label}]' if unit_label else f'{storage} Operation', | ||
| line_shape='hv', | ||
| **color_kwargs, | ||
| **plotly_kwargs, | ||
| ) | ||
| fig.update_layout(barmode='relative', bargap=0, bargroupgap=0) | ||
| fig.update_traces(marker_line_width=0) | ||
| _style_area_as_bar(fig) | ||
| _apply_unified_hover(fig, unit=unit_label) | ||
|
|
||
| # Add charge state as line on secondary y-axis | ||
| # Only pass faceting kwargs that add_line_overlay accepts | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: flixOpt/flixopt
Length of output: 91
🏁 Script executed:
# Read the relevant file to see the _style_area_as_bar implementation wc -l flixopt/statistics_accessor.pyRepository: flixOpt/flixopt
Length of output: 94
🏁 Script executed:
Repository: flixOpt/flixopt
Length of output: 1897
🏁 Script executed:
Repository: flixOpt/flixopt
Length of output: 281
🏁 Script executed:
Repository: flixOpt/flixopt
Length of output: 678
🏁 Script executed:
Repository: flixOpt/flixopt
Length of output: 41
🏁 Script executed:
Repository: flixOpt/flixopt
Length of output: 579
🏁 Script executed:
Repository: flixOpt/flixopt
Length of output: 705
🏁 Script executed:
Repository: flixOpt/flixopt
Length of output: 1085
🏁 Script executed:
Repository: flixOpt/flixopt
Length of output: 2813
🏁 Script executed:
Repository: flixOpt/flixopt
Length of output: 532
🏁 Script executed:
Repository: flixOpt/flixopt
Length of output: 41
🏁 Script executed:
Repository: flixOpt/flixopt
Length of output: 3320
🏁 Script executed:
Repository: flixOpt/flixopt
Length of output: 41
🏁 Script executed:
Repository: flixOpt/flixopt
Length of output: 80
🏁 Script executed:
Repository: flixOpt/flixopt
Length of output: 4422
🏁 Script executed:
Repository: flixOpt/flixopt
Length of output: 5104
🏁 Script executed:
Repository: flixOpt/flixopt
Length of output: 945
🏁 Script executed:
Repository: flixOpt/flixopt
Length of output: 3182
🏁 Script executed:
Repository: flixOpt/flixopt
Length of output: 1401
🏁 Script executed:
Repository: flixOpt/flixopt
Length of output: 377
🏁 Script executed:
Repository: flixOpt/flixopt
Length of output: 1073
Classification ignores animation frames, leading to incorrect stacking in time-varying plots.
_style_area_as_barbuildsclass_maponly fromfig.data(line 186), so animation frames with different sign patterns (e.g., a trace "Solar" positive in summer, zero in winter) are misclassified and stacked incorrectly. The docstring promises animation support but the implementation classifies only from the base figure.Aggregate sign information across
_iter_all_traces(fig)before building the classification map to ensure consistent stackgroup assignment across all frames.🔧 Proposed fix (aggregate sign across all traces)
Also applies to: 237-238
🤖 Prompt for AI Agents