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
@@ -47,7 +47,7 @@ run touched a GPU, so read it as an upper bound and a record of the software pat
|
|||||||
- cursors in both orientations — vertical markers and horizontal rulers;
|
- cursors in both orientations — vertical markers and horizontal rulers;
|
||||||
- draggable regions and measurement bands with exact interval statistics;
|
- draggable regions and measurement bands with exact interval statistics;
|
||||||
- a signal legend anchored to an axes corner;
|
- a signal legend anchored to an axes corner;
|
||||||
- a hover crosshair with a per-signal, interpolated readout;
|
- a hover crosshair with a per-signal readout of the nearest sample's value;
|
||||||
- a multi-plot `Figure` of stacked rows sharing one synchronised x axis;
|
- a multi-plot `Figure` of stacked rows sharing one synchronised x axis;
|
||||||
- live follow (`XFollow::Live`) with relative (multiplicative) zoom and mouse panning;
|
- live follow (`XFollow::Live`) with relative (multiplicative) zoom and mouse panning;
|
||||||
- XY series for irregularly sampled data (`plot/data/xy_series.hpp`): sorted-ascending x
|
- XY series for irregularly sampled data (`plot/data/xy_series.hpp`): sorted-ascending x
|
||||||
|
|||||||
@@ -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
|
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.
|
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**
|
Every number the library reports is one the samples actually hold. What differs between
|
||||||
agree:
|
these two is *how many* samples answer the question:
|
||||||
|
|
||||||
- **`Series::reduce` / `sum` / `sumsq` are exact over a sample range.** They answer "what
|
- **`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
|
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,
|
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
|
never on the `*_buckets` forms, which would silently report wrong extremes for an
|
||||||
unaligned interval.
|
unaligned interval.
|
||||||
- **The crosshair readout is interpolated, and says so.** `crosshair_span(series, x)`
|
- **The crosshair readout is a single sample — the nearest one.** `crosshair_span(series,
|
||||||
returns the two samples `x` lies between (`begin`, `end`, `t`), and
|
x)` returns the two samples `x` lies between (`begin`, `end`, `t`), and the readout takes
|
||||||
`crosshair_interpolate(y_begin, y_end, t)` does a linear interpolation: it answers "what
|
whichever of the two is nearer: `t <= 0.5` the earlier, otherwise the later. It answers
|
||||||
is the signal *at* this instant", which is generally between two samples — a different
|
"which datum is under the pointer", not "what is the signal *at* this instant" — a
|
||||||
question from an interval statistic. The lookup is O(1) (one division and a floor) and
|
linearly interpolated y is a number the series does not contain, and on a source whose
|
||||||
allocation-free; `Plot::crosshair_value(trace, x, out_y)` exposes the same number to a
|
samples are already averaged or decimated at ingest it is a claim about a signal nobody
|
||||||
host. A row whose trace has no sample at the crosshair's x shows `kCrosshairNoValue`
|
measured. The lookup is O(1) and allocation-free, and it reads level 0, so the value is a
|
||||||
(an em dash, `"—"`), and an x outside the series window yields no value rather than the
|
stored sample and never an LOD bucket's aggregate; `Plot::crosshair_value(trace, x,
|
||||||
nearest sample's.
|
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**,
|
`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
|
`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
|
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.
|
a figure-wide horizontal line would be a value that means something different in each row.
|
||||||
|
|
||||||
Readout values are **interpolated**, not interval statistics (see
|
Readout values are the **nearest sample's**, neither interpolated between two samples nor an
|
||||||
[data-and-performance.md](data-and-performance.md)); a row whose trace has no sample at the
|
interval statistic (see [data-and-performance.md](data-and-performance.md)); a row whose trace
|
||||||
crosshair's x is listed with the em dash `"—"` and no marker, rather than a fabricated value.
|
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)
|
## The gesture table (exactly as implemented)
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ class Series {
|
|||||||
/// lands exactly on a sample). False when x lies outside the series' x
|
/// lands exactly on a sample). False when x lies outside the series' x
|
||||||
/// bounds; `i0`/`i1` are then untouched. The crosshair and measurement
|
/// bounds; `i0`/`i1` are then untouched. The crosshair and measurement
|
||||||
/// paths read coordinates only through this and `x_at()`, so a series with
|
/// paths read coordinates only through this and `x_at()`, so a series with
|
||||||
/// its own x array interpolates exactly.
|
/// its own x array brackets exactly.
|
||||||
virtual bool neighbours(f64 x, u64& i0, u64& i1) const;
|
virtual bool neighbours(f64 x, u64& i0, u64& i1) const;
|
||||||
|
|
||||||
/// Index of the sample nearest data-space `x`, clamped to `[0, size()-1]`;
|
/// Index of the sample nearest data-space `x`, clamped to `[0, size()-1]`;
|
||||||
|
|||||||
@@ -44,10 +44,11 @@ struct CrosshairStyle {
|
|||||||
/// drawn -- the single-coordinate `Plot::set_crosshair()` leaves the vertical one only,
|
/// drawn -- the single-coordinate `Plot::set_crosshair()` leaves the vertical one only,
|
||||||
/// and a `Figure` is always vertical-only because its rows share x and not y.
|
/// and a `Figure` is always vertical-only because its rows share x and not y.
|
||||||
///
|
///
|
||||||
/// **The readout's values are interpolated, and deliberately so.** The y a row reports for
|
/// **The readout's values are samples.** The y a row reports for a trace is the y of
|
||||||
/// a trace is *linear interpolation between the two samples adjacent to `x`*, not a sample
|
/// whichever of the two samples adjacent to `x` is **nearer** to it -- not a value
|
||||||
/// and not an interval statistic -- see `crosshair_span()` for the rule stated in full, and
|
/// interpolated between them, and not an interval statistic. See `crosshair_span()` for the
|
||||||
/// for why it is the opposite convention from `Series::reduce()`/`sum()`/`sumsq()`.
|
/// rule stated in full, and for how it differs from
|
||||||
|
/// `Series::reduce()`/`sum()`/`sumsq()`.
|
||||||
struct Crosshair {
|
struct Crosshair {
|
||||||
/// Whether the crosshair is drawn at all. Set by `Plot::set_crosshair()`; cleared by
|
/// Whether the crosshair is drawn at all. Set by `Plot::set_crosshair()`; cleared by
|
||||||
/// `Plot::clear_crosshair()`, which `input::Controller` calls when the pointer leaves the
|
/// `Plot::clear_crosshair()`, which `input::Controller` calls when the pointer leaves the
|
||||||
@@ -58,7 +59,7 @@ struct Crosshair {
|
|||||||
/// Draw the horizontal line (the row of `y`). On for the two-coordinate overload only:
|
/// Draw the horizontal line (the row of `y`). On for the two-coordinate overload only:
|
||||||
/// a crosshair placed at one coordinate has no second value to mark a row with.
|
/// a crosshair placed at one coordinate has no second value to mark a row with.
|
||||||
bool horizontal = true;
|
bool horizontal = true;
|
||||||
/// List the visible traces and their interpolated values in a panel beside the pointer.
|
/// List the visible traces and their values in a panel beside the pointer.
|
||||||
/// On by default, and free to leave on: a plot with no visible trace has no rows to
|
/// On by default, and free to leave on: a plot with no visible trace has no rows to
|
||||||
/// measure and the panel is then not placed at all.
|
/// measure and the panel is then not placed at all.
|
||||||
bool show_readout = true;
|
bool show_readout = true;
|
||||||
@@ -84,14 +85,19 @@ struct Crosshair {
|
|||||||
/// arithmetic, which is O(1) in the series' length. The crosshair runs on every pointer
|
/// arithmetic, which is O(1) in the series' length. The crosshair runs on every pointer
|
||||||
/// move, so that is a requirement rather than a nicety.
|
/// move, so that is a requirement rather than a nicety.
|
||||||
///
|
///
|
||||||
/// **Interpolated, not exact.** The value a caller builds from this span by
|
/// **`t` chooses between the two samples; it does not blend them.** The value a caller
|
||||||
/// `crosshair_interpolate()` between the two samples is the signal *at* `x`, which is
|
/// builds from this span is the y of whichever end `t` is nearer to (`Plot::crosshair_value()`
|
||||||
/// generally between two samples -- the opposite convention from `Series::reduce()`,
|
/// takes `t <= 0.5` as `begin`), so a readout quotes a sample the series actually holds. It
|
||||||
/// `sum()` and `sumsq()`, which are **exact over a sample range**. The two conventions sit
|
/// used to be a linear interpolation between the two, which reads as "the signal at this
|
||||||
/// next to each other in this library on purpose and they do not agree: `reduce()` answers
|
/// instant" -- a number that is *between* the data and therefore in no row of it. On a
|
||||||
/// "what are the samples' extremes over this interval", this answers "what is the signal at
|
/// decimated or averaged source that is a value nothing measured, and at a zoom past the
|
||||||
/// this instant". A readout that pretended to be exact would be quoting an extreme or a
|
/// sample spacing it is visibly not the datum the pointer is over.
|
||||||
/// mean of a window as if it were the value under the pointer.
|
///
|
||||||
|
/// So `t` is a *selector*, and this remains a different convention from
|
||||||
|
/// `Series::reduce()`, `sum()` and `sumsq()`, which are **exact over a sample range**: those
|
||||||
|
/// answer "what are the samples' extremes/total over this interval", this answers "which
|
||||||
|
/// sample is under the pointer, and what is its value". Both now quote the data; what differs
|
||||||
|
/// is whether the answer is one sample or a statistic of many.
|
||||||
///
|
///
|
||||||
/// `valid` is false -- and every field left at its default -- for an **empty series**, a
|
/// `valid` is false -- and every field left at its default -- for an **empty series**, a
|
||||||
/// non-positive `dx()`, a non-finite `x`, and an `x` strictly outside the series' window
|
/// non-positive `dx()`, a non-finite `x`, and an `x` strictly outside the series' window
|
||||||
@@ -104,13 +110,14 @@ struct CrosshairSpan {
|
|||||||
/// Index of the first sample whose x is at or after the query: `begin` when the query
|
/// Index of the first sample whose x is at or after the query: `begin` when the query
|
||||||
/// lands on a sample, `begin + 1` between two of them.
|
/// lands on a sample, `begin + 1` between two of them.
|
||||||
u64 end = 0;
|
u64 end = 0;
|
||||||
/// The query's position between the two samples, in `[0, 1]`.
|
/// The query's position between the two samples, in `[0, 1]`. A *selector*, not a blend
|
||||||
|
/// factor -- see the struct comment.
|
||||||
f64 t = 0.0;
|
f64 t = 0.0;
|
||||||
bool valid = false;
|
bool valid = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The sample span `x` falls in, on `series`; see `CrosshairSpan` for the rule, for the
|
/// The sample span `x` falls in, on `series`; see `CrosshairSpan` for the rule, for the
|
||||||
/// interpolation convention it is half of, and for the cases that yield `valid == false`.
|
/// convention it is half of, and for the cases that yield `valid == false`.
|
||||||
///
|
///
|
||||||
/// O(1) and allocation-free: a subtraction, a division and a truncation. The series' own sample
|
/// O(1) and allocation-free: a subtraction, a division and a truncation. The series' own sample
|
||||||
/// fetch is the caller's -- `Plot::crosshair_value()` reads the span through
|
/// fetch is the caller's -- `Plot::crosshair_value()` reads the span through
|
||||||
@@ -118,13 +125,6 @@ struct CrosshairSpan {
|
|||||||
/// allocates.
|
/// allocates.
|
||||||
CrosshairSpan crosshair_span(const data::Series& series, f64 x) noexcept;
|
CrosshairSpan crosshair_span(const data::Series& series, f64 x) noexcept;
|
||||||
|
|
||||||
/// The interpolated y at `t` between two samples: `y_begin` at `t == 0`, `y_end` at
|
|
||||||
/// `t == 1`. The one place the interpolation's arithmetic lives, so the lookup's rule and
|
|
||||||
/// its documentation cannot drift apart.
|
|
||||||
inline f64 crosshair_interpolate(f64 y_begin, f64 y_end, f64 t) noexcept {
|
|
||||||
return y_begin + (y_end - y_begin) * t;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One trace's line of the readout: which trace it is about, the value under the pointer
|
/// One trace's line of the readout: which trace it is about, the value under the pointer
|
||||||
/// and the boxes the renderer stages.
|
/// and the boxes the renderer stages.
|
||||||
///
|
///
|
||||||
@@ -150,7 +150,7 @@ struct CrosshairRow {
|
|||||||
/// entries -- so a row that is not listed is still a listed *trace*, with a marker, just
|
/// entries -- so a row that is not listed is still a listed *trace*, with a marker, just
|
||||||
/// not a line in a panel that has no room for it.
|
/// not a line in a panel that has no room for it.
|
||||||
bool listed = false;
|
bool listed = false;
|
||||||
/// The trace's interpolated y at the crosshair's x, on the trace's own y axis. Only
|
/// The y of the trace's sample nearest the crosshair's x, on the trace's own y axis. Only
|
||||||
/// meaningful when `valid`.
|
/// meaningful when `valid`.
|
||||||
f64 y = 0.0;
|
f64 y = 0.0;
|
||||||
/// The trace's name, or the positional placeholder of an unnamed one.
|
/// The trace's name, or the positional placeholder of an unnamed one.
|
||||||
@@ -161,9 +161,12 @@ struct CrosshairRow {
|
|||||||
/// `kCrosshairNoValue`, the em dash. The absence is therefore *legible* -- a blank cell reads as
|
/// `kCrosshairNoValue`, the em dash. The absence is therefore *legible* -- a blank cell reads as
|
||||||
/// "still loading" rather than as "nothing here" -- and nothing numeric is fabricated for it.
|
/// "still loading" rather than as "nothing here" -- and nothing numeric is fabricated for it.
|
||||||
std::string value;
|
std::string value;
|
||||||
/// The trace's intersection with the crosshair, in framebuffer pixels: the pen the
|
/// Where the renderer draws the marker, in framebuffer pixels: the pen for `y`, placed
|
||||||
/// renderer draws the marker at. Placed through the *trace's own* axis pair, so the
|
/// through the *trace's own* axis pair, so a trace on a secondary y axis is marked on the
|
||||||
/// marker is where that trace is actually drawn. Meaningless when `!valid`.
|
/// scale it is drawn against. It is the nearest **sample**, so at a zoom past the sample
|
||||||
|
/// spacing the marker sits beside the polyline rather than on it -- deliberately: it marks
|
||||||
|
/// the datum the row reports, and one y feeding both keeps the dot and the number from ever
|
||||||
|
/// disagreeing. Meaningless when `!valid`.
|
||||||
Vec2 marker{};
|
Vec2 marker{};
|
||||||
/// The swatch's box, `Legend::swatch_w` x `Legend::swatch_h` centred in the row. Empty
|
/// The swatch's box, `Legend::swatch_w` x `Legend::swatch_h` centred in the row. Empty
|
||||||
/// when `!listed`.
|
/// when `!listed`.
|
||||||
|
|||||||
@@ -101,8 +101,8 @@ struct PlotLayout {
|
|||||||
/// where a pointer can grab it cannot drift apart.
|
/// where a pointer can grab it cannot drift apart.
|
||||||
std::vector<RegionLayout> regions;
|
std::vector<RegionLayout> regions;
|
||||||
/// The crosshair's geometry, when one is active: the two lines' stroke boxes, the
|
/// The crosshair's geometry, when one is active: the two lines' stroke boxes, the
|
||||||
/// readout panel and one row per visible trace with the interpolated value the pointer
|
/// readout panel and one row per visible trace with the value of the sample nearest the
|
||||||
/// is over. Placed here rather than derived by the renderer, exactly as the legend and
|
/// pointer. Placed here rather than derived by the renderer, exactly as the legend and
|
||||||
/// the regions are -- and, like them, entirely inside `axes`: the panel is flipped and
|
/// the regions are -- and, like them, entirely inside `axes`: the panel is flipped and
|
||||||
/// clamped against the rect it was measured for, so nothing of the readout can be drawn
|
/// clamped against the rect it was measured for, so nothing of the readout can be drawn
|
||||||
/// over a label strip or into a figure's neighbouring row.
|
/// over a label strip or into a figure's neighbouring row.
|
||||||
@@ -693,10 +693,13 @@ class Plot {
|
|||||||
/// coordinates it holds and the geometry `layout()` places from them cannot disagree.
|
/// coordinates it holds and the geometry `layout()` places from them cannot disagree.
|
||||||
const Crosshair& crosshair() const noexcept { return crosshair_; }
|
const Crosshair& crosshair() const noexcept { return crosshair_; }
|
||||||
|
|
||||||
/// The **interpolated** value of `trace`'s series at a data `x`: `crosshair_span()`
|
/// The value of `trace`'s **nearest sample** to a data `x`: `crosshair_span()` brackets
|
||||||
/// between the two adjacent samples, then `crosshair_interpolate()` -- the same lookup
|
/// `x`, and the nearer of the two samples is the answer (`t <= 0.5` takes the earlier) --
|
||||||
/// the readout's rows are built from, exposed for a host that wants the number for
|
/// the same lookup the readout's rows are built from, exposed for a host that wants the
|
||||||
/// itself (a status line, a tooltip of its own).
|
/// number for itself (a status line, a tooltip of its own).
|
||||||
|
///
|
||||||
|
/// Never a value interpolated between two samples: see `CrosshairSpan` for why a readout
|
||||||
|
/// quotes the data rather than the signal it implies.
|
||||||
///
|
///
|
||||||
/// False, leaving `out_y` untouched, for an id that names no trace, a trace with no
|
/// False, leaving `out_y` untouched, for an id that names no trace, a trace with no
|
||||||
/// series, and an `x` the series has no sample at -- the cases `CrosshairSpan::valid`
|
/// series, and an `x` the series has no sample at -- the cases `CrosshairSpan::valid`
|
||||||
@@ -816,7 +819,7 @@ class Plot {
|
|||||||
/// never names a trace.
|
/// never names a trace.
|
||||||
void build_legend();
|
void build_legend();
|
||||||
/// Measures the crosshair into `layout_.crosshair`: the two lines' stroke boxes, one row
|
/// Measures the crosshair into `layout_.crosshair`: the two lines' stroke boxes, one row
|
||||||
/// per visible trace with its interpolated value and marker, and the readout panel --
|
/// per visible trace with its nearest sample's value and marker, and the readout panel --
|
||||||
/// placed beside the crosshair, flipped where it would cross an edge and clamped, so
|
/// placed beside the crosshair, flipped where it would cross an edge and clamped, so
|
||||||
/// every rect it produces is inside `axes`. A plot with no visible trace still gets its
|
/// every rect it produces is inside `axes`. A plot with no visible trace still gets its
|
||||||
/// lines: the rows are the readout's, not the crosshair's.
|
/// lines: the rows are the readout's, not the crosshair's.
|
||||||
|
|||||||
+17
-12
@@ -1082,7 +1082,7 @@ const PlotLayout& Plot::layout() {
|
|||||||
build_regions();
|
build_regions();
|
||||||
build_legend();
|
build_legend();
|
||||||
// The crosshair last: its panel is fitted into `axes` and its lines are held inside it, and
|
// The crosshair last: its panel is fitted into `axes` and its lines are held inside it, and
|
||||||
// its rows are the interpolated values at a data x -- so it wants the transforms the
|
// its rows are the sample values nearest a data x -- so it wants the transforms the
|
||||||
// settled layout pins, exactly as the legend and the regions do.
|
// settled layout pins, exactly as the legend and the regions do.
|
||||||
build_crosshair();
|
build_crosshair();
|
||||||
return layout_;
|
return layout_;
|
||||||
@@ -1352,13 +1352,13 @@ void Plot::build_regions() {
|
|||||||
CrosshairSpan crosshair_span(const data::Series& series, f64 x) noexcept {
|
CrosshairSpan crosshair_span(const data::Series& series, f64 x) noexcept {
|
||||||
CrosshairSpan span;
|
CrosshairSpan span;
|
||||||
const u64 n = series.size();
|
const u64 n = series.size();
|
||||||
// No samples and no finite query: nothing to interpolate between.
|
// No samples and no finite query: nothing to bracket.
|
||||||
if (n == 0u || !std::isfinite(x)) return span;
|
if (n == 0u || !std::isfinite(x)) return span;
|
||||||
|
|
||||||
// The bracketing samples, straight from the series (`neighbours()`/`x_at()`):
|
// The bracketing samples, straight from the series (`neighbours()`/`x_at()`):
|
||||||
// the window is closed at both ends -- the oldest and the newest sample are
|
// the window is closed at both ends -- the oldest and the newest sample are
|
||||||
// samples too -- and a query outside it has no neighbours to interpolate
|
// samples too -- and a query outside it has no neighbours at all. Answering
|
||||||
// between. Answering with the nearest sample's value instead is the clamped lie
|
// with the nearest sample's value instead is the clamped lie
|
||||||
// this refuses: the readout would report a number as if it were under the
|
// this refuses: the readout would report a number as if it were under the
|
||||||
// pointer when the series does not reach that far. The series owns where its
|
// pointer when the series does not reach that far. The series owns where its
|
||||||
// samples are, so an XY series is bracketed by its own x array exactly, where
|
// samples are, so an XY series is bracketed by its own x array exactly, where
|
||||||
@@ -1371,8 +1371,8 @@ CrosshairSpan crosshair_span(const data::Series& series, f64 x) noexcept {
|
|||||||
// A query that landed **on** a sample is a span of one, whichever side the
|
// A query that landed **on** a sample is a span of one, whichever side the
|
||||||
// series' bracket reported: a sample at the query is the answer, and the two
|
// series' bracket reported: a sample at the query is the answer, and the two
|
||||||
// ends of the window are samples too -- a bracket of (0, 1) at a query sitting
|
// ends of the window are samples too -- a bracket of (0, 1) at a query sitting
|
||||||
// on sample 0 would read as an interpolation with no width. Decided on the
|
// on sample 0 would read as a span with a width it does not have. Decided on
|
||||||
// samples' own coordinates, so it holds for any `neighbours()` implementation.
|
// the samples' own coordinates, so it holds for any `neighbours()` impl.
|
||||||
if (series.x_at(i0) == x) {
|
if (series.x_at(i0) == x) {
|
||||||
span.begin = span.end = i0;
|
span.begin = span.end = i0;
|
||||||
span.t = 0.0;
|
span.t = 0.0;
|
||||||
@@ -1387,16 +1387,16 @@ CrosshairSpan crosshair_span(const data::Series& series, f64 x) noexcept {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (i0 == i1) {
|
if (i0 == i1) {
|
||||||
// The series bracketed a single sample for a query that is not on it: nothing
|
// The series bracketed a single sample for a query that is not on it: there
|
||||||
// to interpolate with, and the sample is the honest answer.
|
// is no second sample to choose between, and this one is the honest answer.
|
||||||
span.begin = span.end = i0;
|
span.begin = span.end = i0;
|
||||||
span.t = 0.0;
|
span.t = 0.0;
|
||||||
} else {
|
} else {
|
||||||
span.begin = i0;
|
span.begin = i0;
|
||||||
span.end = i1;
|
span.end = i1;
|
||||||
// `t` from the samples' own x coordinates, so the interpolation is exact on
|
// `t` from the samples' own x coordinates, so "which is nearer" is exact on
|
||||||
// any grid, uniform or not. A degenerate pair (two samples sharing an x)
|
// any grid, uniform or not. A degenerate pair (two samples sharing an x)
|
||||||
// interpolates nothing and reads the earlier sample.
|
// reads the earlier sample.
|
||||||
const f64 xa = series.x_at(i0);
|
const f64 xa = series.x_at(i0);
|
||||||
const f64 xb = series.x_at(i1);
|
const f64 xb = series.x_at(i1);
|
||||||
span.t = xb > xa ? (x - xa) / (xb - xa) : 0.0;
|
span.t = xb > xa ? (x - xa) / (xb - xa) : 0.0;
|
||||||
@@ -1455,7 +1455,12 @@ bool Plot::crosshair_value(u32 trace, f64 x, f64& out_y) const {
|
|||||||
if (crosshair_scratch_.size() != 2u * pairs) return false; // a series that produced nothing
|
if (crosshair_scratch_.size() != 2u * pairs) return false; // a series that produced nothing
|
||||||
const f64 y_begin = static_cast<f64>(crosshair_scratch_[1u]);
|
const f64 y_begin = static_cast<f64>(crosshair_scratch_[1u]);
|
||||||
const f64 y_end = static_cast<f64>(crosshair_scratch_[2u * pairs - 1u]);
|
const f64 y_end = static_cast<f64>(crosshair_scratch_[2u * pairs - 1u]);
|
||||||
out_y = crosshair_interpolate(y_begin, y_end, span.t);
|
// **The nearer sample, never a value between two of them.** A readout quotes the data: a
|
||||||
|
// number the series does not contain is a claim about a signal between its samples, which
|
||||||
|
// is the caller's to make and not a plot's to put in a panel. `span.t` is measured on the
|
||||||
|
// samples' own x, so "nearer" is exact on any grid; a tie reads the earlier sample, which
|
||||||
|
// is the bias `crosshair_span()` already takes for a degenerate pair.
|
||||||
|
out_y = span.t <= 0.5 ? y_begin : y_end;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1530,7 +1535,7 @@ void Plot::build_crosshair() {
|
|||||||
row.label_box = Rect{};
|
row.label_box = Rect{};
|
||||||
row.value_box = Rect{};
|
row.value_box = Rect{};
|
||||||
row.label.assign(trace.name.empty() ? crosshair_unnamed_label(i) : trace.name);
|
row.label.assign(trace.name.empty() ? crosshair_unnamed_label(i) : trace.name);
|
||||||
// The value: the interpolated number where the trace has a sample at this x, and the em dash
|
// The value: the nearest sample's number where the trace has one at this x, and the em dash
|
||||||
// where it does not. The row is listed either way, so the absence has to be *said* -- a blank
|
// where it does not. The row is listed either way, so the absence has to be *said* -- a blank
|
||||||
// value cell reads as a value that has not arrived -- and the dash is measured into the panel's
|
// value cell reads as a value that has not arrived -- and the dash is measured into the panel's
|
||||||
// width like any other value, so the row's boxes are placed by one rule.
|
// width like any other value, so the row's boxes are placed by one rule.
|
||||||
|
|||||||
+11
-8
@@ -877,8 +877,8 @@ namespace {
|
|||||||
|
|
||||||
/// The figure the crosshair cases hover: three rows at deliberately *different* sample rates on
|
/// The figure the crosshair cases hover: three rows at deliberately *different* sample rates on
|
||||||
/// the one shared x, plus a fourth whose data does not reach the hovered x at all. Rows that
|
/// the one shared x, plus a fourth whose data does not reach the hovered x at all. Rows that
|
||||||
/// sample at different rates are the point -- one x, three different interpolations -- and the
|
/// sample at different rates are the point -- one x, and each row reporting a sample off its own
|
||||||
/// last row is the case a readout must not invent a value for.
|
/// grid -- and the last row is the case a readout must not invent a value for.
|
||||||
plot::Figure crosshair_figure() {
|
plot::Figure crosshair_figure() {
|
||||||
plot::Figure figure(4u);
|
plot::Figure figure(4u);
|
||||||
figure.set_viewport(whole_figure());
|
figure.set_viewport(whole_figure());
|
||||||
@@ -906,7 +906,7 @@ plot::Figure crosshair_figure() {
|
|||||||
|
|
||||||
/// Spec §15, the sync: `Figure::set_crosshair()` applies **one** data x to every row, so one
|
/// Spec §15, the sync: `Figure::set_crosshair()` applies **one** data x to every row, so one
|
||||||
/// hover is one instant and N readouts rather than N independent ones. Rows at different sample
|
/// hover is one instant and N readouts rather than N independent ones. Rows at different sample
|
||||||
/// rates therefore report *their own* interpolations of the same x -- the shared axis is x, and
|
/// rates therefore report *their own* nearest sample to the same x -- the shared axis is x, and
|
||||||
/// each row's series is read on its own grid -- while a row whose series does not reach that x
|
/// each row's series is read on its own grid -- while a row whose series does not reach that x
|
||||||
/// reports no value at all instead of a fabricated one.
|
/// reports no value at all instead of a fabricated one.
|
||||||
PLOT_TEST(figure_crosshair_applies_one_x_to_every_row) {
|
PLOT_TEST(figure_crosshair_applies_one_x_to_every_row) {
|
||||||
@@ -930,14 +930,15 @@ PLOT_TEST(figure_crosshair_applies_one_x_to_every_row) {
|
|||||||
PLOT_CHECK(first.rows[0].label == "fast");
|
PLOT_CHECK(first.rows[0].label == "fast");
|
||||||
PLOT_CHECK_NEAR(first.rows[0].y, 30.0, 1e-9);
|
PLOT_CHECK_NEAR(first.rows[0].y, 30.0, 1e-9);
|
||||||
|
|
||||||
// Row 1 samples every *two* units: x = 3 is halfway between sample 1 (y = 100) and sample 2
|
// Row 1 samples every *two* units: x = 3 is halfway between sample 1 (x = 2, y = 100) and
|
||||||
// (y = 200), so it reports 150 -- a different number for the same x, which is what makes this
|
// sample 2 (x = 4, y = 200), which is the tie, so it reports sample 1's 100 -- a different
|
||||||
// case about the rows rather than about one series read four times.
|
// number for the same x, which is what makes this case about the rows rather than about one
|
||||||
|
// series read four times. Not 150: a readout quotes a sample, and this row has none at x = 3.
|
||||||
const plot::CrosshairLayout& second = figure.plot(1u).layout().crosshair;
|
const plot::CrosshairLayout& second = figure.plot(1u).layout().crosshair;
|
||||||
PLOT_CHECK(second.rows.size() == 1u);
|
PLOT_CHECK(second.rows.size() == 1u);
|
||||||
PLOT_CHECK(second.rows[0].valid);
|
PLOT_CHECK(second.rows[0].valid);
|
||||||
PLOT_CHECK(second.rows[0].label == "slow");
|
PLOT_CHECK(second.rows[0].label == "slow");
|
||||||
PLOT_CHECK_NEAR(second.rows[0].y, 150.0, 1e-9);
|
PLOT_CHECK_NEAR(second.rows[0].y, 100.0, 1e-9);
|
||||||
PLOT_CHECK(second.rows[0].value != first.rows[0].value);
|
PLOT_CHECK(second.rows[0].value != first.rows[0].value);
|
||||||
|
|
||||||
// Row 2's trace has no series yet: no row at all -- it is not a signal under the pointer.
|
// Row 2's trace has no series yet: no row at all -- it is not a signal under the pointer.
|
||||||
@@ -972,7 +973,9 @@ PLOT_TEST(figure_crosshair_applies_one_x_to_every_row) {
|
|||||||
PLOT_CHECK_NEAR(figure.plot(row).crosshair().x, 1.0, 1e-12);
|
PLOT_CHECK_NEAR(figure.plot(row).crosshair().x, 1.0, 1e-12);
|
||||||
}
|
}
|
||||||
PLOT_CHECK_NEAR(figure.plot(0u).layout().crosshair.rows[0].y, 10.0, 1e-9);
|
PLOT_CHECK_NEAR(figure.plot(0u).layout().crosshair.rows[0].y, 10.0, 1e-9);
|
||||||
PLOT_CHECK_NEAR(figure.plot(1u).layout().crosshair.rows[0].y, 50.0, 1e-9); // 0 + 100 * 0.5
|
// Not 50: x = 1 is the tie between this row's samples at x = 0 (y = 0) and x = 2 (y = 100),
|
||||||
|
// and a readout quotes one of them.
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(1u).layout().crosshair.rows[0].y, 0.0, 1e-9);
|
||||||
PLOT_CHECK(!figure.plot(3u).layout().crosshair.rows[0].valid);
|
PLOT_CHECK(!figure.plot(3u).layout().crosshair.rows[0].valid);
|
||||||
|
|
||||||
// On *every* row at once, and only the rows free it.
|
// On *every* row at once, and only the rows free it.
|
||||||
|
|||||||
+59
-32
@@ -1965,20 +1965,21 @@ class CountingSeries final : public plot::data::Series {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
/// Spec §15, the lookup: a readout's y is **linear interpolation between the two samples
|
/// Spec §15, the lookup: a readout's y is **the nearer of the two samples adjacent to the
|
||||||
/// adjacent to the crosshair's x**, and hand-computed here rather than checked against the
|
/// crosshair's x**, never a value between them. Every y asserted here is therefore one of the
|
||||||
/// implementation's own arithmetic. The four corners of the rule are all asserted: a query
|
/// series' own `tens` values -- a number *between* two of them, 15 or 22.5, is precisely what
|
||||||
/// between two samples, the two exact-sample ends of the window (where the span collapses onto
|
/// this refuses, and what it used to return. The corners of the rule are all covered: a query
|
||||||
/// one sample), and a one-sample series, whose window is that sample alone.
|
/// either side of the midpoint, a query exactly on it (the tie), the two exact-sample ends of
|
||||||
PLOT_TEST(crosshair_value_is_interpolated_between_neighbours) {
|
/// the window (where the span collapses onto one sample), and a one-sample series.
|
||||||
|
PLOT_TEST(crosshair_value_is_the_nearest_sample) {
|
||||||
CrosshairScene scene;
|
CrosshairScene scene;
|
||||||
const u32 trace = scene.plot.add_trace(
|
const u32 trace = scene.plot.add_trace(
|
||||||
{&scene.series, plot::Color::hex(0x40C0FF), 1.5f, scene.ax, scene.ay});
|
{&scene.series, plot::Color::hex(0x40C0FF), 1.5f, scene.ax, scene.ay});
|
||||||
|
|
||||||
// The span itself, for a query halfway between sample 1 (x = 1, y = 10) and sample 2
|
// The span itself, for a query halfway between sample 1 (x = 1, y = 10) and sample 2
|
||||||
// (x = 2, y = 20): the two neighbours, and the fraction between them. Asserting the span and
|
// (x = 2, y = 20): the two neighbours, and the fraction between them. The span still reports
|
||||||
// not only the value is what pins the convention -- a nearest-sample lookup would report the
|
// both and the fraction between them -- `t` is a *selector* now, not a blend factor, so the
|
||||||
// span's `t` as 0 or 1, and an average of a *range* would not report a span at all.
|
// bracketing is unchanged and only what is built from it differs.
|
||||||
const plot::CrosshairSpan half = plot::crosshair_span(scene.series, 1.5);
|
const plot::CrosshairSpan half = plot::crosshair_span(scene.series, 1.5);
|
||||||
PLOT_CHECK(half.valid);
|
PLOT_CHECK(half.valid);
|
||||||
PLOT_CHECK(half.begin == 1u);
|
PLOT_CHECK(half.begin == 1u);
|
||||||
@@ -1986,14 +1987,25 @@ PLOT_TEST(crosshair_value_is_interpolated_between_neighbours) {
|
|||||||
PLOT_CHECK_NEAR(half.t, 0.5, 1e-12);
|
PLOT_CHECK_NEAR(half.t, 0.5, 1e-12);
|
||||||
|
|
||||||
f64 y = 0.0;
|
f64 y = 0.0;
|
||||||
|
// Either side of the midpoint reads the sample it is nearer to, and *is* that sample: 10 and
|
||||||
|
// 20 are `tens` values, where an interpolation would have answered 11 and 19.
|
||||||
|
PLOT_CHECK(scene.plot.crosshair_value(trace, 1.1, y));
|
||||||
|
PLOT_CHECK_NEAR(y, 10.0, 1e-9);
|
||||||
|
PLOT_CHECK(scene.plot.crosshair_value(trace, 1.9, y));
|
||||||
|
PLOT_CHECK_NEAR(y, 20.0, 1e-9);
|
||||||
|
// Exactly halfway is the tie, and it reads the *earlier* sample -- the bias
|
||||||
|
// `crosshair_span()` already takes for a degenerate pair. 15.0 is the old answer.
|
||||||
PLOT_CHECK(scene.plot.crosshair_value(trace, 1.5, y));
|
PLOT_CHECK(scene.plot.crosshair_value(trace, 1.5, y));
|
||||||
PLOT_CHECK_NEAR(y, 15.0, 1e-9);
|
PLOT_CHECK_NEAR(y, 10.0, 1e-9);
|
||||||
// A quarter of the way through another pair: 20 + (30 - 20) * 0.25.
|
// A quarter of the way through another pair: sample 2, not 22.5.
|
||||||
PLOT_CHECK(scene.plot.crosshair_value(trace, 2.25, y));
|
PLOT_CHECK(scene.plot.crosshair_value(trace, 2.25, y));
|
||||||
PLOT_CHECK_NEAR(y, 22.5, 1e-9);
|
PLOT_CHECK_NEAR(y, 20.0, 1e-9);
|
||||||
|
// Just short of the next sample is that sample, so the readout steps rather than ramps.
|
||||||
|
PLOT_CHECK(scene.plot.crosshair_value(trace, 2.99, y));
|
||||||
|
PLOT_CHECK_NEAR(y, 30.0, 1e-9);
|
||||||
|
|
||||||
// The ends of the window are samples too, so a query exactly on one reads that sample --
|
// The ends of the window are samples too, so a query exactly on one reads that sample --
|
||||||
// and reads it *as* a span of one, with nothing to its left or right to interpolate with.
|
// and reads it *as* a span of one, with no neighbour on the far side.
|
||||||
const plot::CrosshairSpan first = plot::crosshair_span(scene.series, 0.0);
|
const plot::CrosshairSpan first = plot::crosshair_span(scene.series, 0.0);
|
||||||
PLOT_CHECK(first.valid);
|
PLOT_CHECK(first.valid);
|
||||||
PLOT_CHECK(first.begin == 0u);
|
PLOT_CHECK(first.begin == 0u);
|
||||||
@@ -2008,9 +2020,10 @@ PLOT_TEST(crosshair_value_is_interpolated_between_neighbours) {
|
|||||||
PLOT_CHECK(scene.plot.crosshair_value(trace, 3.0, y));
|
PLOT_CHECK(scene.plot.crosshair_value(trace, 3.0, y));
|
||||||
PLOT_CHECK_NEAR(y, 30.0, 1e-9);
|
PLOT_CHECK_NEAR(y, 30.0, 1e-9);
|
||||||
|
|
||||||
// Outside the window there is no value at all -- not the nearest sample's, not the end of
|
// Outside the window there is no value at all. The nearest-sample rule is a rule for an x the
|
||||||
// the window's, which are the clamped lies the header refuses -- and a series with no
|
// series *brackets*; extending it past the ends would answer with the first or last sample for
|
||||||
// samples has no span at any x.
|
// any x whatever, which is the clamped lie the header refuses. A series with no samples has no
|
||||||
|
// span at any x.
|
||||||
PLOT_CHECK(!plot::crosshair_span(scene.series, -1e-9).valid);
|
PLOT_CHECK(!plot::crosshair_span(scene.series, -1e-9).valid);
|
||||||
PLOT_CHECK(!plot::crosshair_span(scene.series, 3.0 + 1e-9).valid);
|
PLOT_CHECK(!plot::crosshair_span(scene.series, 3.0 + 1e-9).valid);
|
||||||
PLOT_CHECK(!scene.plot.crosshair_value(trace, 4.0, y));
|
PLOT_CHECK(!scene.plot.crosshair_value(trace, 4.0, y));
|
||||||
@@ -2023,7 +2036,7 @@ PLOT_TEST(crosshair_value_is_interpolated_between_neighbours) {
|
|||||||
PLOT_CHECK(!scene.plot.crosshair_value(/*trace=*/99u, 1.0, y)); // an id that names nothing
|
PLOT_CHECK(!scene.plot.crosshair_value(/*trace=*/99u, 1.0, y)); // an id that names nothing
|
||||||
|
|
||||||
// A one-sample series: the sample itself is the only query it answers, and it answers it
|
// A one-sample series: the sample itself is the only query it answers, and it answers it
|
||||||
// without a neighbour to interpolate against.
|
// with no second sample to be nearer to.
|
||||||
plot::data::ArraySeries single(std::vector<f32>{7.0f}, 5.0, 1.0);
|
plot::data::ArraySeries single(std::vector<f32>{7.0f}, 5.0, 1.0);
|
||||||
plot::Plot one;
|
plot::Plot one;
|
||||||
one.add_x_axis({"t", {0.0, 10.0}, false, plot::AxisPlace::Bottom, false});
|
one.add_x_axis({"t", {0.0, 10.0}, false, plot::AxisPlace::Bottom, false});
|
||||||
@@ -2039,7 +2052,9 @@ PLOT_TEST(crosshair_value_is_interpolated_between_neighbours) {
|
|||||||
PLOT_CHECK(!one.crosshair_value(single_trace, 5.5, y));
|
PLOT_CHECK(!one.crosshair_value(single_trace, 5.5, y));
|
||||||
|
|
||||||
// The readout's row is the same lookup: the number a user reads off the panel is the value
|
// The readout's row is the same lookup: the number a user reads off the panel is the value
|
||||||
// this test computed by hand, not a second derivation of it.
|
// this test computed by hand, not a second derivation of it. x = 1.5 is the tie, so the row
|
||||||
|
// reads sample 1 -- and the formatted cell is `"10"`, where the interpolating readout showed
|
||||||
|
// `"15"`, a number the series does not contain.
|
||||||
scene.plot.set_crosshair(1.5, true);
|
scene.plot.set_crosshair(1.5, true);
|
||||||
const plot::CrosshairLayout& crosshair = scene.plot.layout().crosshair;
|
const plot::CrosshairLayout& crosshair = scene.plot.layout().crosshair;
|
||||||
PLOT_CHECK(crosshair.active);
|
PLOT_CHECK(crosshair.active);
|
||||||
@@ -2049,8 +2064,8 @@ PLOT_TEST(crosshair_value_is_interpolated_between_neighbours) {
|
|||||||
// the figure's case pins).
|
// the figure's case pins).
|
||||||
PLOT_CHECK(crosshair.rows.size() == 1u);
|
PLOT_CHECK(crosshair.rows.size() == 1u);
|
||||||
PLOT_CHECK(crosshair.rows[0].valid);
|
PLOT_CHECK(crosshair.rows[0].valid);
|
||||||
PLOT_CHECK_NEAR(crosshair.rows[0].y, 15.0, 1e-9);
|
PLOT_CHECK_NEAR(crosshair.rows[0].y, 10.0, 1e-9);
|
||||||
PLOT_CHECK(crosshair.rows[0].value == "15");
|
PLOT_CHECK(crosshair.rows[0].value == "10");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spec §15, the readout's membership: **every visible trace gets a row, whether or not it is
|
/// Spec §15, the readout's membership: **every visible trace gets a row, whether or not it is
|
||||||
@@ -2080,14 +2095,16 @@ PLOT_TEST(crosshair_readout_lists_every_visible_trace_including_unnamed_ones) {
|
|||||||
PLOT_CHECK(crosshair.rows[1].label == plot::crosshair_unnamed_label(1u));
|
PLOT_CHECK(crosshair.rows[1].label == plot::crosshair_unnamed_label(1u));
|
||||||
PLOT_CHECK(crosshair.rows[1].label == "#1");
|
PLOT_CHECK(crosshair.rows[1].label == "#1");
|
||||||
|
|
||||||
// Both are listed with the value their own series has at x = 1.5, the swatch, the label's box
|
// Both are listed with the value their own series has nearest x = 1.5, the swatch, the label's
|
||||||
// and the value's box -- all placed by the plot, all inside the panel it measured.
|
// box and the value's box -- all placed by the plot, all inside the panel it measured. x = 1.5
|
||||||
|
// is the tie between samples 1 and 2, which reads the earlier one: 10, not the 15 an
|
||||||
|
// interpolating readout showed.
|
||||||
PLOT_CHECK(crosshair.panel.w > 0.0f && crosshair.panel.h > 0.0f);
|
PLOT_CHECK(crosshair.panel.w > 0.0f && crosshair.panel.h > 0.0f);
|
||||||
for (const plot::CrosshairRow& row : crosshair.rows) {
|
for (const plot::CrosshairRow& row : crosshair.rows) {
|
||||||
PLOT_CHECK(row.valid);
|
PLOT_CHECK(row.valid);
|
||||||
PLOT_CHECK(row.listed);
|
PLOT_CHECK(row.listed);
|
||||||
PLOT_CHECK_NEAR(row.y, 15.0, 1e-9);
|
PLOT_CHECK_NEAR(row.y, 10.0, 1e-9);
|
||||||
PLOT_CHECK(row.value == "15");
|
PLOT_CHECK(row.value == "10");
|
||||||
PLOT_CHECK(row.swatch.x >= crosshair.panel.x);
|
PLOT_CHECK(row.swatch.x >= crosshair.panel.x);
|
||||||
PLOT_CHECK(row.swatch.right() <= crosshair.panel.right());
|
PLOT_CHECK(row.swatch.right() <= crosshair.panel.right());
|
||||||
PLOT_CHECK(row.swatch.y >= crosshair.panel.y);
|
PLOT_CHECK(row.swatch.y >= crosshair.panel.y);
|
||||||
@@ -2148,7 +2165,7 @@ PLOT_TEST(crosshair_readout_lists_every_visible_trace_including_unnamed_ones) {
|
|||||||
PLOT_CHECK(on.rows[0].listed);
|
PLOT_CHECK(on.rows[0].listed);
|
||||||
PLOT_CHECK(on.rows[1].listed);
|
PLOT_CHECK(on.rows[1].listed);
|
||||||
PLOT_CHECK(on.panel.w > 0.0f && on.panel.h > 0.0f);
|
PLOT_CHECK(on.panel.w > 0.0f && on.panel.h > 0.0f);
|
||||||
PLOT_CHECK(on.rows[0].value == "15");
|
PLOT_CHECK(on.rows[0].value == "10");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spec §15, the panel's placement: near the pointer, flipped over it where it would otherwise
|
/// Spec §15, the panel's placement: near the pointer, flipped over it where it would otherwise
|
||||||
@@ -2253,11 +2270,14 @@ PLOT_TEST(crosshair_lookup_does_not_scan) {
|
|||||||
p.add_y_axis({"v", {0.0, 99.0}, false, plot::AxisPlace::Left, false});
|
p.add_y_axis({"v", {0.0, 99.0}, false, plot::AxisPlace::Left, false});
|
||||||
const u32 trace = p.add_trace({&series, plot::Color::hex(0x40C0FF), 1.5f, 0u, 0u});
|
const u32 trace = p.add_trace({&series, plot::Color::hex(0x40C0FF), 1.5f, 0u, 0u});
|
||||||
|
|
||||||
// A query between two samples: 1234 % 100 = 34, the next is 35, halfway between them.
|
// A query between two samples: 1234 % 100 = 34, the next is 35, and 1234.5 is the tie, which
|
||||||
|
// reads the earlier -- 34, where the interpolating readout answered 34.5. Both samples are
|
||||||
|
// still read: the bound below is on how much data the lookup touches, not on which end it
|
||||||
|
// then reports.
|
||||||
f64 y = 0.0;
|
f64 y = 0.0;
|
||||||
const u64 before_reads = series.samples_read();
|
const u64 before_reads = series.samples_read();
|
||||||
PLOT_CHECK(p.crosshair_value(trace, 1234.5, y));
|
PLOT_CHECK(p.crosshair_value(trace, 1234.5, y));
|
||||||
PLOT_CHECK_NEAR(y, 34.5, 1e-9);
|
PLOT_CHECK_NEAR(y, 34.0, 1e-9);
|
||||||
PLOT_CHECK(series.samples_read() - before_reads <= 2u);
|
PLOT_CHECK(series.samples_read() - before_reads <= 2u);
|
||||||
PLOT_CHECK(series.gather_calls() <= 1u);
|
PLOT_CHECK(series.gather_calls() <= 1u);
|
||||||
|
|
||||||
@@ -2288,7 +2308,7 @@ PLOT_TEST(crosshair_lookup_does_not_scan) {
|
|||||||
const plot::CrosshairLayout& crosshair = p.layout().crosshair;
|
const plot::CrosshairLayout& crosshair = p.layout().crosshair;
|
||||||
PLOT_CHECK(crosshair.rows.size() == 1u);
|
PLOT_CHECK(crosshair.rows.size() == 1u);
|
||||||
PLOT_CHECK(crosshair.rows[0].valid);
|
PLOT_CHECK(crosshair.rows[0].valid);
|
||||||
PLOT_CHECK_NEAR(crosshair.rows[0].y, 34.5, 1e-9);
|
PLOT_CHECK_NEAR(crosshair.rows[0].y, 34.0, 1e-9);
|
||||||
PLOT_CHECK(series.samples_read() - reads <= 2u);
|
PLOT_CHECK(series.samples_read() - reads <= 2u);
|
||||||
PLOT_CHECK(series.gather_calls() - calls <= 1u);
|
PLOT_CHECK(series.gather_calls() - calls <= 1u);
|
||||||
// And the readout is not vacuous: the series it read two samples of really does hold a
|
// And the readout is not vacuous: the series it read two samples of really does hold a
|
||||||
@@ -2511,9 +2531,12 @@ PLOT_TEST(scrolled_back_live_window_keeps_its_decades) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The XY routing, crosshair half: the span brackets the samples the *array*
|
/// The XY routing, crosshair half: the span brackets the samples the *array*
|
||||||
/// names, and the interpolation fraction is measured on the samples' own positions
|
/// names, and `t` is measured on the samples' own positions -- (5-1)/(10-1) over the
|
||||||
/// -- (5-1)/(10-1) over the wide gap, where a uniform-grid guess would have read
|
/// wide gap, where a uniform-grid guess would have read samples that do not exist at
|
||||||
/// samples that do not exist at the positions it assumed.
|
/// the positions it assumed. The gap is what makes the value worth asserting too:
|
||||||
|
/// x = 5 sits 4 from sample 1 and 5 from sample 2, so the nearest-sample rule has a
|
||||||
|
/// real choice to get right, and it only has that choice because `t` came from the
|
||||||
|
/// array.
|
||||||
PLOT_TEST(xy_series_crosshair_brackets_its_own_samples) {
|
PLOT_TEST(xy_series_crosshair_brackets_its_own_samples) {
|
||||||
XyScene scene;
|
XyScene scene;
|
||||||
const u32 trace = scene.plot.add_trace(
|
const u32 trace = scene.plot.add_trace(
|
||||||
@@ -2526,7 +2549,11 @@ PLOT_TEST(xy_series_crosshair_brackets_its_own_samples) {
|
|||||||
PLOT_CHECK_NEAR(span.t, 4.0 / 9.0, 1e-12);
|
PLOT_CHECK_NEAR(span.t, 4.0 / 9.0, 1e-12);
|
||||||
f64 y = 0.0;
|
f64 y = 0.0;
|
||||||
PLOT_CHECK(scene.plot.crosshair_value(trace, 5.0, y));
|
PLOT_CHECK(scene.plot.crosshair_value(trace, 5.0, y));
|
||||||
PLOT_CHECK_NEAR(y, 10.0 + 10.0 * (4.0 / 9.0), 1e-9);
|
PLOT_CHECK_NEAR(y, 10.0, 1e-9); // sample 1, the nearer end of the gap
|
||||||
|
// And past the gap's midpoint it is the other sample, so the choice is `t`'s and not a
|
||||||
|
// fixed preference for one end.
|
||||||
|
PLOT_CHECK(scene.plot.crosshair_value(trace, 6.0, y));
|
||||||
|
PLOT_CHECK_NEAR(y, 20.0, 1e-9);
|
||||||
|
|
||||||
// On a sample, at both ends of the array's window, and outside it: the same
|
// On a sample, at both ends of the array's window, and outside it: the same
|
||||||
// closed-window rule a uniform series follows, on the array's own coordinates.
|
// closed-window rule a uniform series follows, on the array's own coordinates.
|
||||||
|
|||||||
@@ -72,9 +72,9 @@ PLOT_TEST(xy_series_reports_its_own_grid) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PLOT_TEST(xy_series_neighbour_lookups_crosshair_style) {
|
PLOT_TEST(xy_series_neighbour_lookups_crosshair_style) {
|
||||||
// The crosshair asks neighbours() at an arbitrary x and interpolates between
|
// The crosshair asks neighbours() at an arbitrary x and reports whichever of
|
||||||
// the two samples it gets back. On a non-uniform grid that needs real
|
// the two samples it gets back is nearer. On a non-uniform grid that needs
|
||||||
// searches: a uniform-grid formula would land megabytes off here.
|
// real searches: a uniform-grid formula would land megabytes off here.
|
||||||
auto x = spread_x(1000);
|
auto x = spread_x(1000);
|
||||||
const std::vector<f64> xs = x;
|
const std::vector<f64> xs = x;
|
||||||
plot::data::XYSeries s(std::move(x), saw_y(1000));
|
plot::data::XYSeries s(std::move(x), saw_y(1000));
|
||||||
|
|||||||
Reference in new issue
Block a user