Skip to content

Conversation

@FBumann
Copy link
Member

@FBumann FBumann commented Oct 18, 2025

  • 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

Description

Brief description of the changes in this PR.

Type of Change

  • Bug fix
  • New feature
  • Documentation update
  • Code refactoring

Related Issues

Closes #(issue number)

Testing

  • I have tested my changes
  • Existing tests still pass

Checklist

  • My code follows the project style
  • I have updated documentation if needed
  • I have added tests for new functionality (if applicable)

Summary by CodeRabbit

  • New Features

    • Faceting and animation for plots, new select parameter for flexible filtering, heatmap fill control, dashed styling for mixed-area plots, broader pandas/xarray input support, new heatmap entry points.
  • Changed

    • Plotting APIs modernized (select-first semantics), Plotly-first behavior, unified heatmap/time-reshape flow, improved error messages and stricter Matplotlib checks.
  • Deprecated

    • indexer and legacy heatmap params deprecated with warnings.
  • Tests / Docs

    • Added heatmap reshape tests and static test resource; removed legacy plotting test; updated contributor docs.
  • Examples

    • Scenario example extended timeline and updated component parameters and plotting usage.

* 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
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 18, 2025

Walkthrough

Adds faceting and animation to plotting, broad xarray support and heatmap reshaping, introduces a new select parameter replacing indexer, adds facet_by/animate_by/fill/reshape_time controls, renames internal _apply_indexer_to_data_apply_selection_to_data, updates examples and tests, and removes a legacy plotting test.

Changes

Cohort / File(s) Summary
Core plotting
flixopt/plotting.py
with_plotly now accepts `pd.DataFrame
Results / public plotting APIs
flixopt/results.py
Replaces _apply_indexer_to_data with _apply_selection_to_data; introduces select, facet_by, animate_by, facet_cols, reshape_time, fill, colors across plotting methods; plot_heatmap and other plotters accept `xr.DataArray
Example scenario
examples/04_Scenarios/scenario_example.py
Extends timesteps to one week and regenerates realistic hourly time series; updates component parameters (boiler, CHP, storage efficiencies/limits, gas pricing/CO2); switches to scenario_weights; replaces inline plotting with plot_charge_state calls and adjusts node-balance plotting.
New heatmap reshape tests
tests/test_heatmap_reshape.py
Adds tests for reshape_data_for_heatmap: daily/weekly patterns, irregular timestamps with forward-fill, preservation of leading dims (e.g., scenario), and passthrough when reshape_time=None.
Results plot tests updated
tests/test_results_plots.py
Adjusts tests to pass plotting kwargs via a heatmap_kwargs dict (reshape_time, colors, save, show, engine); disables faceting/animation for matplotlib where incompatible; updates call sites to use kwargs.
Tests resources & removals
tests/ressources/Sim1--summary.yaml, tests/test_plots.py
Adds Sim1--summary.yaml test resource; removes legacy/manual plotting test tests/test_plots.py.
Repo config
.github/CONTRIBUTING.md
Development install extras extended to include dev and docs.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant User
    participant Results as CalculationResults
    participant Plotting as flixopt.plotting
    participant Xr as xarray
    participant PX as PlotlyExpress

    User->>Results: plot_heatmap(variable_name, select?, facet_by?, animate_by?)
    Results->>Plotting: apply selection via _apply_selection_to_data(select)
    Plotting->>Xr: detect xr.DataArray/Dataset?
    alt xr input or reshape_time requested
        Plotting->>Plotting: reshape_data_for_heatmap(reshape_time, facet_by, animate_by, fill)
        Plotting->>Plotting: convert xr -> long-form DataFrame (if needed)
        Plotting->>PX: px.imshow/px.line with facet/animation args
    else DataFrame-only
        Plotting->>PX: px.line/px.bar directly
    end
    PX-->>Plotting: go.Figure
    alt engine == matplotlib
        Plotting-->>User: (fig, ax)
    else
        Plotting-->>User: go.Figure
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • baumbude
  • PStange

Poem

🐇 I hopped through arrays both wide and deep,

Facets and frames awake from sleep,
Heatmaps reshaped, old indexers freed,
Select bounds the view — then plots take speed,
A tiny carrot for each rendered feed 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The PR description is largely incomplete relative to the provided template. While the description section header is present, it contains only the template placeholder text ("Brief description of the changes in this PR.") rather than an actual description. The Type of Change section has no checkboxes selected, the Related Issues section remains as placeholder text ("Closes #(issue number)"), and the Testing section has both verification boxes unchecked. Although the description begins with a bullet-point list of commit messages that provides some context about the changes, the structured template sections required for clarity and proper triage are not meaningfully filled out.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The PR title "Feature/398 feature facet plots in results (#419)" is partially related to the changeset. It clearly refers to faceting, which is a significant new feature being added to the results plotting functionality. However, the title focuses only on faceting while the changeset also includes animation (animate_by parameter) as an equally important feature, along with a new select parameter and deprecation of the indexer parameter. Since the title refers to a real and substantial part of the changes without fully capturing all main aspects, it constitutes a partial match rather than a complete summary.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/plots-with-faceting-and-animations

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

* 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
@FBumann
Copy link
Member Author

FBumann commented Oct 18, 2025

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 18, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
CHANGELOG.md (1)

88-91: Fix contradictory/typo’d notes around plotting parameter names.

Bullets conflict: “continue to use mode” vs “Renamed new mode parameter to unit_type”. Also missing backtick.

-### 🐛 Fixed
-- Reverted breaking change from v3.0.0: continue to use `mode parameter in plotting instead of new `style`
-- Renamed new `mode` parameter in plotting methods to `unit_type`
+### 🐛 Fixed
+- Reverted breaking change from v3.0.0: continue to use `mode` parameter in plotting instead of the new `style`.
+### ♻️ Changed
+- (Remove if not intended) If renaming is desired, clarify: “Renamed plotting parameter `style` back to `mode`.” Do not mention `unit_type` unless implemented.

Please adjust according to the actual code (current public API uses mode).

🧹 Nitpick comments (14)
tests/test_overlay_line_on_area.py (3)

105-111: Deep‑copy traces when moving them between figures/frames.

Reusing the same trace objects from charge_fig in fig1/fig2/fig3/fig4/fig5 (and frames) can cause shared‑UID/axis-binding issues and unexpected mutation. Clone the traces before adding.

Apply this pattern once and reuse across all overlays:

+import copy as _copy
...
-    fig1.add_trace(trace)
+    fig1.add_trace(_copy.deepcopy(trace))
...
-                fig3.frames[i].data = fig3.frames[i].data + (trace,)
+                fig3.frames[i].data = fig3.frames[i].data + (_copy.deepcopy(trace),)

Also applies to: 155-161, 202-208, 214-221, 268-273, 275-284, 328-334


105-111: Tighten legend noise for overlay lines.

Multiple identical “ChargeState” traces across subplots/frames can spam legends. Show one legend entry and hide the rest.

Example tweak:

 for trace in charge_fig.data:
     trace.line.width = 3
     trace.line.shape = 'linear'
-    trace.line.dash = 'dash'
-    fig1.add_trace(trace)
+    trace.line.dash = 'dash'
+    trace.showlegend = False
+    fig1.add_trace(_copy.deepcopy(trace))
+# Optionally turn on legend for the first added trace only:
+# fig1.data[-1].showlegend = True

Also applies to: 155-161, 202-208, 268-273, 328-334


107-109: “linear” isn’t “smooth”.

Comment says “Smooth line”, but line.shape='linear' is straight segments. If you actually want smoothing, use spline; if you just want non‑stepped, keep linear and update the comment.

-    trace.line.shape = 'linear'  # Smooth line (not stepped like flows)
+    trace.line.shape = 'linear'  # Straight (non-stepped) line

Or:

-    trace.line.shape = 'linear'
+    trace.line.shape = 'spline'

Also applies to: 158-160, 204-206, 217-219, 270-272, 281-283, 330-333

flixopt/plotting.py (4)

838-846: Simplify time‑step check; avoid pandas Series for deltas.

Converting to pd.Series(...).diff() is heavier and can introduce dtype quirks. Use NumPy timedeltas for a leaner, safer min‑step computation.

-    if data.sizes['time'] > 0:
-        time_diff = pd.Series(data.coords['time'].values).diff().dropna()
-        if len(time_diff) > 0:
-            min_time_diff_min = time_diff.min().total_seconds() / 60
+    if data.sizes['time'] > 1:
+        tvals = pd.to_datetime(data.coords['time'].values)
+        deltas = (tvals[1:] - tvals[:-1]).astype('timedelta64[m]').astype(float)
+        if deltas.size > 0:
+            min_time_diff_min = np.nanmin(deltas)

628-641: Bar width from Timedelta may be odd in Matplotlib.

width = data.index.to_series().diff().dropna().min() yields a Timedelta; Matplotlib accepts numeric widths. Convert to days (float) for consistent rendering across backends.

-        width = data.index.to_series().diff().dropna().min()  # Minimum time difference
+        dt = data.index.to_series().diff().dropna().min()
+        width = getattr(dt, "days", None) if dt is not pd.NaT else None
+        if width is None or width == 0:
+            # Fallback: one unit or 0.8*step in days
+            width = (dt / pd.Timedelta(days=1)) * 0.8 if pd.notna(dt) else 1.0

520-532: Group/stack modes: ensure consistent gap defaults.

Minor nit: grouped bars use bargap=0.2; stacked uses bargap=0. Consider exposing bargap as a kwarg or standardize defaults for visual consistency.

No code change required; consider adding a bar_gap: float | None = None kwarg and pass through to update_layout(...).


497-501: Color mapping determinism.

all_vars = df_long['variable'].unique().tolist() preserves first-seen order; with melted DataFrames this can vary. If you want stable colors across runs, sort the labels before assignment.

-    all_vars = df_long['variable'].unique().tolist()
+    all_vars = sorted(df_long['variable'].unique().tolist())
tests/test_results_plots.py (1)

59-62: Facet/animate disabling for matplotlib is good; consider asserting no errors.

Optional: add a quick smoke assertion that the call returns without raising and closes figures to keep CI quiet.

No diff necessary; consider wrapping results.plot_heatmap(...) in a try/except and plt.close('all') in a finally.

tests/test_plots.py (1)

64-66: Avoid writing artifacts to repo root; use a temp path.

Saving PNGs into the working directory can clutter CI and developer machines. Prefer tempfile.TemporaryDirectory() or pytest’s tmp_path to write images, and remove them after assertions.

Also applies to: 73-75, 80-82, 88-90, 113-115, 141-143, 149-151, 157-159

examples/04_Scenarios/scenario_example.py (1)

85-91: Verify dimension alignment for vector parameters.

  • relative_maximum_final_charge_state has length 3 (likely period).
  • relative_loss_per_hour has length 2 (likely scenario).
  • relative_maximum_charge_state has length 9*20 (likely time).

Make sure Storage maps these arrays to the intended dims (period/scenario/time). Passing raw numpy arrays without explicit xarray coords can silently misalign. Consider using xr.DataArray with named dims for clarity.

tests/test_select_features.py (1)

15-16: Avoid 'browser' renderer in tests.

Setting pio.renderers.default = 'browser' can misbehave on headless CI. Use 'json' (safe, no GUI) or 'svg'/'png' with kaleido if needed.

flixopt/results.py (3)

2107-2161: Handle tuples/ndarrays/Indexes in coord filters.

filter_dataarray_by_coord only treats list as multi-value; tuples, numpy arrays, and pandas Indexes are mishandled. Normalize all sequence-like inputs to a list to avoid false negatives.

Apply this diff:

@@
-    def apply_filter(array, coord_name: str, coord_values: Any | list[Any]):
+    def apply_filter(array, coord_name: str, coord_values: Any | list[Any]):
         # Verify coord exists
         if coord_name not in array.coords:
             raise AttributeError(f"Missing required coordinate '{coord_name}'")
 
-        # Convert single value to list
-        val_list = [coord_values] if isinstance(coord_values, str) else coord_values
+        # Normalize to list for sequence-like inputs (excluding strings)
+        import numpy as _np
+        import pandas as _pd
+        if isinstance(coord_values, (list, tuple, _np.ndarray, _pd.Index)):
+            val_list = list(coord_values)
+        else:
+            val_list = [coord_values]
@@
-        return array.where(
-            array[coord_name].isin(val_list) if isinstance(coord_values, list) else array[coord_name] == coord_values,
-            drop=True,
-        )
+        return array.where(array[coord_name].isin(val_list) if len(val_list) > 1 else array[coord_name] == val_list[0],
+                           drop=True)

1335-1415: Overlay approach may desync facets/frames between figures.

Adding traces/frames from a separately-built figure assumes identical subplot axis ids and frame ordering. If with_plotly evolves, axis ids can diverge. Prefer plotting both flows and charge_state from one combined dataset in a single with_plotly() call, or ensure axis references are remapped when merging figures.


197-203: Minor typo in deprecation warning.

"Acess is now by ..." → "Access is now via ...".

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8323b27 and 84aa03d.

📒 Files selected for processing (10)
  • CHANGELOG.md (1 hunks)
  • examples/04_Scenarios/scenario_example.py (3 hunks)
  • flixopt/plotting.py (4 hunks)
  • flixopt/results.py (14 hunks)
  • tests/ressources/Sim1--summary.yaml (1 hunks)
  • tests/test_facet_plotting.py (1 hunks)
  • tests/test_overlay_line_on_area.py (1 hunks)
  • tests/test_plots.py (2 hunks)
  • tests/test_results_plots.py (1 hunks)
  • tests/test_select_features.py (1 hunks)
⏰ 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.11)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.13)
🔇 Additional comments (2)
tests/ressources/Sim1--summary.yaml (1)

1-92: LGTM — data fixture only.

Structured test resource; no code changes required.

tests/test_plots.py (1)

107-113: Confirm 2D heatmap axis semantics.

reshape_to_2d + dims=['timestep','timeframe'] assumes downstream heatmap helpers interpret those names consistently. If plotting.heatmap_with_* expects 'timeframe' x 'timestep' in a specific order, add an assertion or swap axes accordingly to avoid rotated plots.

Comment on lines 556 to 564
for trace in all_traces:
trace.stackgroup = variable_classification.get(trace.name, None)
# No opacity and no line for stacked areas
if trace.stackgroup is not None:
if hasattr(trace, 'line') and trace.line.color:
trace.fillcolor = trace.line.color # Will be solid by default
trace.line.width = 0

# Update layout with basic styling (Plotly Express handles sizing automatically)
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

Area stack opacity likely too strong; align visuals with intent.

You set fillcolor = trace.line.color and line.width = 0, but no opacity. This makes areas fully opaque and can hide layers. If the goal is muted/stacked areas (and per PR notes “remove line for area traces”), set a sensible opacity.

 for trace in all_traces:
     trace.stackgroup = variable_classification.get(trace.name, None)
     # No opacity and no line for stacked areas
     if trace.stackgroup is not None:
         if hasattr(trace, 'line') and trace.line.color:
-            trace.fillcolor = trace.line.color  # Will be solid by default
-            trace.line.width = 0
+            trace.fillcolor = trace.line.color
+            trace.line.width = 0
+            # Make areas translucent so overlapping stacks remain readable
+            if getattr(trace, "opacity", None) is None:
+                trace.opacity = 0.6

* 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
@FBumann FBumann marked this pull request as ready for review October 18, 2025 19:39
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

♻️ Duplicate comments (2)
flixopt/plotting.py (1)

552-563: Area traces: implement opacity and “mixed” styling as logged.

Currently fully opaque fills hide layers; and “mixed ... dashed lines” isn’t implemented.

-        for trace in all_traces:
-            trace.stackgroup = variable_classification.get(trace.name, None)
-            # No opacity and no line for stacked areas
-            if trace.stackgroup is not None:
-                if hasattr(trace, 'line') and trace.line.color:
-                    trace.fillcolor = trace.line.color  # Will be solid by default
-                    trace.line.width = 0
+        for trace in all_traces:
+            cls = variable_classification.get(trace.name, None)
+            trace.stackgroup = cls if cls in ('positive', 'negative') else None
+            if cls in ('positive', 'negative'):
+                if hasattr(trace, 'line') and trace.line.color:
+                    trace.fillcolor = trace.line.color
+                    trace.line.width = 0
+                    if getattr(trace, 'opacity', None) is None:
+                        trace.opacity = 0.6
+            elif cls == 'mixed':
+                # Show as dashed line, not stacked
+                if hasattr(trace, 'line'):
+                    trace.line.width = 2
+                    trace.line.dash = 'dash'
+                if hasattr(trace, 'fill'):
+                    trace.fill = None
flixopt/results.py (1)

1789-1871: Fix TypeError: accept and forward fill in plot_heatmap.

SegmentedCalculationResults.plot_heatmap passes fill, but module‑level plot_heatmap rejects it; this will crash.

Apply:

 def plot_heatmap(
     data: xr.DataArray | xr.Dataset,
     name: str | None = None,
     folder: pathlib.Path | None = None,
     colors: plotting.ColorType = 'viridis',
     save: bool | pathlib.Path = False,
     show: bool = True,
     engine: plotting.PlottingEngine = 'plotly',
     select: dict[str, Any] | None = None,
     facet_by: str | list[str] | None = None,
     animate_by: str | None = None,
     facet_cols: int = 3,
     reshape_time: tuple[Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'], Literal['W', 'D', 'h', '15min', 'min']]
     | Literal['auto']
     | None = 'auto',
+    fill: Literal['ffill', 'bfill'] | None = None,
     # Deprecated parameters (kept for backwards compatibility)
     indexer: dict[str, Any] | None = None,
     heatmap_timeframes: Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'] | None = None,
     heatmap_timesteps_per_frame: Literal['W', 'D', 'h', '15min', 'min'] | None = None,
     color_map: str | None = None,
 ):
@@
     if engine == 'plotly':
         figure_like = plotting.heatmap_with_plotly(
             data=data,
             facet_by=facet_by,
             animate_by=animate_by,
             colors=colors,
             title=title,
             facet_cols=facet_cols,
             reshape_time=reshape_time,
+            fill=fill,
         )
         default_filetype = '.html'
     elif engine == 'matplotlib':
         figure_like = plotting.heatmap_with_matplotlib(
             data=data,
             colors=colors,
             title=title,
             reshape_time=reshape_time,
+            fill=fill,
         )
         default_filetype = '.png'

Optional symmetry: add fill to CalculationResults.plot_heatmap and forward it.

#!/bin/bash
rg -n "def plot_heatmap\(" flixopt/results.py -n -C2
rg -n "heatmap_with_(plotly|matplotlib)\(" flixopt/plotting.py -n -C2

Also applies to: 1899-2101

🧹 Nitpick comments (2)
tests/test_heatmap_reshape.py (1)

19-30: Optional: make tests deterministic and add one assert per case.

Seed RNG for reproducibility and assert a key size/value per test (e.g., 3×24 for daily/hourly).

+np.random.seed(42)

You can also assert weekly/day labels count for ('W','D') to catch regressions.

Also applies to: 32-37, 39-51, 53-66, 68-76

flixopt/plotting.py (1)

719-889: reshape_to_2d appears unused; consider deprecating to reduce surface area.

Mark as deprecated or remove if not referenced after the new reshape path.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 84aa03d and 894533c.

📒 Files selected for processing (6)
  • .github/CONTRIBUTING.md (1 hunks)
  • flixopt/plotting.py (4 hunks)
  • flixopt/results.py (16 hunks)
  • tests/test_heatmap_reshape.py (1 hunks)
  • tests/test_plots.py (0 hunks)
  • tests/test_results_plots.py (1 hunks)
💤 Files with no reviewable changes (1)
  • tests/test_plots.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_results_plots.py
🧰 Additional context used
🧬 Code graph analysis (3)
tests/test_heatmap_reshape.py (1)
flixopt/plotting.py (1)
  • reshape_data_for_heatmap (720-888)
flixopt/plotting.py (2)
flixopt/results.py (2)
  • size (1589-1597)
  • sizes (433-460)
flixopt/flow_system.py (3)
  • coords (769-775)
  • resample (926-976)
  • isel (889-924)
flixopt/results.py (2)
flixopt/flow_system.py (3)
  • coords (769-775)
  • sel (852-887)
  • to_dataset (243-255)
flixopt/plotting.py (5)
  • with_plotly (329-577)
  • with_matplotlib (580-673)
  • export_figure (1794-1874)
  • heatmap_with_plotly (1489-1680)
  • heatmap_with_matplotlib (1683-1791)
⏰ 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.11)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.13)
🔇 Additional comments (1)
.github/CONTRIBUTING.md (1)

15-15: No issues found—all extras are properly defined.

The verification confirms that all three extras referenced in the pip install command ("full", "dev", "docs") are defined in pyproject.toml under [project.optional-dependencies]. They are in sync and no action is needed.

…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
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
flixopt/results.py (2)

1785-1785: Consider aligning color defaults across methods.

The colors default here is 'portland', while other plot methods (plot_heatmap, plot_node_balance, plot_charge_state) and the module-level plot_heatmap all default to 'viridis'. Unless 'portland' is intentionally chosen for segmented heatmap visualizations, consider standardizing on 'viridis' for consistency.


2342-2364: Optional: Consider renaming _apply_indexer_to_data for consistency.

The function name still references "indexer" but the parameter has been modernized to select. Since this is an internal function (underscore prefix), it's not critical, but renaming to _apply_select_to_data or _apply_selection_to_data would align with the new naming convention.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9c3c580 and 505edca.

📒 Files selected for processing (1)
  • flixopt/results.py (16 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
flixopt/results.py (2)
flixopt/flow_system.py (3)
  • coords (769-775)
  • sel (852-887)
  • to_dataset (243-255)
flixopt/plotting.py (5)
  • with_plotly (329-587)
  • with_matplotlib (590-683)
  • export_figure (1760-1840)
  • heatmap_with_plotly (1455-1646)
  • heatmap_with_matplotlib (1649-1757)
⏰ 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.13)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.10)
🔇 Additional comments (6)
flixopt/results.py (6)

688-801: LGTM: Well-structured API modernization with comprehensive multi-variable support.

The updated signature cleanly integrates faceting, animation, time reshaping, and multi-variable plotting. The delegation to the module-level function maintains separation of concerns, and passing deprecated parameters through for centralized handling is good design.


1040-1055: LGTM: Thorough deprecation handling and dimension safety checks.

The conflict detection between deprecated indexer and new select prevents user confusion. The matplotlib dimension check (lines 1066-1072) correctly guards against multi-dimensional data that would break the single-axis matplotlib plotting path.

Also applies to: 1066-1072


1187-1216: LGTM: Pragmatic auto-selection with clear user guidance.

The auto-selection logic for extra dimensions is a sensible solution for pie charts (which require scalar data). The informative log messages (lines 1204-1207) clearly communicate what was auto-selected and guide users to override via select if needed. The handling of dimensions that may exist in only one dataset (lines 1197-1202) is robust.


1428-1472: LGTM: Clever figure composition for dual-mode visualization.

The approach of creating separate figures for node balance and charge state, then combining their traces, elegantly handles the different visual representations (area/bar vs line). The frame combination logic (lines 1464-1471) is well-guarded, and the comment (line 1463) documents the assumption that both figures share the same animation structure.


2022-2042: LGTM: Robust multi-variable support and thorough dimension validation.

The Dataset-to-DataArray conversion (lines 2022-2030) via xr.concat along a new 'variable' dimension is the standard xarray pattern for multi-variable visualization. The matplotlib dimension checks (lines 2048-2066) comprehensively cover both time-reshaping scenarios and already-reshaped data, with clear error messages guiding users to solutions.

Also applies to: 2048-2066


2307-2313: LGTM: Explicit type handling improves robustness.

The updated normalization logic explicitly handles common sequence types (str, list, tuple, ndarray, Index) rather than relying on implicit duck-typing. This makes the function more predictable and easier to debug.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
flixopt/results.py (1)

1821-1856: Consider more robust conflict detection.

The conflict detection relies on comparing against default values (lines 1824, 1844):

if reshape_time != 'auto':  # Check if user explicitly set reshape_time
if colors != 'portland':  # Check if user explicitly set colors

This approach could fail if:

  • A user explicitly passes the default value (e.g., reshape_time='auto')
  • The default values change in the future

Consider using a sentinel value (e.g., _UNSET = object()) or tracking which parameters were explicitly provided through **kwargs.

Example refactor:

_UNSET = object()

def plot_heatmap(
    self,
    variable_name: str,
    reshape_time: ... = _UNSET,
    colors: ... = _UNSET,
    ...
):
    # Set actual defaults if not provided
    if reshape_time is _UNSET:
        reshape_time = 'auto'
    if colors is _UNSET:
        colors = 'portland'
    
    # Now check conflicts more reliably
    if (heatmap_timeframes is not None) and (reshape_time is not _UNSET):
        raise ValueError(...)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 505edca and 33c4bec.

📒 Files selected for processing (1)
  • flixopt/results.py (16 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
flixopt/results.py (1)
flixopt/plotting.py (5)
  • with_plotly (329-587)
  • with_matplotlib (590-683)
  • export_figure (1760-1840)
  • heatmap_with_plotly (1455-1646)
  • heatmap_with_matplotlib (1649-1757)
⏰ 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.11)
  • GitHub Check: test (3.10)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.13)
🔇 Additional comments (11)
flixopt/results.py (11)

20-23: LGTM: Runtime import optimization.

Moving plotly under TYPE_CHECKING reduces runtime overhead since it's only needed for type annotations.


198-202: LGTM: Clear deprecation guidance.

The updated warning clearly explains the distinction between .flow_system_data (dataset) and .flow_system (restored FlowSystem object).


688-801: LGTM: Modern plotting API with backward compatibility.

The method signature has been expanded with comprehensive faceting, animation, and time-reshaping capabilities while maintaining backward compatibility through deprecated parameters. All parameters are properly forwarded to the module-level plot_heatmap function for centralized handling.


1040-1073: LGTM: Robust deprecation handling and dimension validation.

The deprecation logic properly checks for conflicts between old (indexer) and new (select) parameters. The Matplotlib dimension check (lines 1066-1072) correctly prevents MultiIndex issues by ensuring only the time dimension remains after selection.


1187-1216: LGTM: Smart auto-selection for pie charts.

The auto-selection logic elegantly handles the constraint that pie charts require scalar data. When extra dimensions remain after aggregation, the code automatically selects the first value while logging clear guidance for users who want different selections. This provides a good balance between convenience and explicitness.


1281-1296: LGTM: Consistent deprecation handling.

The deprecation pattern matches other methods with proper conflict detection.


2022-2042: LGTM: Clean Dataset to DataArray conversion.

The conversion of Dataset to DataArray with a new variable dimension (lines 2028-2030) is straightforward and enables multi-variable heatmaps. The fallback title generation is sensible when no explicit name is provided.


2048-2066: LGTM: Thorough matplotlib constraint validation.

The dimension validation correctly handles the constraint that matplotlib heatmaps require 2D data. The logic distinguishes between:

  1. Time dimension present + reshaping enabled → no extra dimensions allowed (lines 2053-2060)
  2. No time dimension or already reshaped → maximum 2 dimensions allowed (lines 2062-2066)

Error messages provide clear guidance on resolution options.


2307-2323: LGTM: Robust type handling for coordinate filters.

The normalization logic correctly distinguishes between string values (which should not be iterated) and various sequence types. The optimization to use == for single values vs isin() for multiple values is sensible.


1428-1472: Verify frame alignment when merging Plotly traces in animation scenarios.

While both datasets are extracted from the same source with identical select() parameters (lines 1424-1426), the frame merging logic at lines 1467-1471 contains an unprotected edge case:

  • Line 1469 checks i < len(figure_like.frames) to handle charge_state_fig having more frames, but doesn't guard against it having fewer frames
  • If charge_state has fewer unique values in the animate_by dimension, the loop terminates early, leaving later animation frames without charge_state traces

Additionally, this code path is not covered by tests—the test suite explicitly disables animate_by for plot_charge_state (lines 70-71 in test_results_plots.py).

Recommendations:

  • Add a defensive check when fewer charge_state frames exist
  • Add test coverage for the animate_by path with plot_charge_state to ensure frame alignment behaves as expected

2342-2364: LGTM: Simplified selection logic.

The new _apply_selection_to_data function is cleaner than the old indexer-based approach, leveraging xarray's native .sel() method. The suffix generation provides useful context for plot titles.

Verify that the old _apply_indexer_to_data function has been completely removed:

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
flixopt/plotting.py (1)

562-579: Set opacity for stacked area traces to prevent hiding layers.

The stacked area traces set fillcolor and line.width = 0 but don't specify opacity. This makes areas fully opaque, which can hide underlying layers in the stack and reduce readability.

Apply this diff to add translucency:

             if cls in ('positive', 'negative'):
                 # Stacked area: add opacity to avoid hiding layers, remove line border
                 if hasattr(trace, 'line') and trace.line.color:
                     trace.fillcolor = trace.line.color
                     trace.line.width = 0
+                    # Make areas translucent so overlapping stacks remain readable
+                    trace.opacity = 0.6
             elif cls == 'mixed':

Note: This issue was previously flagged and remains unaddressed.

🧹 Nitpick comments (1)
flixopt/plotting.py (1)

769-773: Consider lowering the log level for auto-reshape decision.

The INFO level logging for automatic time reshaping may be too noisy in production environments where this function is called frequently. Since this is an internal decision with sensible defaults, consider using DEBUG level or making it configurable.

-            logger.info(
+            logger.debug(
                 "Auto-applying time reshaping: Only 'time' dimension remains after faceting/animation. "
                 "Using default timeframes='D' and timesteps_per_frame='h'. "
                 "To customize, use reshape_time=('D', 'h') or disable with reshape_time=None."
             )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 33c4bec and 92d0590.

📒 Files selected for processing (3)
  • CHANGELOG.md (2 hunks)
  • flixopt/plotting.py (4 hunks)
  • tests/test_results_plots.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_results_plots.py
🧰 Additional context used
🧬 Code graph analysis (1)
flixopt/plotting.py (2)
flixopt/results.py (2)
  • size (1589-1597)
  • sizes (433-460)
flixopt/flow_system.py (2)
  • coords (769-775)
  • resample (926-976)
🔇 Additional comments (4)
CHANGELOG.md (1)

57-83: LGTM! Well-structured changelog entries.

The changelog entries are clear, well-organized, and follow the project's formatting conventions. The descriptions effectively communicate user-facing changes while maintaining appropriate technical detail.

flixopt/plotting.py (3)

42-42: LGTM! Standard xarray import.

The xarray import is necessary for the new xr.DataArray/Dataset input support and follows the standard xr alias convention.


329-525: LGTM! Comprehensive signature expansion with excellent validation.

The with_plotly function has been well-extended to support xarray inputs and faceting/animation:

  • Proper type hints for the expanded input types
  • Defensive empty data checks
  • Good UX warning when fig conflicts with faceting
  • Clean xarray-to-long-form conversion handling both DataArray and Dataset
  • Robust dimension validation with appropriate logging levels
  • Clear faceting setup with proper constraints (max 2 dimensions)

1461-1763: LGTM! Well-designed heatmap functions with robust validation.

Both heatmap_with_plotly and heatmap_with_matplotlib are well-implemented:

  • Comprehensive parameter validation with clear error messages
  • Appropriate warnings for backend limitations (e.g., px.imshow facet_col constraint)
  • Good dimension validation ensuring at least 2D data for heatmap
  • Graceful fallback handling in the Plotly version
  • Excellent docstrings with practical examples
  • Proper code reuse through reshape_data_for_heatmap helper

The functions are lengthy but remain readable due to clear structure and logical flow.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
flixopt/plotting.py (2)

455-485: Consider warning level for missing dimension messages.

Lines 462-465, 472-475, and 481-484 log missing dimensions at debug level. Users might not see these messages in default logging configurations. Consider using logger.warning() instead so users are informed when their faceting/animation requests cannot be fulfilled.

-                logger.debug(
+                logger.warning(
                     f"Dimension '{facet_by}' not found in data. Available dimensions: {available_dims}. "
                     f'Ignoring facet_by parameter.'
                 )

1554-1586: Consider extracting dimension validation into a helper function.

The dimension validation logic (lines 1554-1586) is nearly identical to the validation in with_plotly (lines 455-485). Extracting this into a reusable helper function would reduce code duplication and improve maintainability.

Example:

def _validate_facet_dimensions(
    facet_by: str | list[str] | None,
    animate_by: str | None,
    available_dims: list[str]
) -> tuple[str | list[str] | None, str | None]:
    """Validate and filter facet_by and animate_by dimensions."""
    # Existing validation logic here
    return facet_by, animate_by
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 92d0590 and 55dfde9.

📒 Files selected for processing (3)
  • CHANGELOG.md (2 hunks)
  • examples/04_Scenarios/scenario_example.py (5 hunks)
  • flixopt/plotting.py (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
flixopt/plotting.py (1)
flixopt/flow_system.py (3)
  • coords (769-775)
  • resample (926-976)
  • isel (889-924)
examples/04_Scenarios/scenario_example.py (6)
flixopt/results.py (4)
  • flow_system (290-306)
  • size (1589-1597)
  • plot_charge_state (1347-1512)
  • plot_node_balance_pie (1108-1254)
flixopt/flow_system.py (1)
  • FlowSystem (46-980)
flixopt/effects.py (1)
  • Effect (32-387)
flixopt/linear_converters.py (10)
  • Boiler (24-103)
  • eta (97-98)
  • eta (101-103)
  • eta (185-186)
  • eta (189-191)
  • CHP (373-482)
  • eta_th (467-468)
  • eta_th (471-473)
  • eta_el (476-477)
  • eta_el (480-482)
flixopt/elements.py (1)
  • Flow (234-494)
flixopt/components.py (2)
  • Storage (241-529)
  • Source (1115-1219)
🔇 Additional comments (15)
examples/04_Scenarios/scenario_example.py (6)

11-12: LGTM: Timesteps extended to one week.

The time horizon extension from an unspecified period to one week (168 hours) provides a more realistic demonstration of the scenario capabilities.


17-38: LGTM: Realistic heat demand patterns with proper variation.

The heat demand generation uses realistic daily patterns with appropriate random variation (±5% for base case, ±7% for high demand). The tiling and slicing logic correctly handles the week-long simulation period.


80-84: LGTM: Scenario weights properly configured.

The scenario weights (60% Base Case, 40% High Demand) are properly structured and sum to 1.0 as expected for probability weights.


98-132: LGTM: Component parameters are realistic and valid.

The updated efficiency values (Boiler η=0.92, CHP η_th=0.48, η_el=0.40) are realistic for modern equipment. The CHP total efficiency (0.88) correctly satisfies the constraint η_th + η_el ≤ 1.0.


160-176: LGTM: Realistic gas pricing and CO2 emissions.

The gas prices (0.04, 0.06, 0.11 €/kWh for 2020-2022) realistically reflect the energy crisis escalation. The CO2 emissions factor (0.202 kg/kWh) accurately represents natural gas combustion.


204-205: LGTM: Correct usage of updated plotting API.

The plotting calls correctly use the new select parameter (line 205) replacing the deprecated indexer parameter, and leverage the default faceting behavior for charge state plotting.

CHANGELOG.md (5)

57-60: LGTM: Feature additions accurately documented.

The changelog entries correctly describe the new faceting, animation, selection, and styling features added to the plotting module.


65-67: LGTM: Behavior changes clearly documented.

The documentation correctly describes the change from implicit auto-selection to explicit select parameter usage, which is a notable change in default behavior.


70-70: LGTM: Deprecation properly documented.

The deprecation of the indexer parameter in favor of select is clearly stated, giving users notice to update their code.


75-75: LGTM: Bug fix documented.

The error handling improvement in plot_heatmap() is noted for users experiencing issues.


84-84: LGTM: Internal refactoring documented.

The function rename is properly documented in the development section for contributors who may reference internal APIs.

flixopt/plotting.py (4)

539-579: LGTM: Area classification and styling logic is sound.

The variable classification correctly distinguishes between positive-only, negative-only, and mixed (both signs) variables. Only pure positive/negative variables are stacked, while mixed variables are shown as dashed lines to avoid visual confusion. The styling (fillcolor, line width, dash pattern) appropriately differentiates between these cases.


692-861: LGTM: reshape_data_for_heatmap implements robust time reshaping logic.

The function correctly:

  • Handles auto-detection of when reshaping is needed
  • Validates datetime types and format combinations
  • Performs efficient resampling with numpy
  • Creates properly formatted labels with special handling for week patterns
  • Unstacks data into a 2D heatmap-ready format

The logic is well-structured with appropriate error handling and warnings.


1461-1653: LGTM: heatmap_with_plotly implements robust heatmap creation with faceting.

The function correctly:

  • Delegates time reshaping to reshape_data_for_heatmap
  • Validates dimensions with appropriate logging
  • Handles px.imshow's single-facet limitation gracefully
  • Provides fallback for error cases

The try/except with fallback ensures users always get output even if advanced features fail.


1655-1764: LGTM: heatmap_with_matplotlib handles 2D limitation appropriately.

The function correctly:

  • Delegates time reshaping to the unified helper
  • Handles >2 dimensions by selecting first slice with clear warning
  • Creates proper matplotlib heatmap with colorbar
  • Suggests plotly engine for advanced features

The limitation handling is transparent and helpful for users.

Comment on lines +39 to +78
# Power prices: hourly factors (night low, peak high) and period escalation (2020-2022)
hourly_price_factors = np.array(
[
0.70,
0.65,
0.62,
0.60,
0.62,
0.70,
0.95,
1.15,
1.30,
1.25,
1.10,
1.00,
0.95,
0.90,
0.88,
0.92,
1.00,
1.10,
1.25,
1.40,
1.35,
1.20,
0.95,
0.80,
]
)
power_prices = np.array([0.08, 0.09, 0.10])
period_base_prices = np.array([0.075, 0.095, 0.135]) # €/kWh for 2020, 2021, 2022

flow_system = fx.FlowSystem(timesteps=timesteps, periods=periods, scenarios=scenarios, weights=np.array([0.5, 0.6]))
price_series = np.zeros((n_hours, 3))
for period_idx, base_price in enumerate(period_base_prices):
price_series[:, period_idx] = (
np.tile(hourly_price_factors, n_hours // 24 + 1)[:n_hours]
* base_price
* (1 + np.random.uniform(-0.03, 0.03, n_hours))
)

power_prices = price_series.mean(axis=0)
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 | 🔴 Critical

Fix dimensional error in power price calculation.

Line 78 takes mean(axis=0), which averages across time (axis 0) and produces 3 scalar values (one per period). However, power_prices is later used in effects_per_flow_hour (line 181), which requires temporal data varying over time.

This collapses the hourly price variation into period averages, losing all time-of-day pricing information.

Apply this diff to preserve time-varying prices:

-    power_prices = price_series.mean(axis=0)
+    # power_prices shape: (n_hours, 3) for periods [2020, 2021, 2022]
+    power_prices = price_series

Or if period-specific prices are needed, construct a proper time series per period using the FlowSystem's period dimension.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Power prices: hourly factors (night low, peak high) and period escalation (2020-2022)
hourly_price_factors = np.array(
[
0.70,
0.65,
0.62,
0.60,
0.62,
0.70,
0.95,
1.15,
1.30,
1.25,
1.10,
1.00,
0.95,
0.90,
0.88,
0.92,
1.00,
1.10,
1.25,
1.40,
1.35,
1.20,
0.95,
0.80,
]
)
power_prices = np.array([0.08, 0.09, 0.10])
period_base_prices = np.array([0.075, 0.095, 0.135]) # €/kWh for 2020, 2021, 2022
flow_system = fx.FlowSystem(timesteps=timesteps, periods=periods, scenarios=scenarios, weights=np.array([0.5, 0.6]))
price_series = np.zeros((n_hours, 3))
for period_idx, base_price in enumerate(period_base_prices):
price_series[:, period_idx] = (
np.tile(hourly_price_factors, n_hours // 24 + 1)[:n_hours]
* base_price
* (1 + np.random.uniform(-0.03, 0.03, n_hours))
)
power_prices = price_series.mean(axis=0)
# Power prices: hourly factors (night low, peak high) and period escalation (2020-2022)
hourly_price_factors = np.array(
[
0.70,
0.65,
0.62,
0.60,
0.62,
0.70,
0.95,
1.15,
1.30,
1.25,
1.10,
1.00,
0.95,
0.90,
0.88,
0.92,
1.00,
1.10,
1.25,
1.40,
1.35,
1.20,
0.95,
0.80,
]
)
period_base_prices = np.array([0.075, 0.095, 0.135]) # €/kWh for 2020, 2021, 2022
price_series = np.zeros((n_hours, 3))
for period_idx, base_price in enumerate(period_base_prices):
price_series[:, period_idx] = (
np.tile(hourly_price_factors, n_hours // 24 + 1)[:n_hours]
* base_price
* (1 + np.random.uniform(-0.03, 0.03, n_hours))
)
# power_prices shape: (n_hours, 3) for periods [2020, 2021, 2022]
power_prices = price_series
🤖 Prompt for AI Agents
In examples/04_Scenarios/scenario_example.py around lines 39-78, the code
collapses the hourly price_series to 3 period averages by calling
price_series.mean(axis=0); this removes hourly variation required later by
effects_per_flow_hour. Preserve the time-varying series instead: keep
power_prices as the full price_series (n_hours x periods) so downstream code
receives per-hour prices, or if a single hourly vector is required choose the
appropriate period column (e.g., price_series[:, period_index]) matched to the
FlowSystem period dimension; update any downstream calls that expect the old
shape accordingly.

Comment on lines +146 to +149
relative_maximum_final_charge_state=np.array([0.8, 0.5, 0.1]),
eta_charge=0.95, # Realistic charging efficiency (~95%)
eta_discharge=0.98, # Realistic discharging efficiency (~98%)
relative_loss_per_hour=np.array([0.008, 0.015]), # Realistic thermal losses: 0.8-1.5% per hour
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 | 🔴 Critical

🧩 Analysis chain

Verify dimensional alignment of storage parameter arrays.

The arrays have dimensions that suggest specific mappings:

  • relative_maximum_final_charge_state (3 values) likely maps to 3 periods
  • relative_loss_per_hour (2 values) likely maps to 2 scenarios

Ensure this dimensional mapping is intentional and aligns with how the FlowSystem will interpret these parameters.

Run the following script to confirm the dimensional interpretation:


🏁 Script executed:

#!/bin/bash
# Verify how Storage parameters handle array inputs and their dimensional mapping

# Check Storage class parameter documentation
ast-grep --pattern $'class Storage($$$):
  $$$
  def __init__(
    $$$,
    relative_maximum_final_charge_state$$$,
    $$$,
    relative_loss_per_hour$$$,
    $$$
  ):
    $$$'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find Storage class definition
echo "=== Searching for Storage class ==="
rg -n "class Storage" --type=py -A 20

echo -e "\n=== Searching for relative_maximum_final_charge_state in Storage context ==="
rg -n "relative_maximum_final_charge_state" --type=py -B 5 -A 2

echo -e "\n=== Searching for relative_loss_per_hour in Storage context ==="
rg -n "relative_loss_per_hour" --type=py -B 5 -A 2

Length of output: 27465


🏁 Script executed:

#!/bin/bash
# Verify periods and scenarios count in scenario_example.py
echo "=== Searching for periods definition in scenario_example.py ==="
rg -n "periods|scenarios" examples/04_Scenarios/scenario_example.py -B 2 -A 1 | head -40

echo -e "\n=== Getting FlowSystem setup context ==="
rg -n "FlowSystem" examples/04_Scenarios/scenario_example.py -A 15 | head -50

Length of output: 1603


🏁 Script executed:

#!/bin/bash
# Find fit_to_model_coords implementation to understand array handling
echo "=== Searching for fit_to_model_coords definition ==="
rg -n "def fit_to_model_coords" --type=py -A 30

echo -e "\n=== Check for any validation of array shapes ==="
rg -n "relative_loss_per_hour|relative_maximum_final_charge_state" flixopt/components.py -B 2 -A 2 | grep -A 5 "fit_to_model_coords"

Length of output: 2808


Fix array dimensions to match expected parameter shapes in Storage component.

The storage parameters have dimensional mismatches that will cause failures or unexpected behavior:

  1. relative_maximum_final_charge_state=np.array([0.8, 0.5, 0.1]) — Provided 3 values but the framework expects shape matching dims=['period', 'scenario'] (i.e., 3×2=6 values or a scalar). The 3 values align only with periods, missing the scenario dimension.

  2. relative_loss_per_hour=np.array([0.008, 0.015]) — Provided 2 values for a TemporalDataUser parameter that defaults to all model dimensions (time, period, scenario). Expected shape is 168×3×2 or scalar. The 2 values only match scenarios.

Solutions:

  • Use scalar values: relative_maximum_final_charge_state=0.8 and relative_loss_per_hour=0.008
  • Or reshape arrays to match full expected dimensions with proper broadcasting semantics
🤖 Prompt for AI Agents
In examples/04_Scenarios/scenario_example.py around lines 146 to 149, the
Storage parameters use arrays with incorrect dimensions:
relative_maximum_final_charge_state is a length-3 array (periods) but the
Storage expects dims=['period','scenario'] (3×2) or a scalar, and
relative_loss_per_hour is length-2 (scenarios) but TemporalDataUser expects full
time×period×scenario shape (168×3×2) or a scalar; fix by replacing these with
scalars (e.g., relative_maximum_final_charge_state=0.8 and
relative_loss_per_hour=0.008) or construct arrays with the full expected shape
(or broadcastable shape) matching model dimensions (time, period, scenario) so
shapes align with the Storage component requirements.

Comment on lines +412 to +453
# Convert xarray to long-form DataFrame for Plotly Express
if isinstance(data, (xr.DataArray, xr.Dataset)):
# Convert to long-form (tidy) DataFrame
# Structure: time, variable, value, scenario, period, ... (all dims as columns)
if isinstance(data, xr.Dataset):
# Stack all data variables into long format
df_long = data.to_dataframe().reset_index()
# Melt to get: time, scenario, period, ..., variable, value
id_vars = [dim for dim in data.dims]
value_vars = list(data.data_vars)
df_long = df_long.melt(id_vars=id_vars, value_vars=value_vars, var_name='variable', value_name='value')
else:
# DataArray
df_long = data.to_dataframe().reset_index()
if data.name:
df_long = df_long.rename(columns={data.name: 'value'})
else:
# Unnamed DataArray, find the value column
non_dim_cols = [col for col in df_long.columns if col not in data.dims]
if len(non_dim_cols) != 1:
raise ValueError(
f'Expected exactly one non-dimension column for unnamed DataArray, '
f'but found {len(non_dim_cols)}: {non_dim_cols}'
)
value_col = non_dim_cols[0]
df_long = df_long.rename(columns={value_col: 'value'})
df_long['variable'] = data.name or 'data'
else:
# Already a DataFrame - convert to long format for Plotly Express
df_long = data.reset_index()
if 'time' not in df_long.columns:
# First column is probably time
df_long = df_long.rename(columns={df_long.columns[0]: 'time'})
# Melt to long format
id_vars = [
col
for col in df_long.columns
if col in ['time', 'scenario', 'period']
or col in (facet_by if isinstance(facet_by, list) else [facet_by] if facet_by else [])
]
value_vars = [col for col in df_long.columns if col not in id_vars]
df_long = df_long.melt(id_vars=id_vars, value_vars=value_vars, var_name='variable', value_name='value')
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 | 🟡 Minor

Consider edge cases in DataFrame dimension detection.

Lines 446-452 attempt to identify dimension columns for DataFrame inputs by checking for known names ('time', 'scenario', 'period') and facet_by dimensions. This heuristic may incorrectly classify columns for DataFrames with:

  • Custom dimension names (e.g., 'location', 'technology')
  • Columns named like dimensions but containing values

This is acceptable for typical usage but could cause issues with non-standard DataFrame structures.

Consider documenting the expected DataFrame structure or adding a parameter to explicitly specify dimension columns.

🤖 Prompt for AI Agents
In flixopt/plotting.py around lines 412-453, the heuristic that picks id_vars
for DataFrame inputs (currently only looking for 'time','scenario','period' and
facet_by) is brittle for non-standard DataFrames; add an optional parameter
(e.g., dims or id_vars) to the plotting function so callers can explicitly pass
the columns to treat as dimension/index columns, and update the logic to: use
the provided dims/id_vars if given; otherwise fall back to the current heuristic
(ensure facet_by is normalized to a list first), and if the heuristic finds
nothing, default to treating the first column as 'time' and warn (or raise) to
prompt explicit dims. Also update the docstring to describe the expected
DataFrame shape and the new parameter so users with custom dimension names can
opt in.

…ceting-and-animations

# Conflicts:
#	CHANGELOG.md
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
CHANGELOG.md (1)

67-67: Make the scenario_example.py improvements more specific.

Line 67 states "Improved scenario_example.py" but lacks detail. The PR objectives mention "Adjusts frame size; removes height-related code" and other changes. Provide concrete examples of what was improved so users understand the changes.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 55dfde9 and 9cef222.

📒 Files selected for processing (1)
  • CHANGELOG.md (2 hunks)
⏰ 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)
🔇 Additional comments (2)
CHANGELOG.md (2)

65-65: Clarify whether the selection behavior change qualifies as a breaking change.

Line 65 states that "no longer automatically selects first value for non-time dimensions" — a behavioral change that could break existing code relying on the old auto-selection behavior. This appears to be listed under "Changed" rather than "Breaking Changes" (line 62). Verify this categorization is intentional and will not surprise users upgrading.


73-73: Document removals in the "Removed" section.

The PR objectives mention "removes height-related code and removes an old class," but the "Removed" section (line 73) is empty. If these are user-facing or could impact existing code, document them explicitly here. If they are internal-only, consider mentioning in the Development section instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants