136 lines
4.3 KiB
Python
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},
|
|
},
|
|
}
|