Files
impakt/src/impakt/web/components/plot_grid.py
2026-04-11 06:42:24 -04:00

136 lines
4.3 KiB
Python

"""Multi-pane plot grid component.
Supports configurable layouts (1x1, 2x1, 1x2, 2x2) with independent
plot panes. Each pane has its own channel selection and axis labels.
"""
from __future__ import annotations
from typing import Any
import dash_bootstrap_components as dbc
from dash import dcc, html
# Layout presets: (rows, cols)
LAYOUT_PRESETS: dict[str, tuple[int, int]] = {
"1x1": (1, 1),
"2x1": (2, 1),
"1x2": (1, 2),
"2x2": (2, 2),
"3x1": (3, 1),
}
MAX_PANES = 6
def build_plot_grid(layout: str = "1x1") -> html.Div:
"""Build the plot grid with layout selector and plot panes."""
return html.Div(
[
# Layout selector row
html.Div(
[
dbc.ButtonGroup(
[
dbc.Button(
label,
id={"type": "layout-btn", "index": label},
color="outline-secondary",
size="sm",
active=(label == layout),
style={"fontSize": "11px", "padding": "2px 8px"},
)
for label in LAYOUT_PRESETS
],
className="me-3",
),
html.Span(
id="plot-grid-info", className="text-muted", style={"fontSize": "11px"}
),
],
className="d-flex align-items-center mb-2",
),
# Plot panes container
html.Div(id="plot-grid-container", children=_build_panes(layout)),
# Hidden store for layout state
dcc.Store(id="plot-layout-store", data={"layout": layout, "pane_count": 1}),
]
)
def _build_panes(layout: str) -> list[Any]:
"""Build plot pane elements for a given layout."""
rows, cols = LAYOUT_PRESETS.get(layout, (1, 1))
pane_count = rows * cols
panes = []
for pane_idx in range(pane_count):
pane = _build_single_pane(pane_idx, pane_count)
panes.append(pane)
# Arrange in rows
if cols == 1:
return panes
else:
result = []
for row in range(rows):
row_panes = []
for col in range(cols):
idx = row * cols + col
if idx < len(panes):
row_panes.append(dbc.Col(panes[idx], width=12 // cols))
result.append(dbc.Row(row_panes, className="mb-2"))
return result
def _build_single_pane(pane_idx: int, total_panes: int) -> dbc.Card:
"""Build a single plot pane with its graph and controls."""
height = "500px" if total_panes == 1 else "320px" if total_panes <= 2 else "250px"
return dbc.Card(
[
dbc.CardBody(
[
dcc.Graph(
id={"type": "plot-graph", "index": pane_idx},
config={
"displayModeBar": True,
"scrollZoom": True,
"displaylogo": False,
"modeBarButtonsToRemove": ["select2d", "lasso2d"],
"modeBarButtonsToAdd": ["drawline", "eraseshape"],
},
style={"height": height},
clear_on_unhover=False,
),
],
className="p-0",
),
],
className="mb-1",
)
def build_empty_plot_figure() -> dict[str, Any]:
"""Build an empty plot figure with instruction text."""
return {
"data": [],
"layout": {
"template": "plotly_white",
"annotations": [
{
"text": "Select channels from the tree to plot",
"xref": "paper",
"yref": "paper",
"x": 0.5,
"y": 0.5,
"showarrow": False,
"font": {"size": 14, "color": "#bbb"},
}
],
"xaxis": {"visible": False},
"yaxis": {"visible": False},
"margin": {"l": 40, "r": 20, "t": 30, "b": 40},
},
}