Files
udpscope/docs/superpowers/specs/2026-11-03-split-shared-y-design.md

5.0 KiB

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 is unchanged: it writes x and y once per RowSet row. The comparison panes are flat single-cell rows, so that is once per figure row and no y sharing is involved.
  • 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.