Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5.7 KiB
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
- 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.
- Per-row split. Each figure row holds 1..4 cells, independently of the other rows.
- Draggable cell widths, saved with the session, mirroring row heights (
weight). - Caps. At most 4 cells per row and at most 8 plots in total (
RowSet::kMaxRowskeeps its value and now bounds plots, not figure rows). Every per-plot structure sized for 8 stays as is. - Approach. Extend the vendored plotter's
Figureso a figure row holds several plots; the host treats each cell as one plot. Rejected: severalFigure/PlotAreas 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)
Figureis constructed from cells-per-row (e.g.{1,2,1}).Figure(u32 rows)stays and means one cell per row, so existing behaviour andtests/test_figure.cppare 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)andset_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). FigureLayoutgainscells(oneRectper plot, flattened).rowsstays 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.mdwith 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. RowSetgainscells_per_row_and per-figure-row cell weights, re-applied after every rebuild likerow_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_rowact 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 withoutrowstill loads. Expectgtk warnings: 0, noGtk-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.