# 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.