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 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-10-05 13:45:21 +02:00
1 parent ec93a9831d
commit d03c9fb443
5 files changed
+37 -7

No files matched your search

+13
View File
@@ -749,6 +749,19 @@ far the largest file.
again until the next latch. `PlotArea::create()` is handed `App::placeholder_plot`, 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 never a row: the area keeps a reference to the plot it was created with, and a rebuild
destroys every row. 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` - **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 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 what the app itself wrote a frame earlier, or the snap of it. A tick that reads a row on
+18 -4
View File
@@ -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. 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 `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, through the library's `Plot::crosshair_value()` — **the nearer of the two bracketing
interpolated — so it is the answer to "what is this signal at these two times" rather samples**, never a number between them — so it is the answer to "which datum of this
than "drag a ruler until it crosses the trace". A readout is `--` when the trace has signal is at these two times" rather than "drag a ruler until it crosses the trace". A
nothing at that time and `partial` when only one of the two markers is over samples. 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 — 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 `| who | A | B | delta |`. The first row is the two times and their difference, and the
+1 -1
+2 -2
View File
@@ -73,8 +73,8 @@ class ChannelBuffer final : public plot::data::Series {
/// datagram leaves a gap, so index times must come from the time ring. /// datagram leaves a gap, so index times must come from the time ring.
f64 x_at(u64 i) const override; f64 x_at(u64 i) const override;
/// Bracketing samples by binary search over the time ring, which is monotonic, /// Bracketing samples by binary search over the time ring, which is monotonic,
/// so a crosshair value is interpolated between the two samples actually either /// so a crosshair reads whichever of the two samples actually either side of the
/// side of the pointer -- a grid walk would land on the wrong pair after a gap. /// 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; bool neighbours(f64 x, u64& i0, u64& i1) const override;
u64 snap_index(f64 x) const override; u64 snap_index(f64 x) const override;
plot::Range x_bounds() const override; plot::Range x_bounds() const override;
+3
View File
@@ -19,6 +19,9 @@ owns a <b>Reset</b> button. Drag a marker to move it. The mode dropdown offers f
• <tt>mix</tt> is the reading mode: one line per <i>bound</i> trace, with the trace's own values • <tt>mix</tt> is the reading mode: one line per <i>bound</i> 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 at each marker and their difference. A slot with a name but no channel behind it is not a
trace and gets no line. trace and gets no line.
• A value is the <i>nearest stored sample's</i>, 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 <tt>--</tt>. With only one marker over data the row • A marker past the newest sample reads <tt>--</tt>. With only one marker over data the row
reads <tt>partial</tt>: a difference between a number and nothing is not a number. reads <tt>partial</tt>: a difference between a number and nothing is not a number.
• In <tt>mix</tt> each marker also places a crossing annotation per trace, which is what lets • In <tt>mix</tt> each marker also places a crossing annotation per trace, which is what lets