From fa9f0c6da9d0dd018706beadc0ab6ab50269d36a Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Wed, 4 Nov 2026 01:04:21 +0100 Subject: [PATCH] docs: split rows share one y axis Co-Authored-By: Claude Opus 4.6 --- README.md | 19 +++++++---- docs/USER-GUIDE.md | 10 +++--- docs/claude/architecture.md | 3 +- docs/claude/build-and-run.md | 22 ++++++++++++ docs/claude/invariants-plot-and-layout.md | 41 +++++++++++++++-------- docs/claude/probe-readings.md | 22 ++++++------ help/C/org.udpscope.app/cursors.page | 3 +- help/C/org.udpscope.app/layout.page | 6 ++-- 8 files changed, 86 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index af6ee87..6a83df2 100644 --- a/README.md +++ b/README.md @@ -2580,7 +2580,8 @@ the only place it is visible. Outside a gesture the plot is not read at all: `cu snapped value then, which is what the app wrote itself a frame earlier, and adopting that back is what made a dragged cursor spring to where it had been. A ruler is per row: each row has its own y axis, so two rows' rulers are two different values, and the pane names the row. In a split row (see "Splitting a row") the vertical pair is drawn in -every plot and a ruler belongs to one plot, which the pane names `plot 2a`. +every plot, and so is the horizontal pair: the plots of a split row share one y axis, so the pane lists one ruler +line for the whole row, and dragging a ruler in any plot moves it in all of them. `mix` needs no rulers: it reports the value the trace actually has at each marker, read through the library's `Plot::crosshair_value()` — **the nearer of the two bracketing @@ -2826,11 +2827,15 @@ reach every position the two-per-row reading offers. A row can be split into **two to four plots side by side**, up to **eight plots in all**. A split is only an arrangement of more plots on the screen: **every plot shares the one time window** -- one x link group, so a pan or zoom in any plot moves them all; one crosshair, drawn at the same time in every -plot; and one pair of vertical cursors, at the same two times in every plot. What each plot keeps is its -own **y axis** (auto or fixed), its own horizontal rulers and its own fit; a wheel or an axis drag acts on -the plot under the pointer. Only the **bottom** row's plots carry the time labels, and the left margin +plot; and one pair of vertical cursors, at the same two times in every plot. The plots of a row +also share **one y range**, the way the rows share one x window: the row has one y axis (auto or fixed), +drawn with its labels at the left of the first plot only, one pair of horizontal rulers drawn at the same +value in every plot, and one fit. A wheel, a drag, a box zoom, **Fit all** and the Auto/Fixed dialog act on +the whole row whichever plot they land in, and an auto-scaled row fits the union of its plots' traces (a plot +with none is ignored). Only the **bottom** row's plots carry the time labels, and the left margin holding the y labels is one width for the whole figure, so the plots of a row line up with the rows -above and below it; a plot whose labels are narrower simply has more margin. +above and below it; a plot whose labels are narrower simply has more margin. Because only the first plot +has a margin, the plots of a row get equal plot areas at equal weights. In the **Layout editor** (`l`): @@ -2847,8 +2852,8 @@ In the **Layout editor** (`l`): Plots are named by their row and, in a split row, a letter: **`1`**, **`2a`**..**`2d`**. The names are what the signals page's *Plot on* buttons (tooltip "row 2, cell 1 of 2"), the status notices ("plotting -'sine' on plot 2b") and the cursors pane for a split row (`plot 2a`; a plot of an unsplit row stays -`row N`, counted from 0 as before) show. A signal dragged from its badge lands on the plot under the +'sine' on plot 2b") and the cursors pane (one ruler line per figure row, `row 2` counted from 1 for a split row; a plot of an +unsplit row stays `row N`, counted from 0 as before) show. A signal dragged from its badge lands on the plot under the pointer, with the same free-slot rule as a row. The session file keeps the arrangement (`row` and `width` on each `[[plot]]`, see "The session layout file"). diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index 6ec5201..4962deb 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -87,10 +87,12 @@ up to **4** plots, and the figure up to **8** in all. of one plot it removes the row. Every plot shares **one time window**: panning or zooming in any of them moves them all, the crosshair -sits at the same time in each, and the vertical cursors are the same two times in each. Each plot has -its **own y axis**, its own horizontal rulers and its own fit, and only the bottom row shows the time +sits at the same time in each, and the vertical cursors are the same two times in each. The plots of a split +row also share **one y range**: the y labels are drawn at the left of the first plot only, the +horizontal rulers are the same two values in every plot, and a y zoom, pan, fit or fixed range in any of +them applies to the whole row. Only the bottom row shows the time labels. A plot of a split row is named by its row and a letter, `2a` to `2d`: that is what the signals -page's **Plot on** buttons, the status line and the cursors readout call it, and a signal dragged +page's **Plot on** buttons and the status line call it, and a signal dragged from its badge goes onto the plot under the pointer. The arrangement is saved per source, and a session saved before rows could be split opens with one plot per row, as it was. @@ -153,7 +155,7 @@ does not walk it off its snapshot. The cursors toggle in the header bar (`c`) switches on two instruments per row: - **vertical** — two times, shared by every row (and every plot of a split row); -- **horizontal** — two rulers per plot; +- **horizontal** — two rulers per row (every plot of a split row shows the same two values); - **both** — the two together; - **mix** — the two times, read off each *trace* where they cross it. diff --git a/docs/claude/architecture.md b/docs/claude/architecture.md index cd1832b..6cddb85 100644 --- a/docs/claude/architecture.md +++ b/docs/claude/architecture.md @@ -266,7 +266,8 @@ Layers to keep straight: `figure_row_count()` counts the rows. The figure owns everything the plots have to agree on: the x window (one x link group every plot joins), the live follow (the *group's* — a member's own axis must be `XFollow::Off`, since - `Figure::link_x()` refuses a `Live` member), the left gutter and the crosshair. + `Figure::link_x()` refuses a `Live` member), the y range of each figure row's cells (`set_share_row_y`: a split + row has one y axis, one ruler pair and one fit), the left gutter and the crosshair. - `ExportTable` (`src/export.hpp`) — the visible window as columns, collected from the snapshot or the live rings and handed to the `.npy`/`.csv` writers. Always the stored diff --git a/docs/claude/build-and-run.md b/docs/claude/build-and-run.md index cdf00da..d75f25f 100644 --- a/docs/claude/build-and-run.md +++ b/docs/claude/build-and-run.md @@ -281,6 +281,28 @@ Cell widths use the height scheme: a floor of **64 px logical** per cell, the we the last cell closes the row exactly, and a figure too narrow for the floors shrinks the cells proportionally and drops the column gap. Only the bottom row's cells carry x labels and the x title (every other plot gets `XStrip::None`), and the left y gutter stays one maximum over the whole figure. + +The cells of a figure row can **share one y range**, symmetric to the x link +(`docs/superpowers/specs/2026-11-03-split-shared-y-design.md`): + +- `Figure::set_share_row_y(bool)`, default off so every existing figure and `tests/test_figure.cpp` + read as before; the host turns it on in `RowSet::rebuild_all()` before the cells are populated. When + on, the cells of each figure row form an implicit y link group, rebuilt with the cells. +- Link semantics: `layout()` runs a `sync_y_links()` that adopts a member's changed `y_range` as the + group's and pushes it to the others, so a wheel, pan, box zoom or axis drag (all `set_y_range`) needs + no controller change; a manual `set_y_range` on a member clears the group's auto flag. A member's own + autoscale is suppressed and the group owns the flag: it fits to the union of its members' visible + traces (cells with no trace are ignored; a log axis takes the union in axis space), and `fit_all` and + a double-click fit the group, not one cell. The group only follows on a laid-out frame, so a caller + that sets a range or a fit and reads it back needs one `layout()` in between. +- `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 and never from content, and skips `None` cells in the + common-gutter maximum. A y axis drag can therefore start only in the leftmost cell. +- **Cell placement is by plot width**: the row's y gutter comes off the row width first, then the cells + share the rest by the existing `floor + extra * w / total` rule, the floors applying to plot widths. + Equal weights give equal plot areas; the first cell's rect is gutter plus its plot width and the rects + still tile the row exactly. In the GTK4 and GLFW adapters the pointer is routed by **cell rect** (`row_in(rows, x, y)`: the containing cell, else the nearest by squared distance), one `input::Controller` per plot. The tests are the `figure_cell*` cases in `tests/test_figure.cpp` and `glfw_host_routes_by_cell`. diff --git a/docs/claude/invariants-plot-and-layout.md b/docs/claude/invariants-plot-and-layout.md index d3320de..1b6be4b 100644 --- a/docs/claude/invariants-plot-and-layout.md +++ b/docs/claude/invariants-plot-and-layout.md @@ -123,27 +123,40 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear y), and the **vertical cursor pair is one shared time drawn in every cell** (`cursors_aligned`). A split is an arrangement of more plots, not a second time scale: nothing about the time axis changes (see [invariants-time-axis-and-window.md](invariants-time-axis-and-window.md)). - - **Per-cell y.** Each cell has its own y axis (auto or fixed), its own horizontal rulers and its own - fit; a wheel or axis drag acts on the cell under the pointer, an x gesture on any bottom-row cell - moves the shared window. The cursors pane's ruler label is `plot 2a` for a plot of a split row and - `row N` (the plot index) for one of an unsplit row, so the pane reads the same as before until a row - is split. + - **One y per figure row.** The cells of a figure row share one y range (the plotter's + `Figure::set_share_row_y(true)`, which `rebuild_all()` sets before the cells are populated): one y + axis (auto or fixed), one pair of horizontal rulers, one fit. A wheel or axis drag in any cell moves + the row, an x gesture on any bottom-row cell moves the shared window. The host therefore acts on the + **figure row** of a plot it is given (`set_y_auto`, `set_y_range`, `fit_all`, the Auto/Fixed dialog, + `row_unit`, which joins the units of every cell and reads "" if they differ). The rulers are stored on + the row's first plot index, copied to the other cells by `sync_cursors()` / `insert_cell()` / + `rebuild_all()` and placed in every cell by `place_cursors()`. The cursors pane lists **one ruler line + per figure row**: `row N` with N the 1-based figure row for a split row, and the 0-based plot index + for an unsplit row (the pane read that before splitting existed). Readouts and measurements stay per + trace and per plot index; only the axis is shared. - **Bottom-row x labels only.** The plotter gives every plot that is not in the last figure row `XStrip::None`; the cells of the bottom row each keep their strip, so a middle-drag on the axis works in any of them. The `t0` caption goes on those cells too. - - **One left gutter.** The y gutter is one maximum for the whole figure with the same reserved floor - as before (`Figure::set_min_y_gutter()`), **not a per-column one**: a per-column gutter would make a - cell's width depend on its labels, which "layout must not depend on content" forbids. A cell whose y - labels are narrower than the gutter simply has a wider margin. - - **Widths are weights with a floor.** The plotter keeps 64 px logical per cell and a column gap - (`kColGap`, 6 px); the editor's drag is stricter (below). A weight is a share of what is - left *after* the floors, so cells of `{3, 1}` read 533:220 in a 756 px figure, not 3:1, and the - editor's 96 px drag floor is a floor on the *width*, not on the share. Because a cell's width is + - **One left gutter, on the first cell only.** The y gutter is one maximum for the whole figure with the + same reserved floor as before (`Figure::set_min_y_gutter()`). It is carried by the first cell of each + row alone: `Figure::layout` gives every other cell `YStrip::None` from its column index (structurally, + never from what it holds), which forces its y label width, title width and reserved floor to 0. A + `None` cell contributes 0 to the common maximum, so a split row's non-first cells cannot widen the + gutter and a row's gutter does not depend on how many cells it has. A per-column gutter sized by the + labels would make a cell's width depend on its content, which "layout must not depend on content" + forbids. A row whose labels are narrower than the gutter simply has a wider margin. The weights apply + to **plot widths**, not cell rects (below). + - **Widths are weights with a floor.** The plotter keeps 64 px logical per plot and a column gap + (`kColGap`, 6 px); the editor's drag is stricter (below). The floors and the weights act on **plot widths**: the row's y gutter (carried by the first + cell) is taken off the row width first, then the cells share the rest, so equal weights give equal plot + areas and the first cell's rect is its gutter plus its plot width. A weight is a share of what is + left *after* the gutter and the floors, so cells of `{3, 1}` read 546:207 (rects) in a 756 px + figure, not 3:1, and the editor's 96 px drag floor is a floor on the plot *width*, not on the share. Because a cell's width is `64 + extra * weight / total` and not proportional to its weight, `layout_col_apply()` does not scale the pair's weights by the width ratio: it keeps the pair's weight sum `S` from the drag's *start* (`layout_col_drag_start`) and gives the left cell `S * (want_left - 64) / (pair - 128)`, clamped to `[0.001, 0.999]` (the plotter refuses non-positive weights; `0.5` if the pair is not wider than 128). - `want_left` is the pointer's width clamped to `[96, pair - 96]` logical px. The divider therefore + `want_left` and the pair are plot widths (`layout_col_drag_begin` subtracts the first cell's `y_axis_width + y_title_width` from its rect); `want_left` is the pointer's width clamped to `[96, pair - 96]` logical px. The divider therefore follows the pointer, the shrinking cell reads 96 px at the extreme and the pair keeps its total width (the split probe's `col-drag` line asserts all three). `kPlotterMinCellPx` (64) mirrors the plotter's `kMinCellWidthLogical`; both are logical px (the plotter scales it by `ui_scale`, which is diff --git a/docs/claude/probe-readings.md b/docs/claude/probe-readings.md index fd7c6fc..561f4f0 100644 --- a/docs/claude/probe-readings.md +++ b/docs/claude/probe-readings.md @@ -2853,19 +2853,19 @@ inside the panel does not close it), `click_outside_closes=1` (a click on the pl ``` [split-probe] start plots=1 frows=1 cells=[1] widths=[756] tile=1 x_equal=1 cursors_aligned=1 weights=[-] traces=[1:sine,square,ramp,noise] -[split-probe] split plots=2 frows=1 cells=[2] widths=[376,376] tile=1 x_equal=1 cursors_aligned=1 weights=[-] traces=[1a:sine|1b:square] -[split-probe] weights 3:1 plots=2 frows=1 cells=[2] widths=[533,220] tile=1 x_equal=1 cursors_aligned=1 weights=[3,1] traces=[1a:sine|1b:square] +[split-probe] split plots=2 frows=1 cells=[2] widths=[403,350] tile=1 x_equal=1 cursors_aligned=1 weights=[-] traces=[1a:sine|1b:square] +[split-probe] weights 3:1 plots=2 frows=1 cells=[2] widths=[546,207] tile=1 x_equal=1 cursors_aligned=1 weights=[3,1] traces=[1a:sine|1b:square] [split-probe] caps-calls insert_cell=[1,1,0] refused_cell=1 insert_row=[1,1,0] refused_row=1 plots=8 [split-probe] roundtrip rt=1 expect=r0(h=2.0000)[w=1.0000:sine] r1(h=1.0000)[w=3.0000:square][w=1.0000:ramp] got=r0(h=2.0000)[w=1.0000:sine] r1(h=1.0000)[w=3.0000:square][w=1.0000:ramp] [split-probe] ruler-labels split=[row 1,row 2] mixed=[row 0,row 2] [split-probe] shared-y y_shared=1 y_gutter=1 y_fit=1 ruler_mirror=1 ranges=[A=-3.000000..7.000000 B=-3.000000..7.000000 | fit A=-1.04..1.04 B=-1.04..1.04 data=-1..1] axes_w=[642.4,642.4] auto=1 ``` -`UDPSCOPE_PROBE_SPLIT_MS` (stderr; see [verification.md](verification.md)). Nineteen steps, 250 ms apart; +`UDPSCOPE_PROBE_SPLIT_MS` (stderr; see [verification.md](verification.md)). 23 steps, 250 ms apart; each restructures and the *next* step reports the frame that was laid out with it, so a line's label names the arrangement the line **reads**, not the call that made it. The state lines share one format: `plots` (`row_count()`), `frows` (`figure_row_count()`), `cells=[..]` (per figure row), `widths=[..]` -(logical pixels of each cell, `|` between rows), `weights=[..]` (the stored cell weights, `-` for a row with +(logical pixels of each cell rect, `|` between rows; the first cell's rect includes the row's y gutter, ~52 px here, and the others have none, so equal weights read `[403,350]` -- equal *plot* widths, not equal rects), `weights=[..]` (the stored cell weights, `-` for a row with none), `traces=[label:signals|..]` with the labels `plot_label()` gives (`1`, `1a`, `2b`). Every state line carries the same three assertions: @@ -2879,22 +2879,22 @@ The other lines, in order: | line | value it must read | the defect a wrong reading names | | --- | --- | --- | -| `start` / `split` | `cells=[1]` then `[2]` with `widths=[376,376]` (equal halves of 756) and the traces on `1a`/`1b`; a second cell starts **empty**, the probe assigns it | a split that moved or dropped the first cell's traces, or unequal default cells | -| `weights 3:1` | `widths=[533,220]`, **not** 3:1, and `tile=1` | not a defect: the plotter's 64 px floor per cell is taken off first and the weights share the remainder, so the shares read compressed; a result of exactly 3:1 would be the floor missing | +| `start` / `split` | `cells=[1]` then `[2]` with `widths=[403,350]` (equal plot widths of 350.4 px; the first rect is 52 px wider by the y gutter it alone carries) and the traces on `1a`/`1b`; a second cell starts **empty**, the probe assigns it | a split that moved or dropped the first cell's traces, or unequal default plot widths (`[376,376]` is the pre-sharing reading: a second cell that still has its own gutter) | +| `weights 3:1` | `widths=[546,207]`, **not** 3:1, and `tile=1` | not a defect: the row's y gutter and the plotter's 64 px floor per plot are taken off first and the weights share the remainder, so the shares read compressed; a result of exactly 3:1 would be the floor missing | | `row+cell` | `cells=[2,2]`, `weights=[3,1\|-]` | row 0's cell weights lost or moved when a row and a cell were inserted below it (they belong to the figure row) | | `caps-calls` | `insert_cell=[1,1,0] refused_cell=1 insert_row=[1,1,0] refused_row=1 plots=8` | a cap that is not reached or not held: row 0 starts with two cells and takes two more, refusing the third call at 4; two rows then fit before the plot total reaches 8 and the third is refused; `plots` must stop at **8** | | `caps` | `plots=8 frows=4 cells=[4,2,1,1]`, `tile=1` | a ninth plot, or a fifth cell in a row | | `remove` | `plots=7 cells=[3,2,1,1]` | `remove_cell()` on a multi-cell row that removed the row, or that dropped a neighbour's traces | | `roundtrip-io` | `wrote=1 read=1 plots_in_file=3` | a save that was refused, or a file that lost a `[[plot]]` table | -| `roundtrip` (state line) | `cells=[1,2] widths=[756\|533,220] weights=[-\|3,1]` and the three traces on `1`, `2a`, `2b` | the arrangement did not survive `collect_layout()` -> file -> `apply_layout()` | +| `roundtrip` (state line) | `cells=[1,2] widths=[756\|546,207] weights=[-\|3,1]` and the three traces on `1`, `2a`, `2b` | the arrangement did not survive `collect_layout()` -> file -> `apply_layout()` | | `roundtrip rt=` | **`rt=1`** with `got=` equal to `expect=` (row weights `h=`, cell weights `w=` and each cell's trace, read back at the file's `%.4f`) | **`rt=0`** names the difference: a weight written per plot index instead of per figure row, widths applied before the arrangement (which resets them), or a trace that moved to another cell | | `legacy-io` | `read=1 plots_in_file=3 row_unset=1` | the stripped file still carrying a `row`, which would make the next line test nothing | | `legacy` | `cells=[1,1,1]`, `frows=3`, `widths=[756\|756\|756]`, `traces=[1:sine\|2:square\|3:ramp]` | a session from before the split that did not open as one cell per row, or lost its signals | | `invalid-rows` | `ok=1 frows/plots=[5/5 3/3 3/3 3/3]` | one of four unusable `row` sequences (five cells in a row; a skipped row; a row that goes back; a first row that is not 0) that was refused, or kept as a split instead of loading one cell per row | -| `editor-on` -> `cell-split` | `cells=[1]` then `cell-split-ck cells=2` and `cell-split` with `widths=[376,376]`, the new `1b` empty | the editor's cell `+` not reaching `insert_cell()`, or the figure not re-pointed after it | +| `editor-on` -> `cell-split` | `cells=[1]` then `cell-split-ck cells=2` and `cell-split` with `widths=[403,350]`, the new `1b` empty | the editor's cell `+` not reaching `insert_cell()`, or the figure not re-pointed after it | | `cell-assigned` | `1a:sine\|1b:square` | an assignment into the new cell that landed on another plot | -| `col-drag` | `ok=1 left_gt_right=1 follows_80=1 relative=1 sum_80=1 right_floor=1 left_floor=1 sum_ext=1 both_above_min=1 tile=1`, then `d80=[376.5,376.5]->[456.5,296.5]` and `ext=[657.0,96.0\|96.0,657.0]` (all logical px, 753 px pair) | `ok` is the AND of the checks. `follows_80=0`: the drag of +80 did not put the left cell within 1 px of `start + 80` (the weights were taken as proportional to widths and the plotter's 64 px floor ignored -- an old reading was `[442.9,310.1]`); `relative=0`: three updates (+30, +60, +80) from one begin did not land at +80, i.e. the drag read the live weights instead of the begin snapshot `layout_col_drag_start`; `sum_80=0` / `sum_ext=0`: the pair's total changed instead of being redistributed; `right_floor=0` / `left_floor=0`: after +10000 / -10000 the shrinking cell does not read `kMinCellPx` (96) within 1 px; `both_above_min=0`: a cell below 96 px; `tile=1` is `split_probe_tile()`, the same check the state lines print. `left_gt_right` is `d80.l > d80.r` (after +80 from equal halves the left is the wider). The probe goes through `layout_col_drag_begin()` + `layout_col_apply()`, the pair the gesture calls | -| `pre-insert0` (state line) | after the extreme drags `widths=[96,657]` | the last extreme drag was -10000: the left cell sits on the 96 px floor and the right takes the rest of the pair. A left cell reading 144 is the old floor-ignoring weights (`weights=[0.25,1.75]`); correct weights read `[0.1,1.9]` | +| `col-drag` | `ok=1 left_gt_right=1 follows_80=1 relative=1 sum_80=1 right_floor=1 left_floor=1 sum_ext=1 both_above_min=1 tile=1`, then `d80=[350.4,350.4]->[430.4,270.4]`, `ext=[604.8,96.0\|96.0,604.8]` and `gutter0=52.2` (all logical **plot** widths, `axes.w` plus the right gutter of the two plots, measured after a `layout()` on the new weights; 700.8 px pair) | `ok` is the AND of the checks. The compared widths are plot widths and not cell rects, because only the first cell carries the y gutter (`gutter0=` is its rect width minus its plot width, printed so a reader can tell the two apart): a drag that equates the rect widths moves the divider ~17 px more than the pointer (the gutter's share is treated as weight; `d80=[350.4,350.4]->[447.6,253.2]`, +97.2 for +80) and reaches the left floor at 145.5 instead of 96 (`ext=[607.5,93.3\|93.3,607.5]` or `[659.7,93.3\|145.5,607.5]`), which reads `follows_80=0 relative=0 left_floor=0 right_floor=0 both_above_min=0`. `gutter0=` itself should read the y gutter (about 52 px; 0 means the first cell lost its gutter, a value near the cell width means the plot area collapsed). `follows_80=0`: the drag of +80 did not put the left cell within 1 px of `start + 80` (the weights were taken as proportional to widths and the plotter's 64 px floor ignored -- an old reading was `[442.9,310.1]`); `relative=0`: three updates (+30, +60, +80) from one begin did not land at +80, i.e. the drag read the live weights instead of the begin snapshot `layout_col_drag_start`; `sum_80=0` / `sum_ext=0`: the pair's total changed instead of being redistributed; `right_floor=0` / `left_floor=0`: after +10000 / -10000 the shrinking cell does not read `kMinCellPx` (96) within 1 px; `both_above_min=0`: a cell below 96 px; `tile=1` is `split_probe_tile()`, the same check the state lines print. `left_gt_right` is `d80.l > d80.r` (after +80 from equal halves the left is the wider). The probe goes through `layout_col_drag_begin()` + `layout_col_apply()`, the pair the gesture calls | +| `pre-insert0` (state line) | after the extreme drags `widths=[148,605]`, `weights=[0.111732,1.88827]` | the last extreme drag was -10000: the left *plot* sits on the 96 px floor (the rect reads 148 = 96 plus the 52 px y gutter) and the right takes the rest of the pair. A left rect reading 144 is the old floor-ignoring weights (`weights=[0.25,1.75]`); `[146,607]` / `[96,657]` are the readings of the drag before it measured plot widths and before the sharing (floor 93 px / 96 px rect) | | `insert0-move` | `move_ok=1 before=[sine,square] after=[,sine,square]` | an insert at the left edge that renumbered the cells instead of moving their traces right with them | | `remove-mid` | `rm_ok=1 after=[,square]` | removing a middle cell that dropped or shifted the wrong neighbour | | `row-weights` | `wt_ok=1 before=[3,1,1] after=[1,1]` | the row weights not following an insert or removal of a *figure row* (the vector had the plot-count length once and drifted off the rows) | @@ -2905,7 +2905,7 @@ The other lines, in order: | `assign-1b` / `drop-2b` | `assigned=1 notice='plotting 'sine' on plot 1b'`; `dropped=1 notice='plotting 'square' on plot 2b'` | a notice that names the plot by number alone | | `after-assign` (state line) | `traces=[1a:-\|1b:sine\|2a:-\|2b:square]` | the signal went to a neighbouring cell | | `ruler-labels` | `split=[row 1,row 2]` and, with cells `{1,2}`, `mixed=[row 0,row 2]` | the cursors pane's ruler lines (a label read through the pane's own widgets): **one line per figure row**, because the cells of a split row share their y axis and so their rulers. A split row is `row
` and sits on its first cell; an unsplit row keeps `row ` (**0-based**, as it always was -- `row 0` is the first plot, not a defect). One line per cell (`plot 1a,plot 1b,...`) is the pre-sharing reading | -| `shared-y` | `y_shared=1 y_gutter=1 y_fit=1 ruler_mirror=1`, `ranges=[A=-3..7 B=-3..7 | fit A=B]`, `axes_w` equal to within a pixel, `auto=1` | a row of two cells with a trace each. `y_shared`: a `set_y_range(-3, 7)` on cell A is B's after one frame. `y_gutter`: only cell A has a y gutter (B's `y_axis_width` and `y_label_area.w` are 0) and the two plots are equally wide. `y_fit`: `fit_all()` leaves both cells on one range that covers both traces (not the manual one). `ruler_mirror`: a ruler written on A and read back by `sync_cursors(0)` is B's too. A 0 in the first two is `set_share_row_y(true)` not reaching the figure; in `y_fit`, no frame between `fit_all()` and the read (the probe restructures to pump one) or a group that did not follow the flag | +| `shared-y` | `y_shared=1 y_gutter=1 y_fit=1 ruler_mirror=1`, `ranges=[A=-3..7 B=-3..7 \| fit A=B]`, `axes_w` equal to within a pixel, `auto=1` | a row of two cells with a trace each. `y_shared`: a `set_y_range(-3, 7)` on cell A is B's after one frame. `y_gutter`: only cell A has a y gutter (B's `y_axis_width` and `y_label_area.w` are 0) and the two plots are equally wide. `y_fit`: `fit_all()` leaves both cells on one range that covers both traces (not the manual one). `ruler_mirror`: a ruler written on A and read back by `sync_cursors(0)` is B's too. A 0 in the first two is `set_share_row_y(true)` not reaching the figure; in `y_fit`, no frame between `fit_all()` and the read (the probe restructures to pump one) or a group that did not follow the flag | The run ends with the usual `gtk warnings: 0` and no `Gtk-CRITICAL`. `x_equal=1` and `cursors_aligned=1` on every state line are the only readings of the shared time window across cells. diff --git a/help/C/org.udpscope.app/cursors.page b/help/C/org.udpscope.app/cursors.page index b02eb20..44e2b41 100644 --- a/help/C/org.udpscope.app/cursors.page +++ b/help/C/org.udpscope.app/cursors.page @@ -44,7 +44,8 @@ any row moves it on all of them. That is what makes dt meaningful across rows.

A ruler belongs to its row and nothing shares it, so two rows' rulers are - independent.

+ independent. The plots of a split row are one row here: they share their y axis, so the + rulers are at the same values in each.

mix is the reading mode: one line per bound trace, with the trace's own values at each marker and their difference. A slot with a name but no channel behind it is not a trace and gets no line.

diff --git a/help/C/org.udpscope.app/layout.page b/help/C/org.udpscope.app/layout.page index 89d4572..6da9251 100644 --- a/help/C/org.udpscope.app/layout.page +++ b/help/C/org.udpscope.app/layout.page @@ -57,7 +57,9 @@

All the plots of a row, and of the whole figure, share one time window: zooming or panning along time in any of them moves all of them, and the vertical cursors are drawn in - every plot. The y axes, the horizontal cursors and the fits belong to each plot. Only the + every plot. The plots of a split row also share one y range: the y labels are drawn at the + left of the first plot only, the horizontal cursors sit at the same values in every plot, and a + zoom, pan, fit or fixed range in any of them applies to the whole row. Only the bottom row carries the time labels. A row holds at most 4 plots and the figure at most 8 in total; the + buttons go grey at the limit.

@@ -66,7 +68,7 @@ Worth knowing

A plot's own right-click menu (the same compact rows as the app's - menu) holds the y-axis scale, the two fits, and Remove signal, which swaps + menu) holds the y-axis scale (for the whole row), the two fits, and Remove signal, which swaps the menu to that plot's traces plus All traces on this plot — with a Back row to return. That is how a trace comes off a row without going through the layout editor.