-
Notifications
You must be signed in to change notification settings - Fork 9
Feature/plotting kwargs and streamline data conversion #439
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
Conversation
* Add animation and faceting options to plots * Adjust size of the frame * Utilize plotly express directly * Rmeocve old class * Use plotly express and modify stackgroup afterwards * Add modifications also to animations * Mkae more compact * Remove height stuff * Remove line and make set opacity =0 for area * Integrate faceting and animating into existing with_plotly method * Improve results.py * Improve results.py * Move check if dims are found to plotting.py * Fix usage of indexer * Change selection string with indexer * Change behaviout of parameter "indexing" * Update CHANGELOG.md * Add new selection parameter to plotting methods * deprectae old indexer parameter * deprectae old indexer parameter * Add test * Add test * Add test * Add test * Fix not supportet check for matplotlib * Typo in CHANGELOG.md
* Add animation and faceting options to plots * Adjust size of the frame * Utilize plotly express directly * Rmeocve old class * Use plotly express and modify stackgroup afterwards * Add modifications also to animations * Mkae more compact * Remove height stuff * Remove line and make set opacity =0 for area * Integrate faceting and animating into existing with_plotly method * Improve results.py * Improve results.py * Move check if dims are found to plotting.py * Fix usage of indexer * Change selection string with indexer * Change behaviout of parameter "indexing" * Update CHANGELOG.md * Add new selection parameter to plotting methods * deprectae old indexer parameter * deprectae old indexer parameter * Add test * Add test * Add test * Add test * Add heatmap support * Unify to a single heatmap method per engine * Change defaults * readd time reshaping * readd time reshaping * lengthen scenario example * Update * Improve heatmap plotting * Improve heatmap plotting * Moved reshaping to plotting.py * COmbinations are possible! * Improve 'auto'behavioour * Improve 'auto' behavioour * Improve 'auto' behavioour * Allow multiple variables in a heatmap * Update modeule level plot_heatmap() * remove code duplication * Allow Dataset instead of List of DataArrays * Allow Dataset instead of List of DataArrays * Update plot tests * FIx Missing renme in ElementResults.plot_heatmap() * Update API
* Add animation and faceting options to plots * Adjust size of the frame * Utilize plotly express directly * Rmeocve old class * Use plotly express and modify stackgroup afterwards * Add modifications also to animations * Mkae more compact * Remove height stuff * Remove line and make set opacity =0 for area * Integrate faceting and animating into existing with_plotly method * Improve results.py * Improve results.py * Move check if dims are found to plotting.py * Fix usage of indexer * Change selection string with indexer * Change behaviout of parameter "indexing" * Update CHANGELOG.md * Add new selection parameter to plotting methods * deprectae old indexer parameter * deprectae old indexer parameter * Add test * Add test * Add test * Add test * Add heatmap support * Unify to a single heatmap method per engine * Change defaults * readd time reshaping * readd time reshaping * lengthen scenario example * Update * Improve heatmap plotting * Improve heatmap plotting * Moved reshaping to plotting.py * COmbinations are possible! * Improve 'auto'behavioour * Improve 'auto' behavioour * Improve 'auto' behavioour * Allow multiple variables in a heatmap * Update modeule level plot_heatmap() * remove code duplication * Allow Dataset instead of List of DataArrays * Allow Dataset instead of List of DataArrays * Add tests * More examples * Update plot_charge state() * Try 1 * Try 2 * Add more examples * Add more examples * Add smooth line for charge state and use "area" as default * Update scenario_example.py * Update tests
* Add animation and faceting options to plots * Adjust size of the frame * Utilize plotly express directly * Rmeocve old class * Use plotly express and modify stackgroup afterwards * Add modifications also to animations * Mkae more compact * Remove height stuff * Remove line and make set opacity =0 for area * Integrate faceting and animating into existing with_plotly method * Improve results.py * Improve results.py * Move check if dims are found to plotting.py * Fix usage of indexer * Change selection string with indexer * Change behaviout of parameter "indexing" * Update CHANGELOG.md * Add new selection parameter to plotting methods * deprectae old indexer parameter * deprectae old indexer parameter * Add test * Add test * Add test * Add test * Add heatmap support * Unify to a single heatmap method per engine * Change defaults * readd time reshaping * readd time reshaping * lengthen scenario example * Update * Improve heatmap plotting * Improve heatmap plotting * Moved reshaping to plotting.py * COmbinations are possible! * Improve 'auto'behavioour * Improve 'auto' behavioour * Improve 'auto' behavioour * Allow multiple variables in a heatmap * Update modeule level plot_heatmap() * remove code duplication * Allow Dataset instead of List of DataArrays * Allow Dataset instead of List of DataArrays * Add tests * More examples * Update plot_charge state() * Try 1 * Try 2 * Add more examples * Add more examples * Add smooth line for charge state and use "area" as default * Update scenario_example.py * Update tests * Handle extra dims in pie plots by selecting the first
- Replaced pandas Series diff() with NumPy np.diff() for better performance - Changed check from > 0 to > 1 (can't calculate diff with 0 or 1 element) - Converted to seconds first, then to minutes to avoid pandas timedelta conversion issues
…ith both positive and negative values) - Only stack "positive" and "negative" classifications, not "mixed" or "zero"
…914) - Added fill parameter to CalculationResults.plot_heatmap method (line 702) - Forwarded fill parameter to both heatmap_with_plotly and heatmap_with_matplotlib functions
- Added specific size assertions to all tests:
- Daily/hourly pattern: 3 days × 24 hours
- Weekly/daily pattern: 1 week × 7 days
- Irregular data: 25 hours × 60 minutes
- Multidimensional: 2 days × 24 hours with preserved scenario dimension
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.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
CHANGELOG.md(1 hunks)examples/02_Complex/complex_example_results.py(1 hunks)examples/03_Calculation_types/example_calculation_types.py(1 hunks)flixopt/aggregation.py(1 hunks)flixopt/plotting.py(21 hunks)flixopt/results.py(36 hunks)tests/test_plotting_api.py(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- examples/03_Calculation_types/example_calculation_types.py
- CHANGELOG.md
- examples/02_Complex/complex_example_results.py
🧰 Additional context used
🧬 Code graph analysis (4)
tests/test_plotting_api.py (1)
flixopt/plotting.py (1)
with_plotly(397-690)
flixopt/aggregation.py (1)
flixopt/plotting.py (1)
with_plotly(397-690)
flixopt/plotting.py (2)
flixopt/config.py (1)
CONFIG(61-322)flixopt/aggregation.py (1)
plot(144-175)
flixopt/results.py (1)
flixopt/plotting.py (1)
with_matplotlib(693-840)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: test (3.12)
- GitHub Check: test (3.11)
- GitHub Check: test (3.13)
- GitHub Check: test (3.10)
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.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
flixopt/results.py (2)
1338-1345: Auto-select may KeyError if a dim exists in only one of inputs/outputs.Select per-dataset on existing coords.
- # Apply auto-selection - inputs = inputs.sel(auto_select) - outputs = outputs.sel(auto_select) + # Apply auto-selection only for coords present in each dataset + inputs = inputs.sel({k: v for k, v in auto_select.items() if k in inputs.coords}) + outputs = outputs.sel({k: v for k, v in auto_select.items() if k in outputs.coords})
1640-1645: Overlay line color not applied to animation frames.Charge-state traces in frames lose the custom color.
- for trace in frame.data: - trace.line.width = 2 - trace.line.shape = 'linear' # Smooth line for charge state + for trace in frame.data: + trace.line.width = 2 + trace.line.shape = 'linear' # Smooth line for charge state + trace.line.color = overlay_color figure_like.frames[i].data = figure_like.frames[i].data + (trace,)
🧹 Nitpick comments (7)
tests/test_plotting_api.py (2)
133-134: Avoid brittle float equality in pie sum assertion.Use pytest.approx to tolerate harmless FP rounding.
- assert sum(fig.data[0].values) == 40 + assert sum(fig.data[0].values) == pytest.approx(40)
12-22: Make fixtures deterministic (optional).Random inputs aren’t used in assertions now, but a seed prevents future flakiness.
def sample_dataset(): """Create a sample xarray Dataset for testing.""" - time = np.arange(10) + rng = np.random.default_rng(0) + time = np.arange(10) data = xr.Dataset( { - 'var1': (['time'], np.random.rand(10)), - 'var2': (['time'], np.random.rand(10)), - 'var3': (['time'], np.random.rand(10)), + 'var1': (['time'], rng.random(10)), + 'var2': (['time'], rng.random(10)), + 'var3': (['time'], rng.random(10)), }, coords={'time': time}, ) @@ def sample_dataframe(): """Create a sample pandas DataFrame for testing.""" - time = np.arange(10) - df = pd.DataFrame({'var1': np.random.rand(10), 'var2': np.random.rand(10), 'var3': np.random.rand(10)}, index=time) + rng = np.random.default_rng(1) + time = np.arange(10) + df = pd.DataFrame({'var1': rng.random(10), 'var2': rng.random(10), 'var3': rng.random(10)}, index=time) df.index.name = 'time' return dfAlso applies to: 27-33
flixopt/plotting.py (3)
380-395: Type annotation of resolve_colors is too narrow for Matplotlib.Return type can include RGBA tuples for Matplotlib; narrow typing to str is misleading.
-) -> dict[str, str]: +) -> dict[str, Any]: """Resolve colors parameter to a dict mapping variable names to colors."""
386-394: Plotly + dict colors: normalize non-string color values.If users provide Matplotlib RGBA tuples in a dict while using Plotly, traces may fail. Normalize tuples to 'rgba(r,g,b,a)' strings when engine='plotly'.
- if isinstance(colors, dict): - return colors + if isinstance(colors, dict): + if engine == 'plotly': + def _mpl_tuple_to_rgba(c): + if isinstance(c, tuple) and (len(c) == 3 or len(c) == 4): + r, g, b, *rest = c + a = rest[0] if rest else 1.0 + # Matplotlib tuples are 0..1 floats; convert to 0..255 + if all(isinstance(x, (int, float)) and 0.0 <= x <= 1.0 for x in (r, g, b)): + r, g, b = int(round(r*255)), int(round(g*255)), int(round(b*255)) + return f'rgba({r},{g},{b},{a})' + return c + return {k: _mpl_tuple_to_rgba(v) for k, v in colors.items()} + return colors
1758-1766: export_figure docstring mismatches behavior (show/dpi).Doc says show defaults to None and dpi uses CONFIG; code uses show=True and passes dpi directly (None -> Matplotlib defaults). Align docs.
- show: Whether to display the figure. If None, uses CONFIG.Plotting.default_show (default: None). + show: Whether to display the figure (default: True). @@ - dpi: DPI (dots per inch) for saving Matplotlib figures. If None, uses CONFIG.Plotting.default_dpi. + dpi: DPI (dots per inch) for saving Matplotlib figures. If None, Matplotlib rcParams are used.flixopt/results.py (2)
1665-1675: Matplotlib legend style is overridden by a second default legend call.The new ax.legend() replaces the nicely formatted legend from with_matplotlib. Recreate with the same styling so the overlay integrates cleanly.
- ax.legend() + handles, labels = ax.get_legend_handles_labels() + ax.legend( + handles, + labels, + loc='upper center', + bbox_to_anchor=(0.5, -0.15), + ncol=5, + frameon=False, + )
1309-1315: Optional: title suffix may omit input-side selections.suffix_parts from inputs is overwritten by outputs selection. Concatenate both for clarity.
- inputs, suffix_parts = _apply_selection_to_data(inputs, select=select, drop=True) - outputs, suffix_parts = _apply_selection_to_data(outputs, select=select, drop=True) + inputs, suffix_parts_in = _apply_selection_to_data(inputs, select=select, drop=True) + outputs, suffix_parts_out = _apply_selection_to_data(outputs, select=select, drop=True) + suffix_parts = suffix_parts_in + suffix_parts_out @@ - suffix = '--' + '-'.join(suffix_parts) if suffix_parts else '' + suffix = '--' + '-'.join(sorted(set(suffix_parts))) if suffix_parts else ''Also applies to: 1346-1348
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
flixopt/plotting.py(20 hunks)flixopt/results.py(36 hunks)tests/test_plotting_api.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
tests/test_plotting_api.py (1)
flixopt/plotting.py (4)
with_plotly(397-702)with_matplotlib(705-852)dual_pie_with_plotly(1188-1280)dual_pie_with_matplotlib(1283-1387)
flixopt/results.py (1)
flixopt/plotting.py (1)
with_matplotlib(705-852)
flixopt/plotting.py (2)
flixopt/config.py (2)
CONFIG(61-322)to_dict(289-322)flixopt/aggregation.py (1)
plot(144-175)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: test (3.13)
- GitHub Check: test (3.10)
- GitHub Check: test (3.12)
- GitHub Check: test (3.11)
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.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
flixopt/results.py (4)
1159-1192: Avoid leaking Matplotlib-only kwargs to Plotly; partition kwargs by engine.Passing plot_kwargs to Plotly will raise “unexpected keyword” errors; likewise, Plotly-only kwargs shouldn’t reach Matplotlib. Partition before dispatch.
Apply:
@@ - # Extract dpi for export_figure - dpi = plot_kwargs.pop('dpi', None) # None uses CONFIG.Plotting.default_dpi + # Extract dpi for export_figure + dpi = plot_kwargs.pop('dpi', None) # None handled in export_figure + # Partition kwargs by engine to avoid passing unsupported params + px_kwargs = {k: v for k, v in plot_kwargs.items() if k != 'plot_kwargs'} + mpl_kwargs = {'plot_kwargs': plot_kwargs.get('plot_kwargs', {})} @@ - figure_like = plotting.with_plotly( + figure_like = plotting.with_plotly( ds, @@ - xlabel='Time in h', - **plot_kwargs, + xlabel='Time in h', + **px_kwargs, ) @@ - figure_like = plotting.with_matplotlib( + figure_like = plotting.with_matplotlib( ds, colors=colors, mode=mode, title=title, - **plot_kwargs, + **mpl_kwargs, )
1291-1376: Matplotlib pie doesn’t accept arbitrary kwargs; only pass supported keys.dual_pie_with_matplotlib has no **kwargs, so forwarding plot_kwargs can explode (e.g., dpi, hover_template). Extract only supported params; keep Plotly extras separate.
Apply:
@@ - # Extract dpi for export_figure - dpi = plot_kwargs.pop('dpi', None) # None uses CONFIG.Plotting.default_dpi + # Extract dpi for export_figure + dpi = plot_kwargs.pop('dpi', None) # None handled in export_figure + # Partition kwargs and extract matplotlib-supported keys + pie_px_kwargs = {k: v for k, v in plot_kwargs.items() if k != 'plot_kwargs'} + hole_kw = plot_kwargs.get('hole') + figsize_kw = plot_kwargs.get('figsize') @@ - figure_like = plotting.dual_pie_with_plotly( + figure_like = plotting.dual_pie_with_plotly( data_left=inputs, data_right=outputs, colors=colors, title=title, text_info=text_info, subtitles=('Inputs', 'Outputs'), legend_title='Flows', lower_percentage_group=lower_percentage_group, - **plot_kwargs, + **pie_px_kwargs, ) @@ - figure_like = plotting.dual_pie_with_matplotlib( + figure_like = plotting.dual_pie_with_matplotlib( data_left=inputs.to_pandas(), data_right=outputs.to_pandas(), colors=colors, title=title, subtitles=('Inputs', 'Outputs'), legend_title='Flows', lower_percentage_group=lower_percentage_group, - **plot_kwargs, + **({k: v for k, v in {'hole': hole_kw, 'figsize': figsize_kw}.items() if v is not None}), )
838-859: Don’t forward Matplotlib-style plot_kwargs into Plotly heatmaps.plot_kwargs may include a plot_kwargs dict intended for Matplotlib; remove it when engine='plotly' to prevent px.imshow argument errors.
Apply:
@@ - return plot_heatmap( + return plot_heatmap( data=self.solution[variable_name], @@ - **plot_kwargs, + **plot_kwargs, )And in the module-level plot_heatmap() below (lines ~2274–2316), pop plot_kwargs for Plotly:
@@ - # Extract dpi before passing to plotting functions - dpi = plot_kwargs.pop('dpi', None) # None uses CONFIG.Plotting.default_dpi + # Extract dpi before passing to plotting functions + dpi = plot_kwargs.pop('dpi', None) + # Avoid leaking Matplotlib-only container to Plotly + if engine == 'plotly': + plot_kwargs.pop('plot_kwargs', None)
1578-1664: Partition kwargs in plot_charge_state; fix overlay path for both engines.Same leakage issue as node balance; ensure Plotly doesn’t receive plot_kwargs and Matplotlib only receives plot_kwargs dict.
Apply:
@@ - # Extract dpi for export_figure - dpi = plot_kwargs.pop('dpi', None) # None uses CONFIG.Plotting.default_dpi + # Extract dpi for export_figure + dpi = plot_kwargs.pop('dpi', None) @@ - # Extract charge state line color (for overlay customization) - overlay_color = plot_kwargs.pop('charge_state_line_color', 'black') + # Extract charge state line color (for overlay customization) + overlay_color = plot_kwargs.pop('charge_state_line_color', 'black') + # Partition kwargs by engine + px_kwargs = {k: v for k, v in plot_kwargs.items() if k != 'plot_kwargs'} + mpl_kwargs = {'plot_kwargs': plot_kwargs.get('plot_kwargs', {})} @@ - figure_like = plotting.with_plotly( + figure_like = plotting.with_plotly( ds, @@ - xlabel='Time in h', - **plot_kwargs, + xlabel='Time in h', + **px_kwargs, ) @@ - charge_state_fig = plotting.with_plotly( + charge_state_fig = plotting.with_plotly( charge_state_ds, @@ - xlabel='Time in h', - **plot_kwargs, + xlabel='Time in h', + **px_kwargs, ) @@ - fig, ax = plotting.with_matplotlib( + fig, ax = plotting.with_matplotlib( ds, colors=colors, mode=mode, title=title, - **plot_kwargs, + **mpl_kwargs, )flixopt/plotting.py (1)
1764-1837: Honor “dpi=None uses defaults” and avoid passing None to savefig.export_figure should treat dpi=None as “use rcParams/savefig.dpi”. Current code forwards None, which may error and contradicts docs.
Apply:
@@ -def export_figure( +def export_figure( @@ - dpi: int = 300, + dpi: int | None = 300, ) -> go.Figure | tuple[plt.Figure, plt.Axes]: @@ - if save: - fig.savefig(str(filename), dpi=dpi) + if save: + eff_dpi = matplotlib.rcParams.get('savefig.dpi', 300) if dpi is None else dpi + fig.savefig(str(filename), dpi=eff_dpi) plt.close(fig) # Close figure to free memory
🧹 Nitpick comments (2)
tests/test_plotting_api.py (1)
37-46: Good passthrough check for px kwargs. Consider adding heatmap cases.Add a small test to assert that vmin/vmax work with Plotly heatmaps (mapped to zmin/zmax) and that Matplotlib-only keys (plot_kwargs) don’t break Plotly calls.
flixopt/plotting.py (1)
1402-1462: Optional: accept list colorscales directly for Plotly heatmaps.color_continuous_scale can be a list; enable colors list passthrough instead of forcing 'viridis'.
Apply:
@@ - common_args = { + common_args = { 'img': data, - 'color_continuous_scale': colors if isinstance(colors, str) else 'viridis', + 'color_continuous_scale': colors if isinstance(colors, (str, list)) else 'viridis', 'title': title, }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
flixopt/plotting.py(20 hunks)flixopt/results.py(38 hunks)tests/test_plotting_api.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
tests/test_plotting_api.py (1)
flixopt/plotting.py (4)
with_plotly(397-702)with_matplotlib(705-863)dual_pie_with_plotly(1199-1291)dual_pie_with_matplotlib(1294-1398)
flixopt/results.py (1)
flixopt/plotting.py (2)
with_matplotlib(705-863)dual_pie_with_plotly(1199-1291)
flixopt/plotting.py (2)
flixopt/config.py (1)
CONFIG(61-322)flixopt/aggregation.py (1)
plot(144-175)
| # Merge in additional imshow kwargs | ||
| common_args.update(imshow_kwargs) | ||
|
|
||
| try: | ||
| fig = px.imshow(**common_args) | ||
| except Exception as e: | ||
| logger.error(f'Error creating imshow plot: {e}. Falling back to basic heatmap.') | ||
| # Fallback: create a simple heatmap without faceting | ||
| fig = px.imshow( | ||
| data.values, | ||
| color_continuous_scale=colors if isinstance(colors, str) else 'viridis', | ||
| title=title, | ||
| ) | ||
|
|
||
| # Update layout with basic styling | ||
| fig.update_layout( | ||
| plot_bgcolor='rgba(0,0,0,0)', | ||
| paper_bgcolor='rgba(0,0,0,0)', | ||
| font=dict(size=12), | ||
| ) | ||
| fallback_args = { | ||
| 'img': data.values, | ||
| 'color_continuous_scale': colors if isinstance(colors, str) else 'viridis', | ||
| 'title': title, | ||
| } | ||
| fallback_args.update(imshow_kwargs) | ||
| fig = px.imshow(**fallback_args) | ||
|
|
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.
Map vmin/vmax to zmin/zmax for Plotly heatmaps.
Docs say vmin/vmax are supported “for both engines.” px.imshow expects zmin/zmax; without mapping, users hit TypeError.
Apply:
@@
- # Merge in additional imshow kwargs
- common_args.update(imshow_kwargs)
+ # Map matplotlib-style vmin/vmax to Plotly's zmin/zmax if provided
+ if 'vmin' in imshow_kwargs and 'zmin' not in imshow_kwargs:
+ imshow_kwargs['zmin'] = imshow_kwargs.pop('vmin')
+ if 'vmax' in imshow_kwargs and 'zmax' not in imshow_kwargs:
+ imshow_kwargs['zmax'] = imshow_kwargs.pop('vmax')
+ # Merge in additional imshow kwargs
+ common_args.update(imshow_kwargs)🤖 Prompt for AI Agents
In flixopt/plotting.py around lines 1594 to 1609, px.imshow expects zmin/zmax
but the code passes vmin/vmax which causes a TypeError; before calling px.imshow
(both for the main common_args and the fallback_args) map any vmin -> zmin and
vmax -> zmax (and remove the original vmin/vmax keys) so the arguments passed to
px.imshow use zmin/zmax instead; do this mapping defensively (only if vmin/vmax
present and not None) for both the merged common_args and the fallback_args
prior to calling px.imshow.
Description
Updating the plotting methods to be cleaner and to allow passing kwargs to poorly express and matplotlib
Type of Change
Related Issues
Closes #(issue number)
Testing
Checklist
Summary by CodeRabbit