docs: design spec for the horizontal plot split

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-11-03 14:57:53 +01:00
1 parent c15c89c00a
commit 50078f0217
1 file changed
+92
@@ -0,0 +1,92 @@
# Horizontal plot split -- design
## Goal
Let each row of the stacked plots be split into several plots side by side. All plots share one
time window (one x link, one zoom/pan, one crosshair). A split only arranges more plots on screen; it
is not an independent time window and not an XY plot.
## Decisions
1. **Shared time window.** Cells in a row, and rows in the figure, are all members of the one x link
group. No new time semantics.
2. **Per-row split.** Each figure row holds 1..4 cells, independently of the other rows.
3. **Draggable cell widths**, saved with the session, mirroring row heights (`weight`).
4. **Caps.** At most 4 cells per row and at most 8 plots in total (`RowSet::kMaxRows` keeps its value
and now bounds plots, not figure rows). Every per-plot structure sized for 8 stays as is.
5. **Approach.** Extend the vendored plotter's `Figure` so a figure row holds several plots; the host
treats each cell as one plot. Rejected: several `Figure`/`PlotArea`s linked by hand (cannot express a
per-row split, reintroduces manual x/crosshair sync) and a general tiling tree (not asked for).
## Plotter (`deps/plotter`)
- `Figure` is constructed from cells-per-row (e.g. `{1,2,1}`). `Figure(u32 rows)` stays and means one
cell per row, so existing behaviour and `tests/test_figure.cpp` are unchanged.
- `plot(i)` indexes the flattened cells, top to bottom, left to right. `sync_x_links()` and the
crosshair already work on any set of plots and need no change.
- New: `set_cell_weights(row, weights)` and `set_col_gap(px)`. Widths use the height scheme: a floor
per cell, the weights share the rest, the last cell closes the row exactly (no seam).
- `FigureLayout` gains `cells` (one `Rect` per plot, flattened). `rows` stays the full-width rect per
figure row.
- Only the bottom figure row's cells carry x labels and the x title; other cells get `XStrip::None`.
- The left y gutter stays one common maximum for the whole figure with the same reserved floor. A
per-column gutter would make cell width depend on content, which the layout rule forbids.
- A figure too narrow for the floor widths shrinks the cells proportionally and drops the column gap,
as heights do; no rect is ever negative.
- Pointer hit-testing finds the plot under the cursor by its cell rect.
- The change is documented in `docs/claude/build-and-run.md` with the other vendored changes.
## Host (`src/plot_host.*`, `src/ui_layout.cpp`, `src/layout_store.*`, `src/ui.cpp`)
- Everything keyed by a plot index keeps meaning "flattened plot index": cursors, measurements,
`bind()`, readouts, persistence layers, `cursors_pane_bound`.
- `RowSet` gains `cells_per_row_` and per-figure-row cell weights, re-applied after every rebuild like
`row_weights_`. `split_cell(i)` / `remove_cell(i)` change a row's cell count; a removed cell drops its
traces, a new cell starts empty. `insert_row` / `remove_row` act on figure rows and carry their cells.
- `row_count()` stays the plot count; `figure_row_count()` is added. The plan may rename if this reads
badly.
- Cursors: the vertical pair is one shared time drawn in every cell; horizontal rulers are per cell.
The crosshair is one x on every cell, the hovered cell shows its y.
- Y axes are per cell (auto or manual); fit-all fits each; wheel and axis drags act on the cell under
the pointer; an x gesture anywhere moves the shared window.
- A signal dropped on a cell lands in that cell with the same slot rule as a row. The signals page's
"place on row" choice lists cells ("row 2, left") only for split rows.
- Persistence layers fill free slots of the same cell.
## Layout editor
Mirrors the row controls:
- Each vertical boundary between two cells gets a `+` (insert a cell) and a drag arrow (resize the two
neighbours). Each row's left and right edge gets a `+`, so a one-cell row can be split from either side.
- Each cell keeps its top-left `×`: in a multi-cell row it removes the cell; in a one-cell row it
removes the row, as today. Insensitive when it would leave no plot.
- Cell `+` is insensitive at 4 cells in the row or 8 plots in total; the row `+` is insensitive at 8
plots in total.
- A divider drag moves the two neighbouring widths, each floored at about 96 px logical, relative to the
weights at drag start (as the row drag is). Saved on drag end.
- Outside the editor nothing changes except the extra plots.
## Session file
`[[plot]]` tables gain `row` (the figure row) and `width` (the cell's weight). A session without `row`
loads as one cell per row, so existing sessions open unchanged. Row `weight` stays per figure row (carried
on the row's first cell).
## Verification
- `tests/test_figure.cpp`: `{1}` identical to today; `{1,2}` tiles exactly; weights and floors respected;
a narrow figure degrades without negative rects; x link and crosshair span all cells.
- New headless probe `UDPSCOPE_PROBE_SPLIT_MS`: split a row, drop a signal in each cell, drag a divider;
check the cells tile the width, the shared x range is identical, both vertical cursors line up in every
cell, widths survive a session round-trip, and a session without `row` still loads. Expect
`gtk warnings: 0`, no `Gtk-CRITICAL`.
- Regression: layout, cursors, measurements, readouts and E-1608 probes read as before; the baseline file
is re-baselined only where this feature explains a difference.
- Layout dumps and probes that print per-row layout gain a cells list and a column-weights line.
## Docs
`README.md`, `docs/USER-GUIDE.md`, `help/C/org.udpscope.app/` (layout page), `docs/claude/probe-readings.md`,
`docs/claude/verification.md`, `docs/claude/invariants-plot-and-layout.md` (cell rules),
`docs/claude/build-and-run.md` (plotter change), and `ui_context_help.cpp` where the layout editor is described.