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