Source code for pysatl_cpd.analysis.visualization.online.plotters.online_cpd_plotter_layouts

# -*- coding: ascii -*-
"""
Layout strategies for OnlineCPDPlotter.

This module provides various layout strategies for arranging subplots
in change-point detection visualizations.
"""

__author__ = "Danil Totmyanin"
__copyright__ = "Copyright (c) 2026 PySATL project"
__license__ = "SPDX-License-Identifier: MIT"


from abc import ABC, abstractmethod
from collections.abc import Callable

from plotly.subplots import make_subplots

from pysatl_cpd.analysis.visualization.typedefs import AxMapping, Figure


[docs] class ILayoutStrategy(ABC): """Abstract base class for plot layout strategies."""
[docs] @abstractmethod def create_figure_and_axes(self) -> tuple[Figure, AxMapping]: # pragma: no cover """Create a figure and return its axes mapping.""" raise NotImplementedError
@property @abstractmethod def required_axes(self) -> set[str]: # pragma: no cover """Return the subplot names provided by this layout.""" raise NotImplementedError
[docs] class VerticalLayout(ILayoutStrategy): """Stack all plots in a single column."""
[docs] def create_figure_and_axes(self) -> tuple[Figure, AxMapping]: """Create a three-row vertical figure.""" figure = make_subplots( rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.1, subplot_titles=("Time Series with Change Points", "Detection Function", "Processing Time per Step"), ) axes: AxMapping = { "timeseries": (1, 1), "detection_function": (2, 1), "processing_time": (3, 1), } return figure, axes
@property def required_axes(self) -> set[str]: """Return axes required by vertical layout.""" return {"timeseries", "detection_function", "processing_time"}
[docs] class SplitLayout(ILayoutStrategy): """Put the time series in one column and trace plots in the other."""
[docs] def create_figure_and_axes(self) -> tuple[Figure, AxMapping]: """Create a three-row, two-column split figure.""" figure = make_subplots( rows=3, cols=2, shared_xaxes=True, vertical_spacing=0.1, horizontal_spacing=0.1, subplot_titles=( "Time Series with Change Points", "Detection Function", "Processing Time per Step", "", # Empty for state plot "", # Empty for additional plots "", # Empty for additional plots ), ) axes: AxMapping = { "timeseries": (1, 1), "detection_function": (1, 2), "processing_time": (2, 2), "state": (3, 2), } return figure, axes
@property def required_axes(self) -> set[str]: """Return axes required by split layout.""" return {"timeseries", "detection_function", "processing_time", "state"}
[docs] class DashboardLiteLayout(ILayoutStrategy): """Show one time-series panel above two trace panels."""
[docs] def create_figure_and_axes(self) -> tuple[Figure, AxMapping]: """Create a compact two-row dashboard figure.""" figure = make_subplots( rows=2, cols=2, shared_xaxes=True, vertical_spacing=0.1, horizontal_spacing=0.1, subplot_titles=( "Time Series with Change Points", "", # Empty for state plot "Detection Function", "Processing Time per Step", ), column_widths=[0.7, 0.3], ) axes: AxMapping = { "timeseries": (1, 1), "state": (1, 2), "detection_function": (2, 1), "processing_time": (2, 2), } return figure, axes
@property def required_axes(self) -> set[str]: """Return axes required by dashboard-lite layout.""" return {"timeseries", "detection_function", "processing_time", "state"}
[docs] class DashboardLayout(ILayoutStrategy): """Arrange the time series and three trace plots in a 2x2 grid."""
[docs] def create_figure_and_axes(self) -> tuple[Figure, AxMapping]: """Create a full two-by-two dashboard figure.""" figure = make_subplots( rows=2, cols=2, shared_xaxes=True, vertical_spacing=0.1, horizontal_spacing=0.1, subplot_titles=( "Time Series with Change Points", "State Evolution", "Detection Function", "Processing Time per Step", ), ) axes: AxMapping = { "timeseries": (1, 1), "state": (1, 2), "detection_function": (2, 1), "processing_time": (2, 2), } return figure, axes
@property def required_axes(self) -> set[str]: """Return axes required by dashboard layout.""" return {"timeseries", "detection_function", "processing_time", "state"}
[docs] class CustomLayout(ILayoutStrategy): """Allow callers to provide custom figure-construction logic."""
[docs] def __init__(self, create_figure_func: Callable[[], tuple[Figure, AxMapping]], required_axes: set[str]) -> None: self._create_figure_func = create_figure_func self._required_axes = required_axes
[docs] def create_figure_and_axes(self) -> tuple[Figure, AxMapping]: """Create a figure with the user-provided factory.""" return self._create_figure_func()
@property def required_axes(self) -> set[str]: """Return axes required by custom layout.""" return self._required_axes