diff --git a/flixopt/clustering.py b/flixopt/clustering.py index 1c6f7511b..1595ace5d 100644 --- a/flixopt/clustering.py +++ b/flixopt/clustering.py @@ -7,7 +7,6 @@ import copy import logging -import pathlib import timeit from typing import TYPE_CHECKING @@ -29,6 +28,8 @@ ) if TYPE_CHECKING: + import pathlib + import linopy import pandas as pd import plotly.graph_objects as go @@ -145,7 +146,7 @@ def use_extreme_periods(self): return self.time_series_for_high_peaks or self.time_series_for_low_peaks def plot(self, colormap: str | None = None, show: bool = True, save: pathlib.Path | None = None) -> go.Figure: - from . import plotting + import plotly.express as px df_org = self.original_data.copy().rename( columns={col: f'Original - {col}' for col in self.original_data.columns} @@ -156,10 +157,17 @@ def plot(self, colormap: str | None = None, show: bool = True, save: pathlib.Pat colors = list( process_colors(colormap or CONFIG.Plotting.default_qualitative_colorscale, list(df_org.columns)).values() ) - fig = plotting.with_plotly(df_org.to_xarray(), 'line', colors=colors, xlabel='Time in h') + + # Create line plot for original data (dashed) + index_name = df_org.index.name or 'index' + df_org_long = df_org.reset_index().melt(id_vars=index_name, var_name='variable', value_name='value') + fig = px.line(df_org_long, x=index_name, y='value', color='variable', color_discrete_sequence=colors) for trace in fig.data: - trace.update(dict(line=dict(dash='dash'))) - fig2 = plotting.with_plotly(df_agg.to_xarray(), 'line', colors=colors, xlabel='Time in h') + trace.update(line=dict(dash='dash')) + + # Add aggregated data (solid lines) + df_agg_long = df_agg.reset_index().melt(id_vars=index_name, var_name='variable', value_name='value') + fig2 = px.line(df_agg_long, x=index_name, y='value', color='variable', color_discrete_sequence=colors) for trace in fig2.data: fig.add_trace(trace) @@ -169,14 +177,10 @@ def plot(self, colormap: str | None = None, show: bool = True, save: pathlib.Pat yaxis_title='Value', ) - plotting.export_figure( - figure_like=fig, - default_path=pathlib.Path('aggregated data.html'), - default_filetype='.html', - user_path=save, - show=show, - save=save is not None, - ) + if save is not None: + fig.write_html(str(save)) + if show: + fig.show() return fig diff --git a/flixopt/color_processing.py b/flixopt/color_processing.py index 2959acc82..f6e9a3b9f 100644 --- a/flixopt/color_processing.py +++ b/flixopt/color_processing.py @@ -15,6 +15,57 @@ logger = logging.getLogger('flixopt') +# Type alias for flexible color input +ColorType = str | list[str] | dict[str, str] +"""Flexible color specification type supporting multiple input formats for visualization. + +Color specifications can take several forms to accommodate different use cases: + +**Named colorscales** (str): + - Standard colorscales: 'turbo', 'plasma', 'cividis', 'tab10', 'Set1' + - Energy-focused: 'portland' (custom flixopt colorscale for energy systems) + - Backend-specific maps available in Plotly and Matplotlib + +**Color Lists** (list[str]): + - Explicit color sequences: ['red', 'blue', 'green', 'orange'] + - HEX codes: ['#FF0000', '#0000FF', '#00FF00', '#FFA500'] + - Mixed formats: ['red', '#0000FF', 'green', 'orange'] + +**Label-to-Color Mapping** (dict[str, str]): + - Explicit associations: {'Wind': 'skyblue', 'Solar': 'gold', 'Gas': 'brown'} + - Ensures consistent colors across different plots and datasets + - Ideal for energy system components with semantic meaning + +Examples: + ```python + # Named colorscale + colors = 'turbo' # Automatic color generation + + # Explicit color list + colors = ['red', 'blue', 'green', '#FFD700'] + + # Component-specific mapping + colors = { + 'Wind_Turbine': 'skyblue', + 'Solar_Panel': 'gold', + 'Natural_Gas': 'brown', + 'Battery': 'green', + 'Electric_Load': 'darkred' + } + ``` + +Color Format Support: + - **Named Colors**: 'red', 'blue', 'forestgreen', 'darkorange' + - **HEX Codes**: '#FF0000', '#0000FF', '#228B22', '#FF8C00' + - **RGB Tuples**: (255, 0, 0), (0, 0, 255) [Matplotlib only] + - **RGBA**: 'rgba(255,0,0,0.8)' [Plotly only] + +References: + - HTML Color Names: https://htmlcolorcodes.com/color-names/ + - Matplotlib colorscales: https://matplotlib.org/stable/tutorials/colors/colorscales.html + - Plotly Built-in Colorscales: https://plotly.com/python/builtin-colorscales/ +""" + def _rgb_string_to_hex(color: str) -> str: """Convert Plotly RGB/RGBA string format to hex. diff --git a/flixopt/plotting.py b/flixopt/plotting.py index db78ca19b..db5a3eb5c 100644 --- a/flixopt/plotting.py +++ b/flixopt/plotting.py @@ -39,7 +39,7 @@ import plotly.offline import xarray as xr -from .color_processing import process_colors +from .color_processing import ColorType, process_colors from .config import CONFIG if TYPE_CHECKING: @@ -66,56 +66,6 @@ plt.register_cmap(name='portland', cmap=mcolors.LinearSegmentedColormap.from_list('portland', _portland_colors)) -ColorType = str | list[str] | dict[str, str] -"""Flexible color specification type supporting multiple input formats for visualization. - -Color specifications can take several forms to accommodate different use cases: - -**Named colorscales** (str): - - Standard colorscales: 'turbo', 'plasma', 'cividis', 'tab10', 'Set1' - - Energy-focused: 'portland' (custom flixopt colorscale for energy systems) - - Backend-specific maps available in Plotly and Matplotlib - -**Color Lists** (list[str]): - - Explicit color sequences: ['red', 'blue', 'green', 'orange'] - - HEX codes: ['#FF0000', '#0000FF', '#00FF00', '#FFA500'] - - Mixed formats: ['red', '#0000FF', 'green', 'orange'] - -**Label-to-Color Mapping** (dict[str, str]): - - Explicit associations: {'Wind': 'skyblue', 'Solar': 'gold', 'Gas': 'brown'} - - Ensures consistent colors across different plots and datasets - - Ideal for energy system components with semantic meaning - -Examples: - ```python - # Named colorscale - colors = 'turbo' # Automatic color generation - - # Explicit color list - colors = ['red', 'blue', 'green', '#FFD700'] - - # Component-specific mapping - colors = { - 'Wind_Turbine': 'skyblue', - 'Solar_Panel': 'gold', - 'Natural_Gas': 'brown', - 'Battery': 'green', - 'Electric_Load': 'darkred' - } - ``` - -Color Format Support: - - **Named Colors**: 'red', 'blue', 'forestgreen', 'darkorange' - - **HEX Codes**: '#FF0000', '#0000FF', '#228B22', '#FF8C00' - - **RGB Tuples**: (255, 0, 0), (0, 0, 255) [Matplotlib only] - - **RGBA**: 'rgba(255,0,0,0.8)' [Plotly only] - -References: - - HTML Color Names: https://htmlcolorcodes.com/color-names/ - - Matplotlib colorscales: https://matplotlib.org/stable/tutorials/colors/colorscales.html - - Plotly Built-in Colorscales: https://plotly.com/python/builtin-colorscales/ -""" - PlottingEngine = Literal['plotly', 'matplotlib'] """Identifier for the plotting engine to use.""" diff --git a/flixopt/statistics_accessor.py b/flixopt/statistics_accessor.py index 9afcfd284..9f6bb01be 100644 --- a/flixopt/statistics_accessor.py +++ b/flixopt/statistics_accessor.py @@ -26,10 +26,11 @@ import numpy as np import pandas as pd +import plotly.express as px import plotly.graph_objects as go import xarray as xr -from . import plotting +from .color_processing import ColorType, process_colors from .config import CONFIG if TYPE_CHECKING: @@ -47,6 +48,124 @@ """For include/exclude filtering: 'Boiler' or ['Boiler', 'CHP']""" +def _reshape_time_for_heatmap( + data: xr.DataArray, + reshape: tuple[str, str], + fill: Literal['ffill', 'bfill'] | None = 'ffill', +) -> xr.DataArray: + """Reshape time dimension into 2D (timeframe × timestep) for heatmap display. + + Args: + data: DataArray with 'time' dimension. + reshape: Tuple of (outer_freq, inner_freq), e.g. ('D', 'h') for days × hours. + fill: Method to fill missing values after resampling. + + Returns: + DataArray with 'time' replaced by 'timestep' and 'timeframe' dimensions. + """ + if 'time' not in data.dims: + return data + + timeframes, timesteps_per_frame = reshape + + # Define formats for different combinations + formats = { + ('YS', 'W'): ('%Y', '%W'), + ('YS', 'D'): ('%Y', '%j'), + ('YS', 'h'): ('%Y', '%j %H:00'), + ('MS', 'D'): ('%Y-%m', '%d'), + ('MS', 'h'): ('%Y-%m', '%d %H:00'), + ('W', 'D'): ('%Y-w%W', '%w_%A'), + ('W', 'h'): ('%Y-w%W', '%w_%A %H:00'), + ('D', 'h'): ('%Y-%m-%d', '%H:00'), + ('D', '15min'): ('%Y-%m-%d', '%H:%M'), + ('h', '15min'): ('%Y-%m-%d %H:00', '%M'), + ('h', 'min'): ('%Y-%m-%d %H:00', '%M'), + } + + format_pair = (timeframes, timesteps_per_frame) + if format_pair not in formats: + raise ValueError(f'{format_pair} is not a valid format. Choose from {list(formats.keys())}') + period_format, step_format = formats[format_pair] + + # Resample along time dimension + resampled = data.resample(time=timesteps_per_frame).mean() + + # Apply fill if specified + if fill == 'ffill': + resampled = resampled.ffill(dim='time') + elif fill == 'bfill': + resampled = resampled.bfill(dim='time') + + # Create period and step labels + time_values = pd.to_datetime(resampled.coords['time'].values) + period_labels = time_values.strftime(period_format) + step_labels = time_values.strftime(step_format) + + # Handle special case for weekly day format + if '%w_%A' in step_format: + step_labels = pd.Series(step_labels).replace('0_Sunday', '7_Sunday').values + + # Add period and step as coordinates + resampled = resampled.assign_coords({'timeframe': ('time', period_labels), 'timestep': ('time', step_labels)}) + + # Convert to multi-index and unstack + resampled = resampled.set_index(time=['timeframe', 'timestep']) + result = resampled.unstack('time') + + # Reorder: timestep, timeframe, then other dimensions + other_dims = [d for d in result.dims if d not in ['timestep', 'timeframe']] + return result.transpose('timestep', 'timeframe', *other_dims) + + +def _heatmap_figure( + data: xr.DataArray, + colors: str | list[str] | None = None, + title: str = '', + facet_col: str | None = None, + animation_frame: str | None = None, + facet_col_wrap: int | None = None, + **imshow_kwargs: Any, +) -> go.Figure: + """Create heatmap figure using px.imshow. + + Args: + data: DataArray with 2-4 dimensions. First two are heatmap axes. + colors: Colorscale name (str) or list of colors. Dicts are not supported + for heatmaps as color_continuous_scale requires a colorscale specification. + title: Plot title. + facet_col: Dimension for subplot columns. + animation_frame: Dimension for animation slider. + facet_col_wrap: Max columns before wrapping. + **imshow_kwargs: Additional args for px.imshow. + + Returns: + Plotly Figure. + """ + if data.size == 0: + return go.Figure() + + colors = colors or CONFIG.Plotting.default_sequential_colorscale + facet_col_wrap = facet_col_wrap or CONFIG.Plotting.default_facet_cols + + imshow_args: dict[str, Any] = { + 'img': data, + 'color_continuous_scale': colors, + 'title': title, + **imshow_kwargs, + } + + if facet_col and facet_col in data.dims: + imshow_args['facet_col'] = facet_col + if facet_col_wrap < data.sizes[facet_col]: + imshow_args['facet_col_wrap'] = facet_col_wrap + + if animation_frame and animation_frame in data.dims: + imshow_args['animation_frame'] = animation_frame + + return px.imshow(**imshow_args) + + @dataclass class PlotResult: """Container returned by all plot methods. Holds both data and figure. @@ -150,21 +269,19 @@ def _dataset_to_long_df(ds: xr.Dataset, value_name: str = 'value', var_name: str def _create_stacked_bar( ds: xr.Dataset, - colors: dict[str, str] | None, + colors: ColorType, title: str, facet_col: str | None, facet_row: str | None, **plotly_kwargs: Any, ) -> go.Figure: """Create a stacked bar chart from xarray Dataset.""" - import plotly.express as px - df = _dataset_to_long_df(ds) if df.empty: return go.Figure() x_col = 'time' if 'time' in df.columns else df.columns[0] variables = df['variable'].unique().tolist() - color_map = {var: colors.get(var) for var in variables if colors and var in colors} or None + color_map = process_colors(colors, variables, default_colorscale=CONFIG.Plotting.default_qualitative_colorscale) fig = px.bar( df, x=x_col, @@ -183,21 +300,19 @@ def _create_stacked_bar( def _create_line( ds: xr.Dataset, - colors: dict[str, str] | None, + colors: ColorType, title: str, facet_col: str | None, facet_row: str | None, **plotly_kwargs: Any, ) -> go.Figure: """Create a line chart from xarray Dataset.""" - import plotly.express as px - df = _dataset_to_long_df(ds) if df.empty: return go.Figure() x_col = 'time' if 'time' in df.columns else df.columns[0] variables = df['variable'].unique().tolist() - color_map = {var: colors.get(var) for var in variables if colors and var in colors} or None + color_map = process_colors(colors, variables, default_colorscale=CONFIG.Plotting.default_qualitative_colorscale) return px.line( df, x=x_col, @@ -631,7 +746,7 @@ def balance( include: FilterType | None = None, exclude: FilterType | None = None, unit: Literal['flow_rate', 'flow_hours'] = 'flow_rate', - colors: dict[str, str] | None = None, + colors: ColorType | None = None, facet_col: str | None = 'period', facet_row: str | None = 'scenario', show: bool | None = None, @@ -645,7 +760,7 @@ def balance( include: Only include flows containing these substrings. exclude: Exclude flows containing these substrings. unit: 'flow_rate' (power) or 'flow_hours' (energy). - colors: Color overrides for flows. + colors: Color specification (colorscale name, color list, or label-to-color dict). facet_col: Dimension for column facets. facet_row: Dimension for row facets. show: Whether to display the plot. @@ -708,7 +823,7 @@ def heatmap( *, select: SelectType | None = None, reshape: tuple[str, str] | None = ('D', 'h'), - colorscale: str = 'viridis', + colors: str | list[str] | None = None, facet_col: str | None = 'period', animation_frame: str | None = 'scenario', show: bool | None = None, @@ -725,7 +840,8 @@ def heatmap( select: xarray-style selection, e.g. {'scenario': 'Base Case'}. reshape: Time reshape frequencies as (outer, inner), e.g. ('D', 'h') for days × hours. Set to None to disable reshaping. - colorscale: Plotly colorscale name. + colors: Colorscale name (str) or list of colors for heatmap coloring. + Dicts are not supported for heatmaps (use str or list[str]). facet_col: Dimension for subplot columns (default: 'period'). With multiple variables, 'variable' is used instead. animation_frame: Dimension for animation slider (default: 'scenario'). @@ -771,7 +887,7 @@ def heatmap( # Reshape time only if we wouldn't lose data (all extra dims fit in facet + animation) if reshape and 'time' in da.dims and not would_drop: - da = plotting.reshape_data_for_heatmap(da, reshape) + da = _reshape_time_for_heatmap(da, reshape) heatmap_dims = ['timestep', 'timeframe'] elif has_multiple_vars: # Can't reshape but have multiple vars: use variable + time as heatmap axes @@ -797,9 +913,9 @@ def heatmap( if has_multiple_vars: da = da.rename('') - fig = plotting.heatmap_with_plotly_v2( + fig = _heatmap_figure( da, - colors=colorscale, + colors=colors, facet_col=actual_facet, animation_frame=actual_animation, **plotly_kwargs, @@ -821,7 +937,7 @@ def flows( component: str | list[str] | None = None, select: SelectType | None = None, unit: Literal['flow_rate', 'flow_hours'] = 'flow_rate', - colors: dict[str, str] | None = None, + colors: ColorType | None = None, facet_col: str | None = 'period', facet_row: str | None = 'scenario', show: bool | None = None, @@ -835,7 +951,7 @@ def flows( component: Filter by parent component(s). select: xarray-style selection. unit: 'flow_rate' or 'flow_hours'. - colors: Color overrides. + colors: Color specification (colorscale name, color list, or label-to-color dict). facet_col: Dimension for column facets. facet_row: Dimension for row facets. show: Whether to display. @@ -904,7 +1020,7 @@ def sankey( timestep: int | str | None = None, aggregate: Literal['sum', 'mean'] = 'sum', select: SelectType | None = None, - colors: dict[str, str] | None = None, + colors: ColorType | None = None, show: bool | None = None, **plotly_kwargs: Any, ) -> PlotResult: @@ -914,7 +1030,7 @@ def sankey( timestep: Specific timestep to show, or None for aggregation. aggregate: How to aggregate if timestep is None. select: xarray-style selection. - colors: Color overrides for flows/nodes. + colors: Color specification for nodes (colorscale name, color list, or label-to-color dict). show: Whether to display. Returns: @@ -979,11 +1095,8 @@ def sankey( node_list = list(nodes) node_indices = {n: i for i, n in enumerate(node_list)} - node_colors = [colors.get(node) if colors else None for node in node_list] - if any(node_colors): - node_colors = [c if c else 'lightgray' for c in node_colors] - else: - node_colors = None + color_map = process_colors(colors, node_list) + node_colors = [color_map[node] for node in node_list] fig = go.Figure( data=[ @@ -1019,7 +1132,7 @@ def sizes( *, max_size: float | None = 1e6, select: SelectType | None = None, - colors: dict[str, str] | None = None, + colors: ColorType | None = None, facet_col: str | None = 'period', facet_row: str | None = 'scenario', show: bool | None = None, @@ -1030,7 +1143,7 @@ def sizes( Args: max_size: Maximum size to include (filters defaults). select: xarray-style selection. - colors: Color overrides. + colors: Color specification (colorscale name, color list, or label-to-color dict). facet_col: Dimension for column facets. facet_row: Dimension for row facets. show: Whether to display. @@ -1038,8 +1151,6 @@ def sizes( Returns: PlotResult with size data. """ - import plotly.express as px - self._stats._require_solution() ds = self._stats.sizes @@ -1056,7 +1167,7 @@ def sizes( fig = go.Figure() else: variables = df['variable'].unique().tolist() - color_map = {var: colors.get(var) for var in variables if colors and var in colors} or None + color_map = process_colors(colors, variables) fig = px.bar( df, x='variable', @@ -1083,7 +1194,7 @@ def duration_curve( *, select: SelectType | None = None, normalize: bool = False, - colors: dict[str, str] | None = None, + colors: ColorType | None = None, facet_col: str | None = 'period', facet_row: str | None = 'scenario', show: bool | None = None, @@ -1096,7 +1207,7 @@ def duration_curve( Uses flow_rates from statistics. select: xarray-style selection. normalize: If True, normalize x-axis to 0-100%. - colors: Color overrides. + colors: Color specification (colorscale name, color list, or label-to-color dict). facet_col: Dimension for column facets. facet_row: Dimension for row facets. show: Whether to display. @@ -1162,7 +1273,7 @@ def effects( effect: str | None = None, by: Literal['component', 'contributor', 'time'] = 'component', select: SelectType | None = None, - colors: dict[str, str] | None = None, + colors: ColorType | None = None, facet_col: str | None = 'period', facet_row: str | None = 'scenario', show: bool | None = None, @@ -1176,7 +1287,7 @@ def effects( If None, plots all effects. by: Group by 'component', 'contributor' (individual flows), or 'time'. select: xarray-style selection. - colors: Override colors. + colors: Color specification (colorscale name, color list, or label-to-color dict). facet_col: Dimension for column facets (ignored if not in data). facet_row: Dimension for row facets (ignored if not in data). show: Whether to display. @@ -1190,8 +1301,6 @@ def effects( >>> flow_system.statistics.plot.effects(by='contributor') # By individual flows >>> flow_system.statistics.plot.effects(aspect='temporal', by='time') # Over time """ - import plotly.express as px - self._stats._require_solution() # Get the appropriate effects dataset based on aspect @@ -1267,7 +1376,7 @@ def effects( # Build color map if color_col and color_col in df.columns: color_items = df[color_col].unique().tolist() - color_map = {item: colors.get(item) for item in color_items if colors and item in colors} or None + color_map = process_colors(colors, color_items) else: color_map = None diff --git a/flixopt/topology_accessor.py b/flixopt/topology_accessor.py index 0df05afa2..de4f83685 100644 --- a/flixopt/topology_accessor.py +++ b/flixopt/topology_accessor.py @@ -8,13 +8,12 @@ from __future__ import annotations import logging +import pathlib import warnings from itertools import chain from typing import TYPE_CHECKING, Literal if TYPE_CHECKING: - import pathlib - import pyvis from .flow_system import FlowSystem @@ -22,6 +21,84 @@ logger = logging.getLogger('flixopt') +def _plot_network( + node_infos: dict, + edge_infos: dict, + path: str | pathlib.Path | None = None, + controls: bool + | list[ + Literal['nodes', 'edges', 'layout', 'interaction', 'manipulation', 'physics', 'selection', 'renderer'] + ] = True, + show: bool = False, +) -> pyvis.network.Network | None: + """Visualize network structure using PyVis. + + Args: + node_infos: Dictionary of node information. + edge_infos: Dictionary of edge information. + path: Path to save HTML visualization. + controls: UI controls to add. True for all, or list of specific controls. + show: Whether to open in browser. + + Returns: + Network instance, or None if pyvis not installed. + """ + try: + from pyvis.network import Network + except ImportError: + logger.critical("Plotting the flow system network was not possible. Please install pyvis: 'pip install pyvis'") + return None + + net = Network(directed=True, height='100%' if controls is False else '800px', font_color='white') + + for node_id, node in node_infos.items(): + net.add_node( + node_id, + label=node['label'], + shape={'Bus': 'circle', 'Component': 'box'}[node['class']], + color={'Bus': '#393E46', 'Component': '#00ADB5'}[node['class']], + title=node['infos'].replace(')', '\n)'), + font={'size': 14}, + ) + + for edge in edge_infos.values(): + net.add_edge( + edge['start'], + edge['end'], + label=edge['label'], + title=edge['infos'].replace(')', '\n)'), + font={'color': '#4D4D4D', 'size': 14}, + color='#222831', + ) + + net.barnes_hut(central_gravity=0.8, spring_length=50, spring_strength=0.05, gravity=-10000) + + if controls: + net.show_buttons(filter_=controls) + if not show and not path: + return net + elif path: + path = pathlib.Path(path) if isinstance(path, str) else path + net.write_html(path.as_posix()) + elif show: + path = pathlib.Path('network.html') + net.write_html(path.as_posix()) + + if show: + try: + import webbrowser + + worked = webbrowser.open(f'file://{path.resolve()}', 2) + if not worked: + logger.error(f'Showing the network in the Browser went wrong. Open it manually. Its saved under {path}') + except Exception as e: + logger.error( + f'Showing the network in the Browser went wrong. Open it manually. Its saved under {path}: {e}' + ) + + return net + + class TopologyAccessor: """ Accessor for network topology inspection and visualization on FlowSystem. @@ -136,12 +213,17 @@ def plot( Nodes are styled based on type (circles for buses, boxes for components) and annotated with node information. """ - from . import plotting from .config import CONFIG node_infos, edge_infos = self.infos() - return plotting.plot_network( - node_infos, edge_infos, path, controls, show if show is not None else CONFIG.Plotting.default_show + # Normalize path=False to None for _plot_network compatibility + normalized_path = None if path is False else path + return _plot_network( + node_infos, + edge_infos, + normalized_path, + controls, + show if show is not None else CONFIG.Plotting.default_show, ) def start_app(self) -> None: diff --git a/tests/test_solution_and_plotting.py b/tests/test_solution_and_plotting.py index e302c4267..e5c96da33 100644 --- a/tests/test_solution_and_plotting.py +++ b/tests/test_solution_and_plotting.py @@ -349,10 +349,10 @@ def test_reshape_none_preserves_data(self, long_time_data): def test_heatmap_with_plotly_v2(self, long_time_data): """Test heatmap plotting with Plotly.""" - # Convert to Dataset for plotting - data = long_time_data.to_dataset(name='power') + # Reshape data first (heatmap_with_plotly_v2 requires pre-reshaped data) + reshaped = plotting.reshape_data_for_heatmap(long_time_data, reshape_time=('D', 'h')) - fig = plotting.heatmap_with_plotly_v2(data['power'], reshape_time=('D', 'h')) + fig = plotting.heatmap_with_plotly_v2(reshaped) assert fig is not None def test_heatmap_with_matplotlib(self, long_time_data):