Skip to content

TimeSeries Dashboard (Plotly Figure)

TimeSeriesDashboardGenerator

Main class for generating timeseries heatmap dashboards.

Creates comprehensive dashboards that visualize timeseries data as heatmaps with hour-of-day on y-axis and time aggregations on x-axis. Supports faceting by data columns (MultiIndex) or analysis parameters, customizable KPI statistics, and flexible color schemes including per-facet colorscales.

The dashboard displays heatmaps alongside statistical summaries and supports various time aggregations (daily, weekly, monthly) with configurable grouping functions (mean, sum, min, max, etc.).

The input data must be a pandas DataFrame or Series with:
  • DateTime index (required for time-based aggregations)
  • For faceting: MultiIndex columns with named levels

Examples:

Basic usage with single variable:

>>> import pandas as pd
>>> import numpy as np
>>> from datetime import datetime, timedelta
>>> 
>>> # Create sample timeseries data
>>> dates = pd.date_range('2023-01-01', periods=8760, freq='H')
>>> data = pd.Series(np.random.randn(8760), index=dates, name='power')
>>> 
>>> # Generate basic dashboard
>>> generator = TimeSeriesDashboardGenerator(x_axis='date')
>>> fig = generator.get_figure(data)
>>> fig.show()

Multi-variable with faceting:

>>> # Create multi-column data with proper MultiIndex
>>> variables = ['solar', 'wind', 'load']
>>> scenarios = ['base', 'high', 'low']
>>> 
>>> # Method 1: Using pd.concat to create MultiIndex
>>> data_dict = {}
>>> for scenario in scenarios:
>>>     scenario_data = pd.DataFrame({
>>>         var: np.random.randn(8760) for var in variables
>>>     }, index=dates)
>>>     data_dict[scenario] = scenario_data
>>> 
>>> data_multi = pd.concat(data_dict, axis=1, names=['scenario', 'variable'])
>>> print(data_multi)
    scenario             base  ...   low
    variable            solar  wind  load  ...  load
    datetime
    2023-01-01 00:00:00 -0.95 -1.57  0.89  ...  0.06
    2023-01-01 01:00:00  1.18  0.88 -0.62  ...  1.18
    2023-01-01 02:00:00  0.25  0.31  0.12  ...  0.24
    2023-01-01 03:00:00 -2.02 -0.59 -0.92  ...  0.45
    2023-01-01 04:00:00  1.13  0.73 -1.04  ... -0.05
    ...                   ...   ...   ...  ...   ...
>>>
>>> # Generate dashboard with row and column facets
>>> generator = TimeSeriesDashboardGenerator(
>>>     x_axis='date',
>>>     facet_row='variable',      # First level of MultiIndex
>>>     facet_col='scenario',      # Second level of MultiIndex
>>>     facet_row_order=['solar', 'wind', 'load'],
>>>     facet_col_order=['base', 'high', 'low']
>>> )
>>> fig = generator.get_figure(data_multi)

Custom KPI statistics:

>>> # Define custom aggregation functions
>>> custom_stats = {
>>>     'Peak': lambda x: x.max(),
>>>     'Valley': lambda x: x.min(),
>>>     'Peak-Valley': lambda x: x.max() - x.min(),
>>>     'Above 50%': lambda x: (x > x.quantile(0.5)).sum() / len(x) * 100,
>>>     'Volatility': lambda x: x.std() / x.mean() * 100
>>> }
>>> 
>>> generator = TimeSeriesDashboardGenerator(
>>>     x_axis='week',
>>>     stat_aggs=custom_stats,
>>>     facet_row='variable'
>>> )
>>> fig = generator.get_figure(data_multi)

Parameter-based faceting (multiple x-axis or aggregations):

>>> # Compare different time aggregations
>>> generator = TimeSeriesDashboardGenerator(
>>>     x_axis=['date', 'week', 'month'],    # Multiple x-axis types
>>>     facet_col='x_axis',                  # Facet by x_axis parameter
>>>     facet_row='variable'
>>> )
>>> fig = generator.get_figure(data_multi)
>>> 
>>> # Compare different aggregation methods
>>> generator = TimeSeriesDashboardGenerator(
>>>     x_axis='month',
>>>     groupby_aggregation=['min', 'mean', 'max'],  # Multiple agg methods
>>>     facet_col='groupby_aggregation',             # Facet by aggregation
>>>     facet_row='variable'
>>> )
>>> fig = generator.get_figure(data_multi)
Source code in submodules/mesqual/mesqual/visualizations/plotly_figures/timeseries_dashboard.py
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
class TimeSeriesDashboardGenerator:
    """Main class for generating timeseries heatmap dashboards.

    Creates comprehensive dashboards that visualize timeseries data as heatmaps
    with hour-of-day on y-axis and time aggregations on x-axis. Supports faceting
    by data columns (MultiIndex) or analysis parameters, customizable KPI statistics, and
    flexible color schemes including per-facet colorscales.

    The dashboard displays heatmaps alongside statistical summaries and supports
    various time aggregations (daily, weekly, monthly) with configurable grouping
    functions (mean, sum, min, max, etc.).

    Expected Data Format: The input data must be a pandas DataFrame or Series with:
        - DateTime index (required for time-based aggregations)
        - For faceting: MultiIndex columns with named levels

    Examples:
        Basic usage with single variable:

        >>> import pandas as pd
        >>> import numpy as np
        >>> from datetime import datetime, timedelta
        >>> 
        >>> # Create sample timeseries data
        >>> dates = pd.date_range('2023-01-01', periods=8760, freq='H')
        >>> data = pd.Series(np.random.randn(8760), index=dates, name='power')
        >>> 
        >>> # Generate basic dashboard
        >>> generator = TimeSeriesDashboardGenerator(x_axis='date')
        >>> fig = generator.get_figure(data)
        >>> fig.show()

        Multi-variable with faceting:

        >>> # Create multi-column data with proper MultiIndex
        >>> variables = ['solar', 'wind', 'load']
        >>> scenarios = ['base', 'high', 'low']
        >>> 
        >>> # Method 1: Using pd.concat to create MultiIndex
        >>> data_dict = {}
        >>> for scenario in scenarios:
        >>>     scenario_data = pd.DataFrame({
        >>>         var: np.random.randn(8760) for var in variables
        >>>     }, index=dates)
        >>>     data_dict[scenario] = scenario_data
        >>> 
        >>> data_multi = pd.concat(data_dict, axis=1, names=['scenario', 'variable'])
        >>> print(data_multi)
            scenario             base  ...   low
            variable            solar  wind  load  ...  load
            datetime
            2023-01-01 00:00:00 -0.95 -1.57  0.89  ...  0.06
            2023-01-01 01:00:00  1.18  0.88 -0.62  ...  1.18
            2023-01-01 02:00:00  0.25  0.31  0.12  ...  0.24
            2023-01-01 03:00:00 -2.02 -0.59 -0.92  ...  0.45
            2023-01-01 04:00:00  1.13  0.73 -1.04  ... -0.05
            ...                   ...   ...   ...  ...   ...
        >>>
        >>> # Generate dashboard with row and column facets
        >>> generator = TimeSeriesDashboardGenerator(
        >>>     x_axis='date',
        >>>     facet_row='variable',      # First level of MultiIndex
        >>>     facet_col='scenario',      # Second level of MultiIndex
        >>>     facet_row_order=['solar', 'wind', 'load'],
        >>>     facet_col_order=['base', 'high', 'low']
        >>> )
        >>> fig = generator.get_figure(data_multi)

        Custom KPI statistics:

        >>> # Define custom aggregation functions
        >>> custom_stats = {
        >>>     'Peak': lambda x: x.max(),
        >>>     'Valley': lambda x: x.min(),
        >>>     'Peak-Valley': lambda x: x.max() - x.min(),
        >>>     'Above 50%': lambda x: (x > x.quantile(0.5)).sum() / len(x) * 100,
        >>>     'Volatility': lambda x: x.std() / x.mean() * 100
        >>> }
        >>> 
        >>> generator = TimeSeriesDashboardGenerator(
        >>>     x_axis='week',
        >>>     stat_aggs=custom_stats,
        >>>     facet_row='variable'
        >>> )
        >>> fig = generator.get_figure(data_multi)

        Parameter-based faceting (multiple x-axis or aggregations):

        >>> # Compare different time aggregations
        >>> generator = TimeSeriesDashboardGenerator(
        >>>     x_axis=['date', 'week', 'month'],    # Multiple x-axis types
        >>>     facet_col='x_axis',                  # Facet by x_axis parameter
        >>>     facet_row='variable'
        >>> )
        >>> fig = generator.get_figure(data_multi)
        >>> 
        >>> # Compare different aggregation methods
        >>> generator = TimeSeriesDashboardGenerator(
        >>>     x_axis='month',
        >>>     groupby_aggregation=['min', 'mean', 'max'],  # Multiple agg methods
        >>>     facet_col='groupby_aggregation',             # Facet by aggregation
        >>>     facet_row='variable'
        >>> )
        >>> fig = generator.get_figure(data_multi)
    """
    def __init__(
            self,
            x_axis: X_AXIS_TYPES = 'date',
            facet_col: str = None,
            facet_row: str = None,
            facet_col_wrap: int = None,
            facet_col_order: list[str] = None,
            facet_row_order: list[str] = None,
            ratio_of_stat_col: float = 0.1,
            stat_aggs: Dict[str, Callable[[pd.Series], float | int]] = None,
            groupby_aggregation: GROUPBY_AGG_TYPES = 'mean',
            title: str = None,
            color_continuous_scale: str | list[str] | list[tuple[float, str]] = None,
            color_continuous_midpoint: int | float = None,
            range_color: tuple[float, float] | list[int | float] = None,
            per_facet_col_colorscale: bool = False,
            per_facet_row_colorscale: bool = False,
            facet_row_color_settings: dict = None,
            facet_col_color_settings: dict = None,
            subplots_vertical_spacing: float = None,
            subplots_horizontal_spacing: float = None,
            time_series_figure_kwargs: dict = None,
            stat_figure_kwargs: dict = None,
            universal_figure_kwargs: dict = None,
            use_string_for_axis: bool = False,
            config_cls: type[DashboardConfig] = DashboardConfig,
            data_processor_cls: type[DataProcessor] = DataProcessor,
            color_manager_cls: type[ColorManager] = ColorManager,
            trace_generator_cls: type[TraceGenerator] = TraceGenerator,
            **figure_kwargs
    ):
        """Initialize the timeseries dashboard generator.

        Args:
            x_axis: Time aggregation for x-axis or list for faceting.
            facet_col: Column faceting specification.
            facet_row: Row faceting specification.
            facet_col_wrap: Maximum columns per row in faceted layout.
            facet_col_order: Custom ordering for column facets.
            facet_row_order: Custom ordering for row facets.
            ratio_of_stat_col: Width ratio of statistics column to heatmap.
            stat_aggs: Custom KPI aggregation functions.
            groupby_aggregation: Data aggregation method or list for faceting.
            title: Dashboard title.
            color_continuous_scale: Plotly colorscale specification.
            color_continuous_midpoint: Midpoint for diverging colorscales.
            range_color: Fixed color range for heatmaps.
            per_facet_col_colorscale: Enable separate colorscales per column facet.
            per_facet_row_colorscale: Enable separate colorscales per row facet.
            facet_row_color_settings: Custom color settings per row facet.
            facet_col_color_settings: Custom color settings per column facet.
            subplots_vertical_spacing: Vertical spacing between subplots.
            subplots_horizontal_spacing: Horizontal spacing between subplots.
            time_series_figure_kwargs: Additional heatmap trace parameters.
            stat_figure_kwargs: Additional statistics trace parameters.
            universal_figure_kwargs: Parameters applied to all traces.
            use_string_for_axis: Convert axis values to strings.
            config_cls: Configuration class for dependency injection.
            data_processor_cls: Data processor class for dependency injection.
            color_manager_cls: Color manager class for dependency injection.
            trace_generator_cls: Trace generator class for dependency injection.
            **figure_kwargs: Additional figure-level parameters.
        """
        self.config = config_cls(
            x_axis=x_axis,
            facet_col=facet_col,
            facet_row=facet_row,
            facet_col_wrap=facet_col_wrap,
            facet_col_order=facet_col_order,
            facet_row_order=facet_row_order,
            ratio_of_stat_col=ratio_of_stat_col,
            stat_aggs=stat_aggs,
            groupby_aggregation=groupby_aggregation,
            title=title,
            color_continuous_scale=color_continuous_scale,
            color_continuous_midpoint=color_continuous_midpoint,
            range_color=range_color,
            per_facet_col_colorscale=per_facet_col_colorscale,
            per_facet_row_colorscale=per_facet_row_colorscale,
            facet_row_color_settings=facet_row_color_settings,
            facet_col_color_settings=facet_col_color_settings,
            subplots_vertical_spacing=subplots_vertical_spacing,
            subplots_horizontal_spacing=subplots_horizontal_spacing,
            time_series_figure_kwargs=time_series_figure_kwargs,
            stat_figure_kwargs=stat_figure_kwargs,
            universal_figure_kwargs=universal_figure_kwargs,
            use_string_for_axis=use_string_for_axis,
            ** figure_kwargs
        )
        self.data_processor_cls = data_processor_cls
        self.color_manager_cls = color_manager_cls
        self.trace_generator_cls = trace_generator_cls


    def get_figure(self, data: pd.DataFrame, **kwargs):
        """Generate a complete dashboard figure from timeseries data.

        Creates a plotly figure containing heatmaps with associated statistics,
        properly formatted axes, and optional faceting. Applies all configured
        styling, color schemes, and layout settings.

        Args:
            data: Input timeseries DataFrame or Series with datetime index.
            **kwargs: Runtime configuration overrides.

        Returns:
            Plotly Figure object containing the complete dashboard visualization.
        """
        original_config = copy.deepcopy(self.config)

        for key, value in kwargs.items():
            if hasattr(self.config, key):
                setattr(self.config, key, value)

        if not kwargs.get('_skip_validation', False):
            self.data_processor_cls.validate_input_data_and_config(data, self.config)
        data = self.data_processor_cls.prepare_dataframe_for_facet(data.copy(), self.config)
        data = self.data_processor_cls.ensure_df_has_two_column_levels(data, self.config)
        self.data_processor_cls.update_facet_config(data, self.config)

        fig = self._create_figure_layout_with_subplots(data)

        self._add_heatmap_and_stat_traces_to_figure(data, fig)

        if self.config.per_facet_col_colorscale:
            self._add_column_colorscales(data, fig)
            fig.update_traces(showlegend=False)
        elif self.config.per_facet_row_colorscale:
            self._add_row_colorscales(data, fig)
            fig.update_traces(showlegend=False)

        if self.config.title:
            fig.update_layout(
                title=f'<b>{self.config.title}</b>',
                title_x=0.5,
            )

        self.config = original_config

        return fig

    def get_figures_chunked(
            self,
            data: pd.DataFrame,
            max_n_rows_per_figure: int = None,
            n_figures: int = None,
            chunk_title_suffix: bool = True,
            **kwargs
    ) -> list[go.Figure]:
        """Generate multiple figures by splitting facet rows into chunks.

        Useful for handling large datasets with many row facets by creating
        multiple smaller figures instead of one large figure.

        Args:
            data: Input timeseries DataFrame with datetime index.
            max_n_rows_per_figure: Maximum number of row facets per figure.
            n_figures: Total number of figures to create (alternative to max_n_rows_per_figure).
            chunk_title_suffix: Whether to add "(Part X/Y)" suffix to titles.
            **kwargs: Runtime configuration overrides.

        Returns:
            List of plotly Figure objects, each containing a subset of row facets.

        Raises:
            ValueError: If both or neither of max_n_rows_per_figure and n_figures are provided.
        """
        original_config = copy.deepcopy(self.config)

        if sum(x is not None for x in [max_n_rows_per_figure, n_figures]) != 1:
            raise ValueError("Provide exactly one of: max_n_rows_per_figure or n_figures")

        for key, value in kwargs.items():
            if hasattr(self.config, key):
                setattr(self.config, key, value)

        data = self.data_processor_cls.prepare_dataframe_for_facet(data, self.config)
        data = self.data_processor_cls.ensure_df_has_two_column_levels(data, self.config)
        self.data_processor_cls.update_facet_config(data, self.config)

        if self.config.facet_row is None:
            return [self.get_figure(data, **kwargs)]

        total_rows = len(self.config.facet_row_order)

        if max_n_rows_per_figure:
            n_chunks = math.ceil(total_rows / max_n_rows_per_figure)
            chunk_size = max_n_rows_per_figure
        else:
            n_chunks = n_figures
            chunk_size = math.ceil(total_rows / n_figures)

        figures = []
        original_title = self.config.title

        for i in range(n_chunks):
            start_idx = i * chunk_size
            end_idx = min(start_idx + chunk_size, total_rows)
            chunk_rows = self.config.facet_row_order[start_idx:end_idx]

            if not chunk_rows:
                continue

            chunk_kwargs = kwargs.copy()
            chunk_kwargs['facet_row_order'] = chunk_rows

            if chunk_title_suffix and original_title:
                chunk_kwargs['title'] = f"{original_title} (Part {i + 1}/{n_chunks})"
            elif chunk_title_suffix:
                chunk_kwargs['title'] = f"Part {i + 1}/{n_chunks}"

            cols_in_chunk = [
                c for c, facet_row_category in
                zip(data.columns, data.columns.get_level_values(self.config.facet_row))
                if facet_row_category in chunk_rows
            ]
            data_chunk = data[cols_in_chunk]

            fig = self.get_figure(data_chunk, **chunk_kwargs, _skip_validation=True)
            figures.append(fig)

        self.config = original_config

        return figures

    def _create_figure_layout_with_subplots(self, data: pd.DataFrame) -> go.Figure:
        facet_col_wrap = max([1, self.config.facet_col_wrap])
        ratio_of_stat_col = self.config.ratio_of_stat_col

        has_colorscale_col = self.config.per_facet_row_colorscale
        has_colorscale_row = self.config.per_facet_col_colorscale

        num_facet_rows = max([1, len(self.config.facet_row_order)])
        num_facet_cols = max([1, len(self.config.facet_col_order)])

        num_rows = math.ceil(num_facet_cols / facet_col_wrap) * num_facet_rows
        num_cols = facet_col_wrap * 2  # Each facet gets a heatmap + stats column

        if has_colorscale_col:
            num_cols += 1
        if has_colorscale_row:
            num_rows += 1

        subplot_titles = self._generate_subplot_titles(has_colorscale_col, has_colorscale_row)
        column_widths = self._get_column_widths(facet_col_wrap, has_colorscale_col, ratio_of_stat_col)
        row_heights = self._get_row_heights(has_colorscale_row, num_rows)
        specs = [[{} for _ in range(num_cols)] for _ in range(num_rows)]

        fig = make_subplots(
            rows=num_rows,
            cols=num_cols,
            subplot_titles=subplot_titles,
            column_widths=column_widths,
            row_heights=row_heights,
            specs=specs,
            vertical_spacing=self.config.subplots_vertical_spacing,
            horizontal_spacing=self.config.subplots_horizontal_spacing,
        )
        fig.update_layout(
            plot_bgcolor='rgba(0, 0, 0, 0)',
            margin=dict(t=50, b=50)
        )

        return fig

    def _get_row_heights(self, has_colorscale_row: bool, num_rows: int) -> list[float]:
        row_heights = None
        if has_colorscale_row:
            regular_height = 1.0
            colorscale_height = 0.15

            total_regular_rows = num_rows - 1
            total_height = total_regular_rows * regular_height + colorscale_height
            norm_regular = regular_height / total_height
            norm_colorscale = colorscale_height / total_height

            row_heights = [norm_regular] * total_regular_rows + [norm_colorscale]
        return row_heights

    def _get_column_widths(self, facet_col_wrap, has_colorscale_col, ratio_of_stat_col) -> list[float]:
        if has_colorscale_col:
            colorscale_width = 0.03
            adjusted_width = 1 - colorscale_width
            column_widths = []

            for _ in range(facet_col_wrap):
                heatmap_width = (adjusted_width - ratio_of_stat_col) / facet_col_wrap
                stats_width = ratio_of_stat_col / facet_col_wrap
                column_widths.extend([heatmap_width, stats_width])

            column_widths.append(colorscale_width)
        else:
            column_widths = [(1 - ratio_of_stat_col) / facet_col_wrap, ratio_of_stat_col / facet_col_wrap] * facet_col_wrap
        return column_widths

    def _generate_subplot_titles(self, has_colorscale_col, has_colorscale_row):
        subplot_titles = []
        for row_name in self.config.facet_row_order:
            for col_name in self.config.facet_col_order:
                if row_name and col_name:
                    title = f'{row_name} - {col_name}'
                else:
                    title = row_name or col_name
                subplot_titles.append(title)  # Title for heatmap
                subplot_titles.append(None)  # Title for stats

            if has_colorscale_col:
                subplot_titles.append(row_name)

        if has_colorscale_row:
            for col_name in self.config.facet_col_order:
                subplot_titles.append(col_name)
                subplot_titles.append(None)
        return subplot_titles

    def _add_heatmap_and_stat_traces_to_figure(self, data, fig):
        facet_col_wrap = self.config.facet_col_wrap

        disable_main_colorbars = self.config.per_facet_col_colorscale or self.config.per_facet_row_colorscale
        if disable_main_colorbars:
            self.config.time_series_figure_kwargs['showscale'] = False

        global_color_params = {}
        if not (self.config.per_facet_col_colorscale or self.config.per_facet_row_colorscale):
            global_color_params = self.color_manager_cls.compute_color_params(data, self.config)

        current_row = 1
        row_offset = 0

        for row_idx, row_key in enumerate(self.config.facet_row_order):
            col_offset = 0
            for col_idx, col_key in enumerate(self.config.facet_col_order):
                facet_pos = col_idx % facet_col_wrap
                if facet_pos == 0 and col_idx > 0:
                    row_offset += 1

                fig_row = current_row + row_offset
                fig_col = col_offset + facet_pos * 2 + 1  # +1 because plotly indexing starts at 1

                data_col = facet_key = (row_key, col_key)
                if data_col not in data.columns:
                    continue
                series = data[data_col]

                x_axis = self._get_effective_param_for_data_col('x_axis', data_col)
                groupby_aggregation = self._get_effective_param_for_data_col('groupby_aggregation', data_col)

                self._set_hovertemplates(x_axis)

                grouped_data = self.data_processor_cls.get_grouped_data(series, x_axis, groupby_aggregation)

                color_params = self._get_color_params_for_facet(data, facet_key, global_color_params)

                show_colorbar = False
                if not disable_main_colorbars:
                    show_colorbar = (row_idx == 0 and col_idx == 0)

                heatmap_trace = self.trace_generator_cls.get_heatmap_trace(
                    grouped_data,
                    self.config.time_series_figure_kwargs,
                    color_params,
                    showscale=show_colorbar,
                    use_string_for_axis=self.config.use_string_for_axis,
                )

                fig.add_trace(heatmap_trace, row=fig_row, col=fig_col)

                fig.update_yaxes(
                    tickvals=[time(hour=h, minute=0) for h in [0, 6, 12, 18]] + [max(grouped_data.index)],
                    ticktext=['0', '6', '12', '18', '24'],
                    row=fig_row,
                    col=fig_col,
                    autorange='reversed',
                )

                if x_axis == 'year_week':
                    fig.update_xaxes(dtick=8, row=fig_row, col=fig_col)

                stats_trace = self.trace_generator_cls.get_stats_trace(
                    series,
                    self.config.stat_aggs,
                    self.config.stat_figure_kwargs,
                    color_params
                )

                fig.add_trace(stats_trace, row=fig_row, col=fig_col + 1)

                fig.update_xaxes(showgrid=False, row=fig_row, col=fig_col + 1)
                fig.update_yaxes(showgrid=False, autorange='reversed', row=fig_row, col=fig_col + 1)

            if col_offset == 0:
                current_row += math.ceil(len(self.config.facet_col_order) / facet_col_wrap)

    def _get_color_params_for_facet(self, data: pd.DataFrame, facet_key: tuple[str, str], global_color_params: dict) -> dict:
        if self.config.per_facet_col_colorscale or self.config.per_facet_row_colorscale:
            color_params = self.color_manager_cls.compute_color_params(data, self.config, facet_key)
        else:
            color_params = global_color_params
        return color_params

    def _add_row_colorscales(self, data, fig):
        colorscale_col = self.config.facet_col_wrap * 2 + 1  # Column after all heatmaps and stats

        for row_idx, row_key in enumerate(self.config.facet_row_order):
            row_pos = row_idx * math.ceil(len(self.config.facet_col_order) / self.config.facet_col_wrap) + 1

            facet_key = (row_key, self.config.facet_col_order[0])
            colorscale, z_max, z_min = self._get_color_settings_for_category(data, facet_key)
            colorscale_trace = self.trace_generator_cls.create_colorscale_trace(
                z_min, z_max, colorscale, 'v', row_key
            )

            fig.add_trace(colorscale_trace, row=row_pos, col=colorscale_col)
            fig.update_xaxes(showticklabels=False, showgrid=False, row=row_pos, col=colorscale_col)
            fig.update_yaxes(showticklabels=True, showgrid=False, row=row_pos, col=colorscale_col, side='right')

    def _get_color_settings_for_category(self, data, facet_key):
        color_params = self.color_manager_cls.compute_color_params(data, self.config, facet_key)
        colorscale = color_params.get('colorscale', 'viridis')
        z_min = color_params.get('zmin', 0)
        z_max = color_params.get('zmax', 1)
        return colorscale, z_max, z_min

    def _add_column_colorscales(self, data, fig):
        colorscale_row = math.ceil(len(self.config.facet_col_order) / self.config.facet_col_wrap) * len(
            self.config.facet_row_order) + 1

        for col_idx, col_key in enumerate(self.config.facet_col_order):
            col_pos = (col_idx % self.config.facet_col_wrap) * 2 + 1

            facet_key = (self.config.facet_row_order[0], col_key)

            colorscale, z_max, z_min = self._get_color_settings_for_category(data, facet_key)

            colorscale_trace = self.trace_generator_cls.create_colorscale_trace(
                z_min, z_max, colorscale, 'h', col_key
            )

            fig.add_trace(colorscale_trace, row=colorscale_row, col=col_pos)
            fig.update_xaxes(showticklabels=True, showgrid=False, row=colorscale_row, col=col_pos)
            fig.update_yaxes(showticklabels=False, showgrid=False, row=colorscale_row, col=col_pos)

    def _get_effective_param_for_data_col(self, param_name, data_col):
        param_value = getattr(self.config, param_name)
        if not isinstance(param_value, list):
            return param_value
        else:
            return list(set(param_value).intersection(list(data_col)))[0]

    def _set_hovertemplates(self, x_axis):
        ts_kwargs = self.config.time_series_figure_kwargs
        stat_kwargs = self.config.stat_figure_kwargs

        ts_kwargs['hovertemplate'] = f"{x_axis}: %{{x}}<br>Hour of day: %{{y}}<br>Value: %{{z}}<extra></extra>"
        stat_kwargs['hovertemplate'] = f"aggregation: %{{y}}<br>Value: %{{z}}<extra></extra>"

__init__

__init__(x_axis: X_AXIS_TYPES = 'date', facet_col: str = None, facet_row: str = None, facet_col_wrap: int = None, facet_col_order: list[str] = None, facet_row_order: list[str] = None, ratio_of_stat_col: float = 0.1, stat_aggs: Dict[str, Callable[[Series], float | int]] = None, groupby_aggregation: GROUPBY_AGG_TYPES = 'mean', title: str = None, color_continuous_scale: str | list[str] | list[tuple[float, str]] = None, color_continuous_midpoint: int | float = None, range_color: tuple[float, float] | list[int | float] = None, per_facet_col_colorscale: bool = False, per_facet_row_colorscale: bool = False, facet_row_color_settings: dict = None, facet_col_color_settings: dict = None, subplots_vertical_spacing: float = None, subplots_horizontal_spacing: float = None, time_series_figure_kwargs: dict = None, stat_figure_kwargs: dict = None, universal_figure_kwargs: dict = None, use_string_for_axis: bool = False, config_cls: type[DashboardConfig] = DashboardConfig, data_processor_cls: type[DataProcessor] = DataProcessor, color_manager_cls: type[ColorManager] = ColorManager, trace_generator_cls: type[TraceGenerator] = TraceGenerator, **figure_kwargs)

Initialize the timeseries dashboard generator.

Parameters:

Name Type Description Default
x_axis X_AXIS_TYPES

Time aggregation for x-axis or list for faceting.

'date'
facet_col str

Column faceting specification.

None
facet_row str

Row faceting specification.

None
facet_col_wrap int

Maximum columns per row in faceted layout.

None
facet_col_order list[str]

Custom ordering for column facets.

None
facet_row_order list[str]

Custom ordering for row facets.

None
ratio_of_stat_col float

Width ratio of statistics column to heatmap.

0.1
stat_aggs Dict[str, Callable[[Series], float | int]]

Custom KPI aggregation functions.

None
groupby_aggregation GROUPBY_AGG_TYPES

Data aggregation method or list for faceting.

'mean'
title str

Dashboard title.

None
color_continuous_scale str | list[str] | list[tuple[float, str]]

Plotly colorscale specification.

None
color_continuous_midpoint int | float

Midpoint for diverging colorscales.

None
range_color tuple[float, float] | list[int | float]

Fixed color range for heatmaps.

None
per_facet_col_colorscale bool

Enable separate colorscales per column facet.

False
per_facet_row_colorscale bool

Enable separate colorscales per row facet.

False
facet_row_color_settings dict

Custom color settings per row facet.

None
facet_col_color_settings dict

Custom color settings per column facet.

None
subplots_vertical_spacing float

Vertical spacing between subplots.

None
subplots_horizontal_spacing float

Horizontal spacing between subplots.

None
time_series_figure_kwargs dict

Additional heatmap trace parameters.

None
stat_figure_kwargs dict

Additional statistics trace parameters.

None
universal_figure_kwargs dict

Parameters applied to all traces.

None
use_string_for_axis bool

Convert axis values to strings.

False
config_cls type[DashboardConfig]

Configuration class for dependency injection.

DashboardConfig
data_processor_cls type[DataProcessor]

Data processor class for dependency injection.

DataProcessor
color_manager_cls type[ColorManager]

Color manager class for dependency injection.

ColorManager
trace_generator_cls type[TraceGenerator]

Trace generator class for dependency injection.

TraceGenerator
**figure_kwargs

Additional figure-level parameters.

{}
Source code in submodules/mesqual/mesqual/visualizations/plotly_figures/timeseries_dashboard.py
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
def __init__(
        self,
        x_axis: X_AXIS_TYPES = 'date',
        facet_col: str = None,
        facet_row: str = None,
        facet_col_wrap: int = None,
        facet_col_order: list[str] = None,
        facet_row_order: list[str] = None,
        ratio_of_stat_col: float = 0.1,
        stat_aggs: Dict[str, Callable[[pd.Series], float | int]] = None,
        groupby_aggregation: GROUPBY_AGG_TYPES = 'mean',
        title: str = None,
        color_continuous_scale: str | list[str] | list[tuple[float, str]] = None,
        color_continuous_midpoint: int | float = None,
        range_color: tuple[float, float] | list[int | float] = None,
        per_facet_col_colorscale: bool = False,
        per_facet_row_colorscale: bool = False,
        facet_row_color_settings: dict = None,
        facet_col_color_settings: dict = None,
        subplots_vertical_spacing: float = None,
        subplots_horizontal_spacing: float = None,
        time_series_figure_kwargs: dict = None,
        stat_figure_kwargs: dict = None,
        universal_figure_kwargs: dict = None,
        use_string_for_axis: bool = False,
        config_cls: type[DashboardConfig] = DashboardConfig,
        data_processor_cls: type[DataProcessor] = DataProcessor,
        color_manager_cls: type[ColorManager] = ColorManager,
        trace_generator_cls: type[TraceGenerator] = TraceGenerator,
        **figure_kwargs
):
    """Initialize the timeseries dashboard generator.

    Args:
        x_axis: Time aggregation for x-axis or list for faceting.
        facet_col: Column faceting specification.
        facet_row: Row faceting specification.
        facet_col_wrap: Maximum columns per row in faceted layout.
        facet_col_order: Custom ordering for column facets.
        facet_row_order: Custom ordering for row facets.
        ratio_of_stat_col: Width ratio of statistics column to heatmap.
        stat_aggs: Custom KPI aggregation functions.
        groupby_aggregation: Data aggregation method or list for faceting.
        title: Dashboard title.
        color_continuous_scale: Plotly colorscale specification.
        color_continuous_midpoint: Midpoint for diverging colorscales.
        range_color: Fixed color range for heatmaps.
        per_facet_col_colorscale: Enable separate colorscales per column facet.
        per_facet_row_colorscale: Enable separate colorscales per row facet.
        facet_row_color_settings: Custom color settings per row facet.
        facet_col_color_settings: Custom color settings per column facet.
        subplots_vertical_spacing: Vertical spacing between subplots.
        subplots_horizontal_spacing: Horizontal spacing between subplots.
        time_series_figure_kwargs: Additional heatmap trace parameters.
        stat_figure_kwargs: Additional statistics trace parameters.
        universal_figure_kwargs: Parameters applied to all traces.
        use_string_for_axis: Convert axis values to strings.
        config_cls: Configuration class for dependency injection.
        data_processor_cls: Data processor class for dependency injection.
        color_manager_cls: Color manager class for dependency injection.
        trace_generator_cls: Trace generator class for dependency injection.
        **figure_kwargs: Additional figure-level parameters.
    """
    self.config = config_cls(
        x_axis=x_axis,
        facet_col=facet_col,
        facet_row=facet_row,
        facet_col_wrap=facet_col_wrap,
        facet_col_order=facet_col_order,
        facet_row_order=facet_row_order,
        ratio_of_stat_col=ratio_of_stat_col,
        stat_aggs=stat_aggs,
        groupby_aggregation=groupby_aggregation,
        title=title,
        color_continuous_scale=color_continuous_scale,
        color_continuous_midpoint=color_continuous_midpoint,
        range_color=range_color,
        per_facet_col_colorscale=per_facet_col_colorscale,
        per_facet_row_colorscale=per_facet_row_colorscale,
        facet_row_color_settings=facet_row_color_settings,
        facet_col_color_settings=facet_col_color_settings,
        subplots_vertical_spacing=subplots_vertical_spacing,
        subplots_horizontal_spacing=subplots_horizontal_spacing,
        time_series_figure_kwargs=time_series_figure_kwargs,
        stat_figure_kwargs=stat_figure_kwargs,
        universal_figure_kwargs=universal_figure_kwargs,
        use_string_for_axis=use_string_for_axis,
        ** figure_kwargs
    )
    self.data_processor_cls = data_processor_cls
    self.color_manager_cls = color_manager_cls
    self.trace_generator_cls = trace_generator_cls

get_figure

get_figure(data: DataFrame, **kwargs)

Generate a complete dashboard figure from timeseries data.

Creates a plotly figure containing heatmaps with associated statistics, properly formatted axes, and optional faceting. Applies all configured styling, color schemes, and layout settings.

Parameters:

Name Type Description Default
data DataFrame

Input timeseries DataFrame or Series with datetime index.

required
**kwargs

Runtime configuration overrides.

{}

Returns:

Type Description

Plotly Figure object containing the complete dashboard visualization.

Source code in submodules/mesqual/mesqual/visualizations/plotly_figures/timeseries_dashboard.py
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
def get_figure(self, data: pd.DataFrame, **kwargs):
    """Generate a complete dashboard figure from timeseries data.

    Creates a plotly figure containing heatmaps with associated statistics,
    properly formatted axes, and optional faceting. Applies all configured
    styling, color schemes, and layout settings.

    Args:
        data: Input timeseries DataFrame or Series with datetime index.
        **kwargs: Runtime configuration overrides.

    Returns:
        Plotly Figure object containing the complete dashboard visualization.
    """
    original_config = copy.deepcopy(self.config)

    for key, value in kwargs.items():
        if hasattr(self.config, key):
            setattr(self.config, key, value)

    if not kwargs.get('_skip_validation', False):
        self.data_processor_cls.validate_input_data_and_config(data, self.config)
    data = self.data_processor_cls.prepare_dataframe_for_facet(data.copy(), self.config)
    data = self.data_processor_cls.ensure_df_has_two_column_levels(data, self.config)
    self.data_processor_cls.update_facet_config(data, self.config)

    fig = self._create_figure_layout_with_subplots(data)

    self._add_heatmap_and_stat_traces_to_figure(data, fig)

    if self.config.per_facet_col_colorscale:
        self._add_column_colorscales(data, fig)
        fig.update_traces(showlegend=False)
    elif self.config.per_facet_row_colorscale:
        self._add_row_colorscales(data, fig)
        fig.update_traces(showlegend=False)

    if self.config.title:
        fig.update_layout(
            title=f'<b>{self.config.title}</b>',
            title_x=0.5,
        )

    self.config = original_config

    return fig

get_figures_chunked

get_figures_chunked(data: DataFrame, max_n_rows_per_figure: int = None, n_figures: int = None, chunk_title_suffix: bool = True, **kwargs) -> list[Figure]

Generate multiple figures by splitting facet rows into chunks.

Useful for handling large datasets with many row facets by creating multiple smaller figures instead of one large figure.

Parameters:

Name Type Description Default
data DataFrame

Input timeseries DataFrame with datetime index.

required
max_n_rows_per_figure int

Maximum number of row facets per figure.

None
n_figures int

Total number of figures to create (alternative to max_n_rows_per_figure).

None
chunk_title_suffix bool

Whether to add "(Part X/Y)" suffix to titles.

True
**kwargs

Runtime configuration overrides.

{}

Returns:

Type Description
list[Figure]

List of plotly Figure objects, each containing a subset of row facets.

Raises:

Type Description
ValueError

If both or neither of max_n_rows_per_figure and n_figures are provided.

Source code in submodules/mesqual/mesqual/visualizations/plotly_figures/timeseries_dashboard.py
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
def get_figures_chunked(
        self,
        data: pd.DataFrame,
        max_n_rows_per_figure: int = None,
        n_figures: int = None,
        chunk_title_suffix: bool = True,
        **kwargs
) -> list[go.Figure]:
    """Generate multiple figures by splitting facet rows into chunks.

    Useful for handling large datasets with many row facets by creating
    multiple smaller figures instead of one large figure.

    Args:
        data: Input timeseries DataFrame with datetime index.
        max_n_rows_per_figure: Maximum number of row facets per figure.
        n_figures: Total number of figures to create (alternative to max_n_rows_per_figure).
        chunk_title_suffix: Whether to add "(Part X/Y)" suffix to titles.
        **kwargs: Runtime configuration overrides.

    Returns:
        List of plotly Figure objects, each containing a subset of row facets.

    Raises:
        ValueError: If both or neither of max_n_rows_per_figure and n_figures are provided.
    """
    original_config = copy.deepcopy(self.config)

    if sum(x is not None for x in [max_n_rows_per_figure, n_figures]) != 1:
        raise ValueError("Provide exactly one of: max_n_rows_per_figure or n_figures")

    for key, value in kwargs.items():
        if hasattr(self.config, key):
            setattr(self.config, key, value)

    data = self.data_processor_cls.prepare_dataframe_for_facet(data, self.config)
    data = self.data_processor_cls.ensure_df_has_two_column_levels(data, self.config)
    self.data_processor_cls.update_facet_config(data, self.config)

    if self.config.facet_row is None:
        return [self.get_figure(data, **kwargs)]

    total_rows = len(self.config.facet_row_order)

    if max_n_rows_per_figure:
        n_chunks = math.ceil(total_rows / max_n_rows_per_figure)
        chunk_size = max_n_rows_per_figure
    else:
        n_chunks = n_figures
        chunk_size = math.ceil(total_rows / n_figures)

    figures = []
    original_title = self.config.title

    for i in range(n_chunks):
        start_idx = i * chunk_size
        end_idx = min(start_idx + chunk_size, total_rows)
        chunk_rows = self.config.facet_row_order[start_idx:end_idx]

        if not chunk_rows:
            continue

        chunk_kwargs = kwargs.copy()
        chunk_kwargs['facet_row_order'] = chunk_rows

        if chunk_title_suffix and original_title:
            chunk_kwargs['title'] = f"{original_title} (Part {i + 1}/{n_chunks})"
        elif chunk_title_suffix:
            chunk_kwargs['title'] = f"Part {i + 1}/{n_chunks}"

        cols_in_chunk = [
            c for c, facet_row_category in
            zip(data.columns, data.columns.get_level_values(self.config.facet_row))
            if facet_row_category in chunk_rows
        ]
        data_chunk = data[cols_in_chunk]

        fig = self.get_figure(data_chunk, **chunk_kwargs, _skip_validation=True)
        figures.append(fig)

    self.config = original_config

    return figures

DashboardConfig

Configuration class for timeseries dashboard visualization.

Manages all configuration parameters for generating heatmap-based timeseries dashboards with customizable statistics, faceting, and color schemes.

Custom KPI Statistics

You can define custom KPIs by providing a dictionary of functions that operate on pandas Series. Each function should take a Series and return a single numeric value.

KPI Customization Example:

>>> custom_kpis = {
...     'Peak Load': lambda x: x.max(),
...     'Capacity Factor': lambda x: x.mean() / x.max() * 100,
...     'Ramp Rate': lambda x: x.diff().abs().max(),
...     'Hours Above Mean': lambda x: (x > x.mean()).sum(),
...     'Volatility': lambda x: x.std() / x.mean() * 100 if x.mean() != 0 else 0
... }

Available built-in statistics are provided in DEFAULT_STATISTICS and STATISTICS_LIBRARY class attributes.

Data Format Requirements

Input data must be a pandas DataFrame or Series with: - DatetimeIndex (hourly or sub-hourly recommended) - For faceting: MultiIndex columns with named levels - Column names will be used as facet category labels

MultiIndex structure for faceting:

>>> # Two-level MultiIndex example
>>> data.columns = pd.MultiIndex.from_tuples([
>>>     ('scenario1', 'solar'), ('scenario1', 'wind'),
>>>     ('scenario2', 'solar'), ('scenario2', 'wind')
>>> ], names=['scenario', 'technology'])
>>>
>>> # Use column level names for faceting
>>> config = DashboardConfig(
...     facet_row='technology',  # Use 'technology' level
...     facet_col='scenario'     # Use 'scenario' level
... )
Source code in submodules/mesqual/mesqual/visualizations/plotly_figures/timeseries_dashboard.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
class DashboardConfig:
    """Configuration class for timeseries dashboard visualization.

    Manages all configuration parameters for generating heatmap-based timeseries
    dashboards with customizable statistics, faceting, and color schemes.

    Custom KPI Statistics:
        You can define custom KPIs by providing a dictionary of functions that
        operate on pandas Series. Each function should take a Series and return
        a single numeric value.

        KPI Customization Example:

            >>> custom_kpis = {
            ...     'Peak Load': lambda x: x.max(),
            ...     'Capacity Factor': lambda x: x.mean() / x.max() * 100,
            ...     'Ramp Rate': lambda x: x.diff().abs().max(),
            ...     'Hours Above Mean': lambda x: (x > x.mean()).sum(),
            ...     'Volatility': lambda x: x.std() / x.mean() * 100 if x.mean() != 0 else 0
            ... }

        Available built-in statistics are provided in DEFAULT_STATISTICS and
        STATISTICS_LIBRARY class attributes.

    Data Format Requirements:
        Input data must be a pandas DataFrame or Series with:
        - DatetimeIndex (hourly or sub-hourly recommended)
        - For faceting: MultiIndex columns with named levels
        - Column names will be used as facet category labels

        MultiIndex structure for faceting:

            >>> # Two-level MultiIndex example
            >>> data.columns = pd.MultiIndex.from_tuples([
            >>>     ('scenario1', 'solar'), ('scenario1', 'wind'),
            >>>     ('scenario2', 'solar'), ('scenario2', 'wind')
            >>> ], names=['scenario', 'technology'])
            >>>
            >>> # Use column level names for faceting
            >>> config = DashboardConfig(
            ...     facet_row='technology',  # Use 'technology' level
            ...     facet_col='scenario'     # Use 'scenario' level
            ... )
    """
    DEFAULT_STATISTICS = {
        'Datums': lambda x: len(x),
        'Abs max': lambda x: x.abs().max(),
        'Abs mean': lambda x: x.abs().mean(),
        'Max': lambda x: x.max(),
        'Mean': lambda x: x.mean(),
        'Min': lambda x: x.min(),
    }

    STATISTICS_LIBRARY = {
        '# Values': lambda x: (~x.isna()).sum(),
        '# NaNs': lambda x: x.isna().sum(),
        '% == 0': lambda x: (x.round(2) == 0).sum() / (~x.isna()).sum() * 100,
        '% != 0': lambda x: ((x.round(2) != 0) & (~x.isna())).sum() / (~x.isna()).sum() * 100,
        '% > 0': lambda x: (x.round(2) > 0).sum() / (~x.isna()).sum() * 100,
        '% < 0': lambda x: (x.round(2) < 0).sum() / (~x.isna()).sum() * 100,
        'Mean of v>0': lambda x: x.where(x > 0, np.nan).mean(),
        'Mean of v<0': lambda x: x.where(x < 0, np.nan).mean(),
        'Median': lambda x: x.median(),
        'Q0.99': lambda x: x.quantile(0.99),
        'Q0.95': lambda x: x.quantile(0.95),
        'Q0.05': lambda x: x.quantile(0.05),
        'Q0.01': lambda x: x.quantile(0.01),
        'Std': lambda x: x.std(),
    }

    def __init__(
            self,
            x_axis: X_AXIS_TYPES = 'date',
            facet_col: str = None,
            facet_row: str = None,
            facet_col_wrap: int = None,
            facet_col_order: list[str] = None,
            facet_row_order: list[str] = None,
            ratio_of_stat_col: float = 0.1,
            stat_aggs: Dict[str, Callable[[pd.Series], float | int]] = None,
            groupby_aggregation: GROUPBY_AGG_TYPES = 'mean',
            title: str = None,
            color_continuous_scale: str | list[str] | list[tuple[float, str]] = 'Turbo',
            color_continuous_midpoint: int | float = None,
            range_color: list[int | float] = None,
            per_facet_col_colorscale: bool = False,
            per_facet_row_colorscale: bool = False,
            facet_row_color_settings: dict = None,
            facet_col_color_settings: dict = None,
            subplots_vertical_spacing: float = None,
            subplots_horizontal_spacing: float = None,
            time_series_figure_kwargs: dict = None,
            stat_figure_kwargs: dict = None,
            universal_figure_kwargs: dict = None,
            use_string_for_axis: bool = False,
            **figure_kwargs
    ):
        """Initialize dashboard configuration.

        Args:
            x_axis: X-axis aggregation type ('date', 'year_month', 'year_week', 'week', 'month', 'year')
                   or list of aggregation types for faceting.
            facet_col: Column name to use for column faceting, or 'x_axis'/'groupby_aggregation'
                      for parameter-based faceting.
            facet_row: Column name to use for row faceting, or 'x_axis'/'groupby_aggregation'
                      for parameter-based faceting.
            facet_col_wrap: Maximum number of columns per row when using column faceting.
            facet_col_order: Custom ordering for column facets.
            facet_row_order: Custom ordering for row facets.
            ratio_of_stat_col: Width ratio of statistics column relative to heatmap column.
            stat_aggs: Dictionary of statistic names to aggregation functions for KPI calculation.
            groupby_aggregation: Aggregation method for grouping data ('mean', 'sum', etc.) or list
                               of methods for faceting.
            title: Dashboard title.
            color_continuous_scale: Plotly colorscale name or custom colorscale.
            color_continuous_midpoint: Midpoint value for diverging colorscales.
            range_color: Fixed color range [min, max] for heatmaps.
            per_facet_col_colorscale: Whether to use separate colorscales per column facet.
            per_facet_row_colorscale: Whether to use separate colorscales per row facet.
            facet_row_color_settings: Custom color settings per row facet category.
            facet_col_color_settings: Custom color settings per column facet category.
            subplots_vertical_spacing: Vertical spacing between subplots.
            subplots_horizontal_spacing: Horizontal spacing between subplots.
            time_series_figure_kwargs: Additional kwargs for heatmap traces.
            stat_figure_kwargs: Additional kwargs for statistics traces.
            universal_figure_kwargs: kwargs applied to all traces.
            use_string_for_axis: Whether to convert axis values to strings.
            **figure_kwargs: Additional figure-level kwargs.
        """
        self.x_axis = x_axis
        self.facet_col = facet_col
        self.facet_row = facet_row
        self.facet_col_wrap = facet_col_wrap
        self.facet_col_order = facet_col_order
        self.facet_row_order = facet_row_order
        self.ratio_of_stat_col = ratio_of_stat_col
        self.stat_aggs = stat_aggs or self.DEFAULT_STATISTICS
        self.groupby_aggregation = groupby_aggregation
        self.title = title

        self.per_facet_col_colorscale = per_facet_col_colorscale
        self.per_facet_row_colorscale = per_facet_row_colorscale

        if per_facet_col_colorscale and per_facet_row_colorscale:
            raise ValueError("Cannot use both per_facet_col_colorscale and per_facet_row_colorscale simultaneously")
        if facet_row_color_settings and not per_facet_row_colorscale:
            raise ValueError("Set per_facet_row_colorscale to True in order to use facet_row_color_settings.")
        if facet_col_color_settings and not per_facet_col_colorscale:
            raise ValueError("Set per_facet_col_colorscale to True in order to use facet_col_color_settings.")

        self.facet_row_color_settings = facet_row_color_settings or {}
        self.facet_col_color_settings = facet_col_color_settings or {}

        self.time_series_figure_kwargs = time_series_figure_kwargs or {}
        self.stat_figure_kwargs = stat_figure_kwargs or {}

        self.subplots_vertical_spacing = subplots_vertical_spacing
        self.subplots_horizontal_spacing = subplots_horizontal_spacing

        universal_figure_kwargs = universal_figure_kwargs or {}

        self.figure_kwargs = {
            'color_continuous_scale': color_continuous_scale,
            'color_continuous_midpoint': color_continuous_midpoint,
            'range_color': range_color,
            **universal_figure_kwargs,
            **figure_kwargs,
        }
        self.use_string_for_axis = use_string_for_axis

__init__

__init__(x_axis: X_AXIS_TYPES = 'date', facet_col: str = None, facet_row: str = None, facet_col_wrap: int = None, facet_col_order: list[str] = None, facet_row_order: list[str] = None, ratio_of_stat_col: float = 0.1, stat_aggs: Dict[str, Callable[[Series], float | int]] = None, groupby_aggregation: GROUPBY_AGG_TYPES = 'mean', title: str = None, color_continuous_scale: str | list[str] | list[tuple[float, str]] = 'Turbo', color_continuous_midpoint: int | float = None, range_color: list[int | float] = None, per_facet_col_colorscale: bool = False, per_facet_row_colorscale: bool = False, facet_row_color_settings: dict = None, facet_col_color_settings: dict = None, subplots_vertical_spacing: float = None, subplots_horizontal_spacing: float = None, time_series_figure_kwargs: dict = None, stat_figure_kwargs: dict = None, universal_figure_kwargs: dict = None, use_string_for_axis: bool = False, **figure_kwargs)

Initialize dashboard configuration.

Parameters:

Name Type Description Default
x_axis X_AXIS_TYPES

X-axis aggregation type ('date', 'year_month', 'year_week', 'week', 'month', 'year') or list of aggregation types for faceting.

'date'
facet_col str

Column name to use for column faceting, or 'x_axis'/'groupby_aggregation' for parameter-based faceting.

None
facet_row str

Column name to use for row faceting, or 'x_axis'/'groupby_aggregation' for parameter-based faceting.

None
facet_col_wrap int

Maximum number of columns per row when using column faceting.

None
facet_col_order list[str]

Custom ordering for column facets.

None
facet_row_order list[str]

Custom ordering for row facets.

None
ratio_of_stat_col float

Width ratio of statistics column relative to heatmap column.

0.1
stat_aggs Dict[str, Callable[[Series], float | int]]

Dictionary of statistic names to aggregation functions for KPI calculation.

None
groupby_aggregation GROUPBY_AGG_TYPES

Aggregation method for grouping data ('mean', 'sum', etc.) or list of methods for faceting.

'mean'
title str

Dashboard title.

None
color_continuous_scale str | list[str] | list[tuple[float, str]]

Plotly colorscale name or custom colorscale.

'Turbo'
color_continuous_midpoint int | float

Midpoint value for diverging colorscales.

None
range_color list[int | float]

Fixed color range [min, max] for heatmaps.

None
per_facet_col_colorscale bool

Whether to use separate colorscales per column facet.

False
per_facet_row_colorscale bool

Whether to use separate colorscales per row facet.

False
facet_row_color_settings dict

Custom color settings per row facet category.

None
facet_col_color_settings dict

Custom color settings per column facet category.

None
subplots_vertical_spacing float

Vertical spacing between subplots.

None
subplots_horizontal_spacing float

Horizontal spacing between subplots.

None
time_series_figure_kwargs dict

Additional kwargs for heatmap traces.

None
stat_figure_kwargs dict

Additional kwargs for statistics traces.

None
universal_figure_kwargs dict

kwargs applied to all traces.

None
use_string_for_axis bool

Whether to convert axis values to strings.

False
**figure_kwargs

Additional figure-level kwargs.

{}
Source code in submodules/mesqual/mesqual/visualizations/plotly_figures/timeseries_dashboard.py
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
def __init__(
        self,
        x_axis: X_AXIS_TYPES = 'date',
        facet_col: str = None,
        facet_row: str = None,
        facet_col_wrap: int = None,
        facet_col_order: list[str] = None,
        facet_row_order: list[str] = None,
        ratio_of_stat_col: float = 0.1,
        stat_aggs: Dict[str, Callable[[pd.Series], float | int]] = None,
        groupby_aggregation: GROUPBY_AGG_TYPES = 'mean',
        title: str = None,
        color_continuous_scale: str | list[str] | list[tuple[float, str]] = 'Turbo',
        color_continuous_midpoint: int | float = None,
        range_color: list[int | float] = None,
        per_facet_col_colorscale: bool = False,
        per_facet_row_colorscale: bool = False,
        facet_row_color_settings: dict = None,
        facet_col_color_settings: dict = None,
        subplots_vertical_spacing: float = None,
        subplots_horizontal_spacing: float = None,
        time_series_figure_kwargs: dict = None,
        stat_figure_kwargs: dict = None,
        universal_figure_kwargs: dict = None,
        use_string_for_axis: bool = False,
        **figure_kwargs
):
    """Initialize dashboard configuration.

    Args:
        x_axis: X-axis aggregation type ('date', 'year_month', 'year_week', 'week', 'month', 'year')
               or list of aggregation types for faceting.
        facet_col: Column name to use for column faceting, or 'x_axis'/'groupby_aggregation'
                  for parameter-based faceting.
        facet_row: Column name to use for row faceting, or 'x_axis'/'groupby_aggregation'
                  for parameter-based faceting.
        facet_col_wrap: Maximum number of columns per row when using column faceting.
        facet_col_order: Custom ordering for column facets.
        facet_row_order: Custom ordering for row facets.
        ratio_of_stat_col: Width ratio of statistics column relative to heatmap column.
        stat_aggs: Dictionary of statistic names to aggregation functions for KPI calculation.
        groupby_aggregation: Aggregation method for grouping data ('mean', 'sum', etc.) or list
                           of methods for faceting.
        title: Dashboard title.
        color_continuous_scale: Plotly colorscale name or custom colorscale.
        color_continuous_midpoint: Midpoint value for diverging colorscales.
        range_color: Fixed color range [min, max] for heatmaps.
        per_facet_col_colorscale: Whether to use separate colorscales per column facet.
        per_facet_row_colorscale: Whether to use separate colorscales per row facet.
        facet_row_color_settings: Custom color settings per row facet category.
        facet_col_color_settings: Custom color settings per column facet category.
        subplots_vertical_spacing: Vertical spacing between subplots.
        subplots_horizontal_spacing: Horizontal spacing between subplots.
        time_series_figure_kwargs: Additional kwargs for heatmap traces.
        stat_figure_kwargs: Additional kwargs for statistics traces.
        universal_figure_kwargs: kwargs applied to all traces.
        use_string_for_axis: Whether to convert axis values to strings.
        **figure_kwargs: Additional figure-level kwargs.
    """
    self.x_axis = x_axis
    self.facet_col = facet_col
    self.facet_row = facet_row
    self.facet_col_wrap = facet_col_wrap
    self.facet_col_order = facet_col_order
    self.facet_row_order = facet_row_order
    self.ratio_of_stat_col = ratio_of_stat_col
    self.stat_aggs = stat_aggs or self.DEFAULT_STATISTICS
    self.groupby_aggregation = groupby_aggregation
    self.title = title

    self.per_facet_col_colorscale = per_facet_col_colorscale
    self.per_facet_row_colorscale = per_facet_row_colorscale

    if per_facet_col_colorscale and per_facet_row_colorscale:
        raise ValueError("Cannot use both per_facet_col_colorscale and per_facet_row_colorscale simultaneously")
    if facet_row_color_settings and not per_facet_row_colorscale:
        raise ValueError("Set per_facet_row_colorscale to True in order to use facet_row_color_settings.")
    if facet_col_color_settings and not per_facet_col_colorscale:
        raise ValueError("Set per_facet_col_colorscale to True in order to use facet_col_color_settings.")

    self.facet_row_color_settings = facet_row_color_settings or {}
    self.facet_col_color_settings = facet_col_color_settings or {}

    self.time_series_figure_kwargs = time_series_figure_kwargs or {}
    self.stat_figure_kwargs = stat_figure_kwargs or {}

    self.subplots_vertical_spacing = subplots_vertical_spacing
    self.subplots_horizontal_spacing = subplots_horizontal_spacing

    universal_figure_kwargs = universal_figure_kwargs or {}

    self.figure_kwargs = {
        'color_continuous_scale': color_continuous_scale,
        'color_continuous_midpoint': color_continuous_midpoint,
        'range_color': range_color,
        **universal_figure_kwargs,
        **figure_kwargs,
    }
    self.use_string_for_axis = use_string_for_axis

DataProcessor

Source code in submodules/mesqual/mesqual/visualizations/plotly_figures/timeseries_dashboard.py
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
class DataProcessor:
    @staticmethod
    def validate_input_data_and_config(data: pd.DataFrame, config: DashboardConfig) -> None:
        x_axis = config.x_axis
        groupby_aggregation = config.groupby_aggregation
        facet_col = config.facet_col
        facet_row = config.facet_row
        facet_col_wrap = config.facet_col_wrap

        if facet_col_wrap is not None and facet_row is not None:
            raise ValueError('You cannot set facet_row if you are setting a facet_col_wrap')

        if isinstance(data, pd.Series):
            if sum(facet not in [None, 'x_axis', 'groupby_aggregation'] for facet in [facet_col, facet_row]):
                raise ValueError('You can not define facet_col or facet_row if you just have a pd.Series')
        elif data.columns.nlevels > 2:
            raise ValueError('Your data must not have more than 2 column index levels.')
        elif data.columns.nlevels == 2:
            if (facet_col is None) and (facet_row is None):
                raise ValueError('If you have two column levels, you must define both, facet_col and facet_row.')
            if isinstance(x_axis, list) or isinstance(groupby_aggregation, list):
                raise ValueError(
                    'You cannot set x_axis or groupby_aggregation to a list if your data already has 2 levels.'
                )
        elif data.columns.nlevels == 1:
            if sum(facet not in [None, 'x_axis', 'groupby_aggregation'] for facet in [facet_col, facet_row]) > 1:
                raise ValueError('You only have 1 column level. You can only define facet_col or facet_row')
            if isinstance(x_axis, list) and isinstance(groupby_aggregation, list):
                raise ValueError(
                    'You cannot set x_axis and groupby_aggregation to a list if your data already has 1 level.'
                )

        if isinstance(x_axis, list):
            if not any('x_axis' == facet for facet in [facet_col, facet_row]):
                raise ValueError(
                    "x_axis must be either 'facet_col' or 'facet_row' when provided as a list."
                )
        else:
            if any('x_axis' == facet for facet in [facet_col, facet_row]):
                raise ValueError(
                    "You provided a str for x_axis, "
                    "but set facet_col or facet_row to 'x_axis'. This is not allowed! \n"
                    "You must provide a List[str] and in order to use facet_row / facet_col "
                    "for different x_axis."
                )

        if isinstance(groupby_aggregation, list):
            if not any('groupby_aggregation' == facet for facet in [facet_col, facet_row]):
                raise ValueError(
                    "groupby_aggregation must be either 'facet_col' or 'facet_row' when provided as a list."
                )
        else:
            if any('groupby_aggregation' == facet for facet in [facet_col, facet_row]):
                raise ValueError(
                    "You provided a str for groupby_aggregation, "
                    "but set facet_col or facet_row to 'groupby_aggregation'. This is not allowed! \n"
                    "You must provide a List[str] and in order to use facet_row / facet_col "
                    "for different groupby_aggregation."
                )

    @staticmethod
    def prepare_dataframe_for_facet(data: pd.DataFrame, config: DashboardConfig) -> pd.DataFrame:
        for k in ['x_axis', 'groupby_aggregation']:
            config_value = getattr(config, k)
            if isinstance(config_value, list):
                data = pd.concat(
                    {i: data.copy(deep=True) for i in config_value},
                    axis=1,
                    names=[k],
                )
        return data

    @classmethod
    def ensure_df_has_two_column_levels(cls, data: pd.DataFrame, config: DashboardConfig) -> pd.DataFrame:
        if isinstance(data, pd.Series):
            data = data.to_frame(data.name or 'Time series')

        if data.columns.nlevels == 1:
            data.columns.name = data.columns.name or 'variable'
            data = cls._insert_empty_column_index_level(data)

        if config.facet_col in [data.columns.names[0]]:
            data.columns = data.columns.reorder_levels([1, 0])

        return data

    @staticmethod
    def update_facet_config(data: pd.DataFrame, config: DashboardConfig) -> None:
        unique_facet_col_keys = data.columns.get_level_values(config.facet_col).unique().to_list()
        if config.facet_col_order is None:
            config.facet_col_order = unique_facet_col_keys
        else:
            config.facet_col_order += [c for c in unique_facet_col_keys if c not in config.facet_col_order]

        unique_facet_row_keys = data.columns.get_level_values(config.facet_row).unique().to_list()
        if config.facet_row_order is None:
            config.facet_row_order = unique_facet_row_keys
        else:
            config.facet_row_order += [c for c in unique_facet_row_keys if c not in config.facet_row_order]

        if config.facet_col_wrap is None:
            config.facet_col_wrap = len(config.facet_col_order)

    @staticmethod
    def get_grouped_data(series: pd.Series, x_axis: str, groupby_aggregation: str) -> pd.DataFrame:
        """Group and aggregate time series data into heatmap format.

        Transforms timeseries data into a matrix suitable for heatmap visualization
        with hour-of-day on y-axis and specified time aggregation on x-axis.

        Args:
            series: Input timeseries data with datetime index.
            x_axis: Time aggregation method ('date', 'week', 'month', etc.).
            groupby_aggregation: Aggregation function name ('mean', 'sum', etc.).

        Returns:
            DataFrame with time categories as columns and hour-of-day as rows.
        """
        temp = series.to_frame('value')
        temp.loc[:, 'time'] = temp.index.time
        temp.loc[:, 'minute'] = temp.index.minute
        temp.loc[:, 'hour'] = temp.index.hour + 1
        temp.loc[:, 'date'] = temp.index.date
        temp.loc[:, 'month'] = temp.index.month
        temp.loc[:, 'week'] = temp.index.isocalendar().week
        temp.loc[:, 'year_month'] = temp.index.strftime('%Y-%m')
        temp.loc[:, 'year_week'] = temp.index.strftime('%Y-CW%U')

        y_axis = 'time'
        groupby = [y_axis, x_axis]
        temp = temp.groupby(groupby)['value'].agg(groupby_aggregation)
        temp = temp.unstack(x_axis)
        temp_data = temp.sort_index(ascending=False)
        return temp_data

    @staticmethod
    def _insert_empty_column_index_level(df: pd.DataFrame, level_name: str = None) -> pd.DataFrame:
        level_value = ''
        return pd.concat({level_value: df}, axis=1, names=[level_name])

    @staticmethod
    def _prepend_empty_row(df: pd.DataFrame) -> pd.DataFrame:
        empty_row = pd.DataFrame([[np.nan] * len(df.columns)], index=[' '], columns=df.columns)
        return pd.concat([empty_row, df])

get_grouped_data staticmethod

get_grouped_data(series: Series, x_axis: str, groupby_aggregation: str) -> DataFrame

Group and aggregate time series data into heatmap format.

Transforms timeseries data into a matrix suitable for heatmap visualization with hour-of-day on y-axis and specified time aggregation on x-axis.

Parameters:

Name Type Description Default
series Series

Input timeseries data with datetime index.

required
x_axis str

Time aggregation method ('date', 'week', 'month', etc.).

required
groupby_aggregation str

Aggregation function name ('mean', 'sum', etc.).

required

Returns:

Type Description
DataFrame

DataFrame with time categories as columns and hour-of-day as rows.

Source code in submodules/mesqual/mesqual/visualizations/plotly_figures/timeseries_dashboard.py
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
@staticmethod
def get_grouped_data(series: pd.Series, x_axis: str, groupby_aggregation: str) -> pd.DataFrame:
    """Group and aggregate time series data into heatmap format.

    Transforms timeseries data into a matrix suitable for heatmap visualization
    with hour-of-day on y-axis and specified time aggregation on x-axis.

    Args:
        series: Input timeseries data with datetime index.
        x_axis: Time aggregation method ('date', 'week', 'month', etc.).
        groupby_aggregation: Aggregation function name ('mean', 'sum', etc.).

    Returns:
        DataFrame with time categories as columns and hour-of-day as rows.
    """
    temp = series.to_frame('value')
    temp.loc[:, 'time'] = temp.index.time
    temp.loc[:, 'minute'] = temp.index.minute
    temp.loc[:, 'hour'] = temp.index.hour + 1
    temp.loc[:, 'date'] = temp.index.date
    temp.loc[:, 'month'] = temp.index.month
    temp.loc[:, 'week'] = temp.index.isocalendar().week
    temp.loc[:, 'year_month'] = temp.index.strftime('%Y-%m')
    temp.loc[:, 'year_week'] = temp.index.strftime('%Y-CW%U')

    y_axis = 'time'
    groupby = [y_axis, x_axis]
    temp = temp.groupby(groupby)['value'].agg(groupby_aggregation)
    temp = temp.unstack(x_axis)
    temp_data = temp.sort_index(ascending=False)
    return temp_data

ColorManager

Manages color settings and scale computation for dashboard visualizations.

Handles colorscale selection, range computation, and facet-specific color customization for heatmap and statistics traces.

Source code in submodules/mesqual/mesqual/visualizations/plotly_figures/timeseries_dashboard.py
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
class ColorManager:
    """Manages color settings and scale computation for dashboard visualizations.

    Handles colorscale selection, range computation, and facet-specific color
    customization for heatmap and statistics traces.
    """
    @staticmethod
    def get_color_settings_for_facet_category(config: DashboardConfig, facet_key: tuple[str, str]):
        """Get color settings for a specific facet category.

        Args:
            config: Dashboard configuration object.
            facet_key: Tuple of (row_key, col_key) identifying the facet.

        Returns:
            Dictionary of color settings for the specified facet category.
        """
        row_key, col_key = facet_key
        settings = {
            'color_continuous_scale': config.figure_kwargs.get('color_continuous_scale'),
            'color_continuous_midpoint': config.figure_kwargs.get('color_continuous_midpoint'),
            'range_color': config.figure_kwargs.get('range_color')
        }

        if config.per_facet_row_colorscale and row_key in config.facet_row_color_settings:
            settings.update(config.facet_row_color_settings.get(row_key, {}))
        elif config.per_facet_col_colorscale and col_key in config.facet_col_color_settings:
            settings.update(config.facet_col_color_settings.get(col_key, {}))

        return settings

    @staticmethod
    def compute_color_params(data, config: DashboardConfig, facet_key: tuple[str, str] = None):
        """Compute color parameters for heatmap traces.

        Calculates colorscale, min/max values, and other color-related parameters
        based on data range and configuration settings.

        Args:
            data: Input data for color range calculation.
            config: Dashboard configuration object.
            facet_key: Optional facet identifier for per-facet colorscales.

        Returns:
            Dictionary of color parameters for plotly traces.
        """
        if facet_key is not None:
            settings = ColorManager.get_color_settings_for_facet_category(config, facet_key)
        else:
            settings = config.figure_kwargs

        if facet_key is not None:
            if config.per_facet_row_colorscale:
                row_key, _ = facet_key
                filtered_data = data.loc[:, (row_key, slice(None))]
            elif config.per_facet_col_colorscale:
                _, col_key = facet_key
                filtered_data = data.loc[:, (slice(None), col_key)]
            else:
                filtered_data = data
        else:
            filtered_data = data

        color_continuous_scale = settings.get('color_continuous_scale')
        color_continuous_midpoint = settings.get('color_continuous_midpoint')
        range_color = settings.get('range_color')

        result = {}
        if color_continuous_scale:
            result['colorscale'] = color_continuous_scale

        if range_color:
            result['zmin'] = range_color[0]
            result['zmax'] = range_color[1]
        elif color_continuous_midpoint == 0:
            _absmax = filtered_data.abs().max().max()
            result['zmin'] = -_absmax
            result['zmax'] = _absmax
        elif color_continuous_midpoint:
            raise NotImplementedError("color_continuous_midpoint other than 0 is not implemented")
        else:
            result['zmin'] = filtered_data.min().min()
            result['zmax'] = filtered_data.max().max()

        return result

get_color_settings_for_facet_category staticmethod

get_color_settings_for_facet_category(config: DashboardConfig, facet_key: tuple[str, str])

Get color settings for a specific facet category.

Parameters:

Name Type Description Default
config DashboardConfig

Dashboard configuration object.

required
facet_key tuple[str, str]

Tuple of (row_key, col_key) identifying the facet.

required

Returns:

Type Description

Dictionary of color settings for the specified facet category.

Source code in submodules/mesqual/mesqual/visualizations/plotly_figures/timeseries_dashboard.py
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
@staticmethod
def get_color_settings_for_facet_category(config: DashboardConfig, facet_key: tuple[str, str]):
    """Get color settings for a specific facet category.

    Args:
        config: Dashboard configuration object.
        facet_key: Tuple of (row_key, col_key) identifying the facet.

    Returns:
        Dictionary of color settings for the specified facet category.
    """
    row_key, col_key = facet_key
    settings = {
        'color_continuous_scale': config.figure_kwargs.get('color_continuous_scale'),
        'color_continuous_midpoint': config.figure_kwargs.get('color_continuous_midpoint'),
        'range_color': config.figure_kwargs.get('range_color')
    }

    if config.per_facet_row_colorscale and row_key in config.facet_row_color_settings:
        settings.update(config.facet_row_color_settings.get(row_key, {}))
    elif config.per_facet_col_colorscale and col_key in config.facet_col_color_settings:
        settings.update(config.facet_col_color_settings.get(col_key, {}))

    return settings

compute_color_params staticmethod

compute_color_params(data, config: DashboardConfig, facet_key: tuple[str, str] = None)

Compute color parameters for heatmap traces.

Calculates colorscale, min/max values, and other color-related parameters based on data range and configuration settings.

Parameters:

Name Type Description Default
data

Input data for color range calculation.

required
config DashboardConfig

Dashboard configuration object.

required
facet_key tuple[str, str]

Optional facet identifier for per-facet colorscales.

None

Returns:

Type Description

Dictionary of color parameters for plotly traces.

Source code in submodules/mesqual/mesqual/visualizations/plotly_figures/timeseries_dashboard.py
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
@staticmethod
def compute_color_params(data, config: DashboardConfig, facet_key: tuple[str, str] = None):
    """Compute color parameters for heatmap traces.

    Calculates colorscale, min/max values, and other color-related parameters
    based on data range and configuration settings.

    Args:
        data: Input data for color range calculation.
        config: Dashboard configuration object.
        facet_key: Optional facet identifier for per-facet colorscales.

    Returns:
        Dictionary of color parameters for plotly traces.
    """
    if facet_key is not None:
        settings = ColorManager.get_color_settings_for_facet_category(config, facet_key)
    else:
        settings = config.figure_kwargs

    if facet_key is not None:
        if config.per_facet_row_colorscale:
            row_key, _ = facet_key
            filtered_data = data.loc[:, (row_key, slice(None))]
        elif config.per_facet_col_colorscale:
            _, col_key = facet_key
            filtered_data = data.loc[:, (slice(None), col_key)]
        else:
            filtered_data = data
    else:
        filtered_data = data

    color_continuous_scale = settings.get('color_continuous_scale')
    color_continuous_midpoint = settings.get('color_continuous_midpoint')
    range_color = settings.get('range_color')

    result = {}
    if color_continuous_scale:
        result['colorscale'] = color_continuous_scale

    if range_color:
        result['zmin'] = range_color[0]
        result['zmax'] = range_color[1]
    elif color_continuous_midpoint == 0:
        _absmax = filtered_data.abs().max().max()
        result['zmin'] = -_absmax
        result['zmax'] = _absmax
    elif color_continuous_midpoint:
        raise NotImplementedError("color_continuous_midpoint other than 0 is not implemented")
    else:
        result['zmin'] = filtered_data.min().min()
        result['zmax'] = filtered_data.max().max()

    return result

TraceGenerator

Generates plotly trace objects for dashboard visualization.

Creates heatmap traces for timeseries data, statistics traces for KPI display, and colorscale traces for custom color legends.

Source code in submodules/mesqual/mesqual/visualizations/plotly_figures/timeseries_dashboard.py
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
class TraceGenerator:
    """Generates plotly trace objects for dashboard visualization.

    Creates heatmap traces for timeseries data, statistics traces for KPI display,
    and colorscale traces for custom color legends.
    """

    @classmethod
    def get_heatmap_trace(cls, data: pd.DataFrame, ts_kwargs, color_kwargs, use_string_for_axis, **kwargs):
        """Create a heatmap trace for timeseries data visualization.

        Args:
            data: DataFrame with time categories as columns and hour-of-day as rows.
            ts_kwargs: Additional kwargs for the heatmap trace.
            color_kwargs: Color-related parameters (colorscale, zmin, zmax).
            use_string_for_axis: Whether to convert axis values to strings.
            **kwargs: Additional plotly Heatmap parameters.

        Returns:
            Plotly Heatmap trace object for timeseries visualization.
        """
        if set(data.columns).issubset(list(range(1, 13))):
            x = [calendar.month_abbr[m] for m in range(1, 13)]
        else:
            if use_string_for_axis:
                x = [str(i).replace('-', '_') for i in data.columns]
            else:
                x = data.columns

        trace_kwargs = {**color_kwargs, **ts_kwargs, **kwargs}

        assert 'colorscale' in trace_kwargs and 'zmin' in trace_kwargs and 'zmax' in trace_kwargs

        trace_heatmap = go.Heatmap(x=x, z=data.values, y=data.index, **trace_kwargs)
        return trace_heatmap

    @classmethod
    def get_stats_trace(cls, series: pd.Series, stat_aggs, stat_kwargs, color_kwargs, **kwargs):
        """Create a heatmap trace for displaying KPI statistics.

        Args:
            series: Input timeseries data for statistics calculation.
            stat_aggs: Dictionary of statistic names to aggregation functions.
            stat_kwargs: Additional kwargs for the statistics trace.
            color_kwargs: Color-related parameters (colorscale, zmin, zmax).
            **kwargs: Additional plotly Heatmap parameters.

        Returns:
            Plotly Heatmap trace object displaying calculated statistics.
        """
        data_stats = pd.Series({agg: func(series) for agg, func in stat_aggs.items()})
        data_stats = data_stats.to_frame('stats')
        data_stats = DataProcessor._prepend_empty_row(data_stats)

        if 'ygap' not in stat_kwargs:
            stat_kwargs['ygap'] = 5

        text_data = data_stats.map(lambda x: f'{x:.0f}')
        text_data = text_data.replace('nan', '').replace('null', '')

        trace_kwargs = {**color_kwargs, **stat_kwargs, **kwargs}
        trace_kwargs['showscale'] = False  # Stats should never have a colorbar

        assert 'colorscale' in trace_kwargs and 'zmin' in trace_kwargs and 'zmax' in trace_kwargs

        trace_stats = go.Heatmap(
            z=data_stats.values,
            x=data_stats.columns,
            y=data_stats.index,
            text=text_data.values,
            texttemplate="%{text}",
            **trace_kwargs
        )
        return trace_stats

    @staticmethod
    def create_colorscale_trace(z_min, z_max, colorscale, orientation='v', title=None):
        """Create a colorscale trace for custom color legend display.

        Args:
            z_min: Minimum value for colorscale range.
            z_max: Maximum value for colorscale range.
            colorscale: Plotly colorscale specification.
            orientation: Colorscale orientation ('v' for vertical, 'h' for horizontal).
            title: Optional title for the colorscale.

        Returns:
            Plotly Heatmap trace object representing the colorscale legend.
        """
        if orientation == 'v':
            z_vals = np.linspace(z_min, z_max, 100).reshape(-1, 1)
        else:
            z_vals = np.linspace(z_min, z_max, 100).reshape(1, -1)

        axis_vals = np.linspace(z_min, z_max, 100)

        colorbar_settings = {
            'thickness': 15,
            'title': title or ''
        }

        if orientation == 'h':
            colorbar_settings.update({
                'orientation': 'h',
                'y': -0.15,
                'xanchor': 'center',
                'x': 0.5
            })
            x = axis_vals
            y = None
        else:
            x = None
            y = axis_vals

        return go.Heatmap(
            x=x,
            y=y,
            z=z_vals,
            colorscale=colorscale,
            showscale=False,
            zmin=z_min,
            zmax=z_max,
            colorbar=colorbar_settings
        )

get_heatmap_trace classmethod

get_heatmap_trace(data: DataFrame, ts_kwargs, color_kwargs, use_string_for_axis, **kwargs)

Create a heatmap trace for timeseries data visualization.

Parameters:

Name Type Description Default
data DataFrame

DataFrame with time categories as columns and hour-of-day as rows.

required
ts_kwargs

Additional kwargs for the heatmap trace.

required
color_kwargs

Color-related parameters (colorscale, zmin, zmax).

required
use_string_for_axis

Whether to convert axis values to strings.

required
**kwargs

Additional plotly Heatmap parameters.

{}

Returns:

Type Description

Plotly Heatmap trace object for timeseries visualization.

Source code in submodules/mesqual/mesqual/visualizations/plotly_figures/timeseries_dashboard.py
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
@classmethod
def get_heatmap_trace(cls, data: pd.DataFrame, ts_kwargs, color_kwargs, use_string_for_axis, **kwargs):
    """Create a heatmap trace for timeseries data visualization.

    Args:
        data: DataFrame with time categories as columns and hour-of-day as rows.
        ts_kwargs: Additional kwargs for the heatmap trace.
        color_kwargs: Color-related parameters (colorscale, zmin, zmax).
        use_string_for_axis: Whether to convert axis values to strings.
        **kwargs: Additional plotly Heatmap parameters.

    Returns:
        Plotly Heatmap trace object for timeseries visualization.
    """
    if set(data.columns).issubset(list(range(1, 13))):
        x = [calendar.month_abbr[m] for m in range(1, 13)]
    else:
        if use_string_for_axis:
            x = [str(i).replace('-', '_') for i in data.columns]
        else:
            x = data.columns

    trace_kwargs = {**color_kwargs, **ts_kwargs, **kwargs}

    assert 'colorscale' in trace_kwargs and 'zmin' in trace_kwargs and 'zmax' in trace_kwargs

    trace_heatmap = go.Heatmap(x=x, z=data.values, y=data.index, **trace_kwargs)
    return trace_heatmap

get_stats_trace classmethod

get_stats_trace(series: Series, stat_aggs, stat_kwargs, color_kwargs, **kwargs)

Create a heatmap trace for displaying KPI statistics.

Parameters:

Name Type Description Default
series Series

Input timeseries data for statistics calculation.

required
stat_aggs

Dictionary of statistic names to aggregation functions.

required
stat_kwargs

Additional kwargs for the statistics trace.

required
color_kwargs

Color-related parameters (colorscale, zmin, zmax).

required
**kwargs

Additional plotly Heatmap parameters.

{}

Returns:

Type Description

Plotly Heatmap trace object displaying calculated statistics.

Source code in submodules/mesqual/mesqual/visualizations/plotly_figures/timeseries_dashboard.py
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
@classmethod
def get_stats_trace(cls, series: pd.Series, stat_aggs, stat_kwargs, color_kwargs, **kwargs):
    """Create a heatmap trace for displaying KPI statistics.

    Args:
        series: Input timeseries data for statistics calculation.
        stat_aggs: Dictionary of statistic names to aggregation functions.
        stat_kwargs: Additional kwargs for the statistics trace.
        color_kwargs: Color-related parameters (colorscale, zmin, zmax).
        **kwargs: Additional plotly Heatmap parameters.

    Returns:
        Plotly Heatmap trace object displaying calculated statistics.
    """
    data_stats = pd.Series({agg: func(series) for agg, func in stat_aggs.items()})
    data_stats = data_stats.to_frame('stats')
    data_stats = DataProcessor._prepend_empty_row(data_stats)

    if 'ygap' not in stat_kwargs:
        stat_kwargs['ygap'] = 5

    text_data = data_stats.map(lambda x: f'{x:.0f}')
    text_data = text_data.replace('nan', '').replace('null', '')

    trace_kwargs = {**color_kwargs, **stat_kwargs, **kwargs}
    trace_kwargs['showscale'] = False  # Stats should never have a colorbar

    assert 'colorscale' in trace_kwargs and 'zmin' in trace_kwargs and 'zmax' in trace_kwargs

    trace_stats = go.Heatmap(
        z=data_stats.values,
        x=data_stats.columns,
        y=data_stats.index,
        text=text_data.values,
        texttemplate="%{text}",
        **trace_kwargs
    )
    return trace_stats

create_colorscale_trace staticmethod

create_colorscale_trace(z_min, z_max, colorscale, orientation='v', title=None)

Create a colorscale trace for custom color legend display.

Parameters:

Name Type Description Default
z_min

Minimum value for colorscale range.

required
z_max

Maximum value for colorscale range.

required
colorscale

Plotly colorscale specification.

required
orientation

Colorscale orientation ('v' for vertical, 'h' for horizontal).

'v'
title

Optional title for the colorscale.

None

Returns:

Type Description

Plotly Heatmap trace object representing the colorscale legend.

Source code in submodules/mesqual/mesqual/visualizations/plotly_figures/timeseries_dashboard.py
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
@staticmethod
def create_colorscale_trace(z_min, z_max, colorscale, orientation='v', title=None):
    """Create a colorscale trace for custom color legend display.

    Args:
        z_min: Minimum value for colorscale range.
        z_max: Maximum value for colorscale range.
        colorscale: Plotly colorscale specification.
        orientation: Colorscale orientation ('v' for vertical, 'h' for horizontal).
        title: Optional title for the colorscale.

    Returns:
        Plotly Heatmap trace object representing the colorscale legend.
    """
    if orientation == 'v':
        z_vals = np.linspace(z_min, z_max, 100).reshape(-1, 1)
    else:
        z_vals = np.linspace(z_min, z_max, 100).reshape(1, -1)

    axis_vals = np.linspace(z_min, z_max, 100)

    colorbar_settings = {
        'thickness': 15,
        'title': title or ''
    }

    if orientation == 'h':
        colorbar_settings.update({
            'orientation': 'h',
            'y': -0.15,
            'xanchor': 'center',
            'x': 0.5
        })
        x = axis_vals
        y = None
    else:
        x = None
        y = axis_vals

    return go.Heatmap(
        x=x,
        y=y,
        z=z_vals,
        colorscale=colorscale,
        showscale=False,
        zmin=z_min,
        zmax=z_max,
        colorbar=colorbar_settings
    )