diff --git a/docs/superpowers/specs/2026-11-03-split-shared-y-design.md b/docs/superpowers/specs/2026-11-03-split-shared-y-design.md new file mode 100644 index 0000000..64650e9 --- /dev/null +++ b/docs/superpowers/specs/2026-11-03-split-shared-y-design.md @@ -0,0 +1,80 @@ +# Split rows share one y axis -- design + +Amends `2026-11-03-horizontal-split-design.md`. Where the two disagree, this one governs. + +## Goal + +The cells of a split row share one y range and draw one y axis, the way the rows of the figure share +one x window and draw one x axis. A split row reads as one wide plot with dividers. + +## Decisions + +1. **Always shared, no option.** All cells of a figure row have one y range. An unsplit row is + unaffected. +2. **One y axis per row.** Only the leftmost cell has a y gutter and y labels. The other cells have + none, as only the bottom row carries x labels. +3. **Rulers are per row.** The two horizontal rulers (cursor pair) belong to the figure row and are + drawn at the same y in every cell. A drag in any cell moves them. +4. **Approach.** Extend the vendored plotter with a y link, symmetric to the x link. Rejected: syncing + the ranges from the host every frame (the plotter's own per-plot autoscale and the controller's + gestures would fight it, which is the problem the x link already solves). + +## Plotter (`deps/plotter`) + +- `Figure::set_share_row_y(bool)`, default off so existing figures and `tests/test_figure.cpp` are + unchanged. When on, the cells of each figure row form an implicit y link group. The group is rebuilt + with the cells. +- A y link syncs like the x link: in `layout()` a new `sync_y_links()` adopts a member's changed + `y_range` as the group range and pushes it to the other members. Wheel, pan, box zoom and axis drag + go through `set_y_range`, so they need no controller change. +- Auto scale: a member's own autoscale is suppressed (as `Axis::link_member` already blocks `Live`). + The group owns the auto flag and fits to the union of its members' visible traces; cells with no + trace are ignored; a log axis takes the union in axis space. `fit_all` and double-click fit the + group, not one cell. A manual `set_y_range` on a member clears the group's auto flag. +- `YStrip {Both, None}` and `Plot::set_y_strip_mode`, mirroring `XStrip`. `None` forces the y label + width, title width and reserved floor to 0. `Figure::layout` sets `None` on every cell but the + first of its row, structurally from the column index, never from content. `None` cells are skipped + in the common-gutter maximum. +- Cell widths: the row's y gutter is taken off the row width first, then the cells share the rest by + the existing `floor + extra*w/total` rule. Equal weights therefore give equal plot areas; the first + cell's rect is gutter plus its plot width, and the cell rects still tile the row exactly. The + floors apply to plot widths. +- A y axis-drag can only start in the leftmost cell (the others have no label area). +- Documented in `docs/claude/build-and-run.md` with the other vendored changes. + +## Host (`src/plot_host.*`, `src/ui*.cpp`) + +- `set_y_auto`, `set_y_range`, `fit_all`, and the Auto/Fixed dialog act on the figure row of the plot + they are given. The dialog reads the row's range. +- Rulers: one pair per figure row, stored on the row's first plot index and placed in every cell by + `place_cursors`; `sync_cursors` reads the dragged one back; `reset_cursors` and the measure level + marks use the row's range. The cursors pane lists the pair once per figure row. +- `row_unit` joins the units over all cells of the row, "" if mixed. +- `sync_compare_axes` writes y once per figure row. +- `rebuild_all` turns the sharing on for the new `Figure` before the cells are populated. +- Readouts and measurements stay per trace and per plot index; only the axis is shared. +- Session file: unchanged (y ranges were never persisted). + +## Layout editor + +Divider drag maps the pointer to weights through plot widths, not cell widths: for the first cell, +subtract the row's gutter from the left rect before applying `frac = (want_left-64)/(pair-128)`. The +96 px logical floor applies to the plot area. Nothing else changes. + +## Verification + +- `tests/test_figure.cpp`: with sharing on, a `{2}` row has equal y ranges after a `set_y_range` on + either cell; a second cell has zero y gutter and the first the common maximum; widths tile exactly and + equal weights give equal plot widths; group autoscale covers the union and ignores empty cells; sharing + off reproduces today's layout; `{1}` rows unaffected. +- Split probe gains: after a y gesture on one cell the other's range is identical; non-first cells have + no y gutter; fit-all covers both cells' traces; both rulers line up across cells; a divider drag still + tracks the pointer within 1 px. +- Regression: layout, cursors, measurements, readouts, E-1608 probes read as before; re-baseline only + what this feature explains. `gtk warnings: 0`, no `Gtk-CRITICAL`. + +## Docs + +`README.md`, `docs/USER-GUIDE.md`, `help/C/org.udpscope.app/` (layout page), `docs/claude/probe-readings.md`, +`docs/claude/invariants-plot-and-layout.md` (the "one gutter, not per column" rule and the per-cell y +rule), `docs/claude/build-and-run.md`, `ui_context_help.cpp`, `CLAUDE.md` if it mentions per-cell y.