fix(plot): a crosshair readout quotes a sample, never a value between two
The readout's y was a linear interpolation between the two samples adjacent to the pointer, which answers "what is the signal at this instant" -- a number the series does not contain. On a source whose samples are already averaged or decimated at ingest that is a value nothing measured, and at a zoom past the sample spacing it is visibly not the datum under the pointer. `crosshair_span()`'s `t` becomes a selector rather than a blend factor: the nearer end wins, a tie reading the earlier sample (the bias the span already takes for a degenerate pair). `t` is measured on the samples' own x through `neighbours()`/`x_at()`, so "nearer" is exact on a non-uniform clock too, and the lookup still reads level 0, so the value is a stored sample and never an LOD bucket's aggregate. `crosshair_interpolate()` had no other caller and is gone. `CrosshairRow::marker` is placed from the same y, so the dot follows the reported sample: past the sample spacing it sits beside the polyline rather than on it, deliberately -- one y feeding both keeps the mark and the number from ever disagreeing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
26a7c82a80
commit
c028f48fa0
10 files changed
+150
-105
No files matched your search
@@ -166,10 +166,10 @@ series therefore stages O(visible pixels) vertices, not O(samples), and `FrameSt
|
||||
which path each trace took. Below the threshold the raw samples are drawn instead; both
|
||||
sides go through the same instanced anti-aliased quad pipeline.
|
||||
|
||||
## Exact versus interpolated
|
||||
## One sample or a statistic of many
|
||||
|
||||
The library deliberately holds two conventions next to each other, and they do **not**
|
||||
agree:
|
||||
Every number the library reports is one the samples actually hold. What differs between
|
||||
these two is *how many* samples answer the question:
|
||||
|
||||
- **`Series::reduce` / `sum` / `sumsq` are exact over a sample range.** They answer "what
|
||||
are the samples' extremes / sum / sum of squares over `[begin, end)`". Each is
|
||||
@@ -179,15 +179,18 @@ agree:
|
||||
and no query allocates. `Measure::evaluate` and `Plot::region_stats` are built on these,
|
||||
never on the `*_buckets` forms, which would silently report wrong extremes for an
|
||||
unaligned interval.
|
||||
- **The crosshair readout is interpolated, and says so.** `crosshair_span(series, x)`
|
||||
returns the two samples `x` lies between (`begin`, `end`, `t`), and
|
||||
`crosshair_interpolate(y_begin, y_end, t)` does a linear interpolation: it answers "what
|
||||
is the signal *at* this instant", which is generally between two samples — a different
|
||||
question from an interval statistic. The lookup is O(1) (one division and a floor) and
|
||||
allocation-free; `Plot::crosshair_value(trace, x, out_y)` exposes the same number to a
|
||||
host. A row whose trace has no sample at the crosshair's x shows `kCrosshairNoValue`
|
||||
(an em dash, `"—"`), and an x outside the series window yields no value rather than the
|
||||
nearest sample's.
|
||||
- **The crosshair readout is a single sample — the nearest one.** `crosshair_span(series,
|
||||
x)` returns the two samples `x` lies between (`begin`, `end`, `t`), and the readout takes
|
||||
whichever of the two is nearer: `t <= 0.5` the earlier, otherwise the later. It answers
|
||||
"which datum is under the pointer", not "what is the signal *at* this instant" — a
|
||||
linearly interpolated y is a number the series does not contain, and on a source whose
|
||||
samples are already averaged or decimated at ingest it is a claim about a signal nobody
|
||||
measured. The lookup is O(1) and allocation-free, and it reads level 0, so the value is a
|
||||
stored sample and never an LOD bucket's aggregate; `Plot::crosshair_value(trace, x,
|
||||
out_y)` exposes the same number to a host, and `CrosshairRow::marker` is placed from that
|
||||
same y, so the dot and the number cannot disagree. A row whose trace has no sample at the
|
||||
crosshair's x shows `kCrosshairNoValue` (an em dash, `"—"`), and an x outside the series
|
||||
window yields no value rather than clamping to the first or last sample.
|
||||
|
||||
`Measurement` (from `Plot::measurement()`) documents `integral` as a **left-Riemann sum**,
|
||||
`sum(begin, end) * dx` — the samples taken as the value at the left edge of each step. That
|
||||
|
||||
+4
-3
@@ -113,9 +113,10 @@ across the figure, and `Figure::set_crosshair(x, hovered_row, y_in_row, active)`
|
||||
horizontal line **only in the row the pointer is in** — a figure's rows share x and not y, so
|
||||
a figure-wide horizontal line would be a value that means something different in each row.
|
||||
|
||||
Readout values are **interpolated**, not interval statistics (see
|
||||
[data-and-performance.md](data-and-performance.md)); a row whose trace has no sample at the
|
||||
crosshair's x is listed with the em dash `"—"` and no marker, rather than a fabricated value.
|
||||
Readout values are the **nearest sample's**, neither interpolated between two samples nor an
|
||||
interval statistic (see [data-and-performance.md](data-and-performance.md)); a row whose trace
|
||||
has no sample at the crosshair's x is listed with the em dash `"—"` and no marker, rather than a
|
||||
fabricated value.
|
||||
|
||||
## The gesture table (exactly as implemented)
|
||||
|
||||
|
||||
Reference in new issue
Block a user