feat(plot): the readout marker snaps to the sample it quotes, and the value mode is a choice
The mark was placed at the *pointer's* x with the quoted sample's y, so with the nearest-sample readout it sat off the datum horizontally -- and further off the deeper the zoom. `CrosshairRow` now carries the data `x` the value belongs to and the marker is placed from it, so in `NearestSample` the mark lands on the sample the panel is quoting. `Style::crosshair_marker_px` (9) and `crosshair_marker_width` (1.5) replace the renderer's hard-coded 3 px arm and 1 px stroke: a snapped mark lands among a trace's own sample dots (`marker_px`, 3) and at that size would read as one more sample rather than as the answer to the question the hover asks. `CrosshairValue` makes the previous behaviour reachable rather than deleted. The two modes answer different questions -- `NearestSample` "which datum is under the pointer", `Interpolated` "what is the signal at this instant" -- and only the caller knows which it is asking, so the plot offers both with the sample as the default. `crosshair_interpolate()` is restored for it. `Plot::crosshair_point()` is the lookup with both coordinates, public because a host that draws instruments of its own needs the same snap and a second rule for it would drift from this one; `crosshair_value()` delegates to it.
This commit is contained in:
1 parent
c028f48fa0
commit
c960decaea
9 files changed
+372
-78
No files matched your search
@@ -688,23 +688,31 @@ class Plot {
|
||||
/// dash, which live on `CrosshairLayout` because they depend on `ui_scale`.
|
||||
const CrosshairStyle& crosshair_style() const noexcept { return crosshair_.style; }
|
||||
|
||||
/// Chooses what a readout's numbers *are*: one of the series' own samples, or a value
|
||||
/// computed between two of them. `CrosshairValue` argues the two cases and says where each
|
||||
/// puts the marker; the default is `NearestSample`.
|
||||
///
|
||||
/// Deliberately **not** part of `set_crosshair()`, like the readout switch and the style
|
||||
/// override: it is a statement about the data, so a pointer move must not be able to change
|
||||
/// it. It takes effect on the next `layout()` and reaches `crosshair_value()` immediately.
|
||||
void set_crosshair_value_mode(CrosshairValue mode) noexcept { crosshair_.value_mode = mode; }
|
||||
/// The readout's value mode, as set -- see `set_crosshair_value_mode()`.
|
||||
CrosshairValue crosshair_value_mode() const noexcept { return crosshair_.value_mode; }
|
||||
|
||||
/// The crosshair's state: where it is, which lines it draws and whether the readout is
|
||||
/// on. Read-only -- `set_crosshair()` and `clear_crosshair()` are how it moves -- so the
|
||||
/// coordinates it holds and the geometry `layout()` places from them cannot disagree.
|
||||
const Crosshair& crosshair() const noexcept { return crosshair_; }
|
||||
|
||||
/// The value of `trace`'s **nearest sample** to a data `x`: `crosshair_span()` brackets
|
||||
/// `x`, and the nearer of the two samples is the answer (`t <= 0.5` takes the earlier) --
|
||||
/// the same lookup the readout's rows are built from, exposed for a host that wants the
|
||||
/// 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.
|
||||
/// The value this plot's readout would report for `trace` at a data `x`: the nearer
|
||||
/// bracketing sample's y, or the blend of the two, according to
|
||||
/// `crosshair_value_mode()` -- the same lookup the readout's rows are built from, exposed
|
||||
/// for a host that wants the number for itself (a status line, a tooltip of its own).
|
||||
///
|
||||
/// 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`
|
||||
/// documents, plus the trace itself. Never a clamped or nearest-sample answer: see
|
||||
/// `crosshair_span()` for why an out-of-window x is not a value.
|
||||
/// documents, plus the trace itself. Never a clamped answer: see `crosshair_span()` for why
|
||||
/// an out-of-window x is not a value.
|
||||
///
|
||||
/// O(1) and allocation-free once the plot has looked a value up before: the span is
|
||||
/// index arithmetic and the samples are read through one `Series::gather()` of at most
|
||||
@@ -712,6 +720,17 @@ class Plot {
|
||||
/// on every pointer move -- once per visible trace, so nothing here may scan a series.
|
||||
bool crosshair_value(u32 trace, f64 x, f64& out_y) const;
|
||||
|
||||
/// The whole **point** the readout reports, x as well as y: `crosshair_value()`'s number
|
||||
/// together with the data x it belongs to. In `CrosshairValue::NearestSample` that x is the
|
||||
/// quoted sample's own, which is generally not the `x` asked for; in `Interpolated` it is
|
||||
/// `x` itself.
|
||||
///
|
||||
/// This is what places `CrosshairRow::marker`, and it is public because a host that draws
|
||||
/// instruments of its own -- a cursor pinned to the value it reads, an annotation naming the
|
||||
/// sample -- needs the same snap, and recomputing it would be a second rule that can drift
|
||||
/// from this one. Same failure cases, same cost, same scratch buffer as `crosshair_value()`.
|
||||
bool crosshair_point(u32 trace, f64 x, f64& out_x, f64& out_y) const;
|
||||
|
||||
private:
|
||||
/// One measure plus its cache. The dirty flag is what keeps measurement() from
|
||||
/// re-evaluating every frame; `last_series`, `last_size` and `last_x0` catch a
|
||||
|
||||
Reference in new issue
Block a user