From d03c9fb443f9e1a7d59d1a26552e081dc04eef87 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Mon, 5 Oct 2026 13:45:21 +0200 Subject: [PATCH] fix: a cursor readout quotes the nearest stored sample, not a value between two The crosshair and the cursors pane's mix values are one library call, and it interpolated linearly between the two samples bracketing the pointer. That is wrong twice over here. Ingest decimation is a boxcar average over `time_stride` samples, so a number computed between two stored samples is a second, undeclared estimate laid over the first that nothing on screen or in an export holds. And past the sample spacing the polyline is a line the renderer drew, so quoting a point on it reports the drawing rather than the data. The fix is upstream and unconditional (plotter c028f48): `crosshair_span()`'s `t` selects the nearer end instead of blending, with a tie reading the earlier sample. `CrosshairRow::marker` is placed from the same y, so the dot follows the quoted sample. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 13 +++++++++++++ README.md | 22 ++++++++++++++++++---- deps/plotter | 2 +- src/channel.hpp | 4 ++-- src/help/09-cursors.txt | 3 +++ 5 files changed, 37 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f2915cc..67de04b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -749,6 +749,19 @@ far the largest file. again until the next latch. `PlotArea::create()` is handed `App::placeholder_plot`, never a row: the area keeps a reference to the plot it was created with, and a rebuild destroys every row. +- **A readout quotes a stored sample, never a value between two of them.** The hover + crosshair and the cursors pane's `mix` values are one call, `Plot::crosshair_value()`, and it + reports the **nearer** of the two samples bracketing the x (a tie reads the earlier). Upstream + interpolated linearly, and that is wrong twice over here: ingest decimation is a *boxcar + average* over `time_stride` samples, so a number between two stored samples is a second + estimate laid over the first that nothing on screen or in an export holds; and past the sample + spacing the polyline is a line the renderer drew, so a point on it reports the drawing. The + change is in `deps/plotter` and unconditional — no setting, because the library had no business + offering a readout the data does not contain. `CrosshairRow::marker` is placed from the same y, + so the dot lands on the quoted sample and **sits beside the polyline at a deep zoom, by + design**: one y feeding both is what keeps the mark and the number from disagreeing. The guards + are the library's `crosshair_value_is_the_nearest_sample` and `[cursors-probe]`'s `first mark at + x=.. y=..` agreeing with the pane's first value. - **The cursors are read back from the plots only while a gesture is on that row.** `Cursor::x` is the *free* pointer position while a drag runs and the snapped value otherwise -- which is what the app itself wrote a frame earlier, or the snap of it. A tick that reads a row on diff --git a/README.md b/README.md index 66988ef..eb2a9a2 100644 --- a/README.md +++ b/README.md @@ -1139,10 +1139,24 @@ is what made a dragged cursor spring to where it had been. A ruler is per row: e two different values, and the pane names the row. `mix` needs no rulers: it reports the value the trace actually has at each marker, read -through the library's `Plot::crosshair_value()` — the two bracketing samples, -interpolated — so it is the answer to "what is this signal at these two times" rather -than "drag a ruler until it crosses the trace". A readout is `--` when the trace has -nothing at that time and `partial` when only one of the two markers is over samples. +through the library's `Plot::crosshair_value()` — **the nearer of the two bracketing +samples**, never a number between them — so it is the answer to "which datum of this +signal is at these two times" rather than "drag a ruler until it crosses the trace". A +readout is `--` when the trace has nothing at that time and `partial` when only one of +the two markers is over samples. + +The value used to be a linear interpolation between the brackets, which is upstream's +old convention and was wrong for this app in two ways at once. The samples here are +**already averaged** — ingest decimation is a boxcar over `time_stride` samples (see +"Decimation at ingest is an average") — so a number computed *between* two of them is a +second, undeclared estimate laid over the first, and nothing on screen or in an export +holds it. And at a zoom past the sample spacing it is visibly not the datum the pointer +is over: the polyline between two samples is a straight line the renderer drew, and +quoting a point on it reports the drawing rather than the data. A tie reads the earlier +sample. The fix is upstream and unconditional (`deps/plotter`, `crosshair_span()`'s `t` +is now a selector rather than a blend factor), so the library's own hover crosshair +changed with the pane — which is the point, since both are the same lookup and a user +comparing them must not see two numbers. The readouts are a **table**: a header naming the columns, then one row per instrument — `| who | A | B | delta |`. The first row is the two times and their difference, and the diff --git a/deps/plotter b/deps/plotter index 26a7c82..c028f48 160000 --- a/deps/plotter +++ b/deps/plotter @@ -1 +1 @@ -Subproject commit 26a7c82a8050fa5446b0ca73b21203e38e47a916 +Subproject commit c028f48fa0e2499e3e0774b00297297b028378a5 diff --git a/src/channel.hpp b/src/channel.hpp index cf22750..d301fc4 100644 --- a/src/channel.hpp +++ b/src/channel.hpp @@ -73,8 +73,8 @@ class ChannelBuffer final : public plot::data::Series { /// datagram leaves a gap, so index times must come from the time ring. f64 x_at(u64 i) const override; /// Bracketing samples by binary search over the time ring, which is monotonic, - /// so a crosshair value is interpolated between the two samples actually either - /// side of the pointer -- a grid walk would land on the wrong pair after a gap. + /// so a crosshair reads whichever of the two samples actually either side of the + /// pointer is nearer -- a grid walk would land on the wrong pair after a gap. bool neighbours(f64 x, u64& i0, u64& i1) const override; u64 snap_index(f64 x) const override; plot::Range x_bounds() const override; diff --git a/src/help/09-cursors.txt b/src/help/09-cursors.txt index b2a6a20..e7960ef 100644 --- a/src/help/09-cursors.txt +++ b/src/help/09-cursors.txt @@ -19,6 +19,9 @@ owns a Reset button. Drag a marker to move it. The mode dropdown offers f • 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. +• A value is the nearest stored sample's, never a number between two samples. Zoom in past + the sample spacing and the line between two samples is something the plot drew, not data — + so a reading off it would be the drawing. The hover crosshair reads the same way. • A marker past the newest sample reads --. With only one marker over data the row reads partial: a difference between a number and nothing is not a number. • In mix each marker also places a crossing annotation per trace, which is what lets