docs: split rows share one y axis

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-11-04 01:04:21 +01:00
1 parent b8177cc25a
commit fa9f0c6da9
8 files changed
+86 -40

No files matched your search

+12 -7
View File
@@ -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 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 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 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 `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 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 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 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 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 plot; and one pair of vertical cursors, at the same two times in every plot. The plots of a row
own **y axis** (auto or fixed), its own horizontal rulers and its own fit; a wheel or an axis drag acts on also share **one y range**, the way the rows share one x window: the row has one y axis (auto or fixed),
the plot under the pointer. Only the **bottom** row's plots carry the time labels, and the left margin 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 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`): 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 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 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 '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
`row N`, counted from 0 as before) show. A signal dragged from its badge lands on the plot under the 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` 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"). on each `[[plot]]`, see "The session layout file").
+6 -4
View File
@@ -87,10 +87,12 @@ up to **4** plots, and the figure up to **8** in all.
of one plot it removes the row. 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 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 sits at the same time in each, and the vertical cursors are the same two times in each. The plots of a split
its **own y axis**, its own horizontal rulers and its own fit, and only the bottom row shows the time 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 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 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. 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: 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); - **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; - **both** — the two together;
- **mix** — the two times, read off each *trace* where they cross it. - **mix** — the two times, read off each *trace* where they cross it.
+2 -1
View File
@@ -266,7 +266,8 @@ Layers to keep straight:
`figure_row_count()` counts the rows. The figure owns `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), 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 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 - `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 snapshot or the live rings and handed to the `.npy`/`.csv` writers. Always the stored
+22
View File
@@ -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 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 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. (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 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 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`. the `figure_cell*` cases in `tests/test_figure.cpp` and `glfw_host_routes_by_cell`.
+27 -14
View File
@@ -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`). 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 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)). (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 - **One y per figure row.** The cells of a figure row share one y range (the plotter's
fit; a wheel or axis drag acts on the cell under the pointer, an x gesture on any bottom-row cell `Figure::set_share_row_y(true)`, which `rebuild_all()` sets before the cells are populated): one y
moves the shared window. The cursors pane's ruler label is `plot 2a` for a plot of a split row and axis (auto or fixed), one pair of horizontal rulers, one fit. A wheel or axis drag in any cell moves
`row N` (the plot index) for one of an unsplit row, so the pane reads the same as before until a row the row, an x gesture on any bottom-row cell moves the shared window. The host therefore acts on the
is split. **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 - **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 `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. 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 - **One left gutter, on the first cell only.** The y gutter is one maximum for the whole figure with the
as before (`Figure::set_min_y_gutter()`), **not a per-column one**: a per-column gutter would make a same reserved floor as before (`Figure::set_min_y_gutter()`). It is carried by the first cell of each
cell's width depend on its labels, which "layout must not depend on content" forbids. A cell whose y row alone: `Figure::layout` gives every other cell `YStrip::None` from its column index (structurally,
labels are narrower than the gutter simply has a wider margin. never from what it holds), which forces its y label width, title width and reserved floor to 0. A
- **Widths are weights with a floor.** The plotter keeps 64 px logical per cell and a column gap `None` cell contributes 0 to the common maximum, so a split row's non-first cells cannot widen the
(`kColGap`, 6 px); the editor's drag is stricter (below). A weight is a share of what is gutter and a row's gutter does not depend on how many cells it has. A per-column gutter sized by the
left *after* the floors, so cells of `{3, 1}` read 533:220 in a 756 px figure, not 3:1, and the labels would make a cell's width depend on its content, which "layout must not depend on content"
editor's 96 px drag floor is a floor on the *width*, not on the share. Because a cell's width is 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 `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* 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 (`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). `[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 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 (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 `kMinCellWidthLogical`; both are logical px (the plotter scales it by `ui_scale`, which is
+11 -11
View File
@@ -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] 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] 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=[533,220] tile=1 x_equal=1 cursors_aligned=1 weights=[3,1] 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] 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] 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] 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 [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 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: 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=[..]` `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 none), `traces=[label:signals|..]` with the labels `plot_label()` gives (`1`, `1a`, `2b`). Every state line
carries the same three assertions: 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 | | 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 | | `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=[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 | | `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) | | `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-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 | | `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 | | `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-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 | | `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-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 | | `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 | | `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 | | `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 | | `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=[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]` | | `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 | | `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 | | `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) | | `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 | | `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 | | `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 <figure row, from 1>` and sits on its first cell; an unsplit row keeps `row <plot index>` (**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 | | `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 <figure row, from 1>` and sits on its first cell; an unsplit row keeps `row <plot index>` (**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 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. the only readings of the shared time window across cells.
+2 -1
View File
@@ -44,7 +44,8 @@
any row moves it on all of them. That is what makes <code>dt</code> meaningful across any row moves it on all of them. That is what makes <code>dt</code> meaningful across
rows.</p></item> rows.</p></item>
<item><p>A ruler belongs to its row and nothing shares it, so two rows' rulers are <item><p>A ruler belongs to its row and nothing shares it, so two rows' rulers are
independent.</p></item> 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.</p></item>
<item><p><gui>mix</gui> is the reading mode: one line per <em>bound</em> trace, with the <item><p><gui>mix</gui> is the reading mode: one line per <em>bound</em> trace, with the
trace's own values at each marker and their difference. A slot with a name but no 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.</p></item> channel behind it is not a trace and gets no line.</p></item>
+4 -2
View File
@@ -57,7 +57,9 @@
</steps> </steps>
<p>All the plots of a row, and of the whole figure, share one time window: zooming or <p>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 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 bottom row carries the time labels. A row holds at most 4 plots and the figure at most 8
in total; the <gui>+</gui> buttons go grey at the limit.</p> in total; the <gui>+</gui> buttons go grey at the limit.</p>
</section> </section>
@@ -66,7 +68,7 @@
<title>Worth knowing</title> <title>Worth knowing</title>
<list> <list>
<item><p>A plot's own <gui>right-click menu</gui> (the same compact rows as the app's <item><p>A plot's own <gui>right-click menu</gui> (the same compact rows as the app's
menu) holds the y-axis scale, the two fits, and <gui>Remove signal</gui>, which swaps menu) holds the y-axis scale (for the whole row), the two fits, and <gui>Remove signal</gui>, which swaps
the menu to that plot's traces plus <gui>All traces on this plot</gui> — with a the menu to that plot's traces plus <gui>All traces on this plot</gui> — with a
<gui>Back</gui> row to return. That is how a trace comes off a row without going through <gui>Back</gui> row to return. That is how a trace comes off a row without going through
the layout editor.</p></item> the layout editor.</p></item>