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
+41
-12
@@ -1410,6 +1410,13 @@ CrosshairSpan crosshair_span(const data::Series& series, f64 x) noexcept {
|
||||
return span;
|
||||
}
|
||||
|
||||
f64 crosshair_interpolate(f64 y_begin, f64 y_end, f64 t) noexcept {
|
||||
// This form, not `(1 - t) * a + t * b`: at `t == 0` it is `y_begin` and at `t == 1` it is
|
||||
// `y_end`, both exactly, so a query that landed on a sample reads that sample back whatever
|
||||
// mode is in force. The symmetric form rounds at both ends.
|
||||
return y_begin + t * (y_end - y_begin);
|
||||
}
|
||||
|
||||
std::string crosshair_unnamed_label(u32 trace) {
|
||||
char buf[16];
|
||||
std::snprintf(buf, sizeof buf, "#%u", static_cast<unsigned>(trace));
|
||||
@@ -1439,6 +1446,11 @@ void Plot::set_crosshair(f64 x, f64 y, bool active) {
|
||||
}
|
||||
|
||||
bool Plot::crosshair_value(u32 trace, f64 x, f64& out_y) const {
|
||||
f64 ignored = 0.0;
|
||||
return crosshair_point(trace, x, ignored, out_y);
|
||||
}
|
||||
|
||||
bool Plot::crosshair_point(u32 trace, f64 x, f64& out_x, f64& out_y) const {
|
||||
if (trace >= traces_.size()) return false;
|
||||
const data::Series* series = traces_[trace].series;
|
||||
if (series == nullptr) return false;
|
||||
@@ -1455,12 +1467,23 @@ 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
|
||||
const f64 y_begin = static_cast<f64>(crosshair_scratch_[1u]);
|
||||
const f64 y_end = static_cast<f64>(crosshair_scratch_[2u * pairs - 1u]);
|
||||
// **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;
|
||||
if (crosshair_.value_mode == CrosshairValue::Interpolated) {
|
||||
// The signal the samples imply *at* this x, so the point is the query's own x: that is
|
||||
// where an interpolated number lives, and the marker on the polyline there is honest
|
||||
// about it.
|
||||
out_x = x;
|
||||
out_y = crosshair_interpolate(y_begin, y_end, span.t);
|
||||
return true;
|
||||
}
|
||||
// `NearestSample`: the nearer of the two, and **its own x with it**. `span.t` is measured on
|
||||
// the samples' own coordinates, 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. The x
|
||||
// comes back through `x_at()` rather than from the gathered pairs, which are relative to
|
||||
// `x0()` by the `Series::gather()` contract -- adding it back here would be a second
|
||||
// reconstruction of a coordinate the series will hand over directly.
|
||||
const bool take_begin = span.t <= 0.5;
|
||||
out_x = series->x_at(take_begin ? span.begin : span.end);
|
||||
out_y = take_begin ? y_begin : y_end;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1529,17 +1552,23 @@ void Plot::build_crosshair() {
|
||||
row.trace = i;
|
||||
row.valid = false;
|
||||
row.listed = false;
|
||||
row.x = 0.0;
|
||||
row.y = 0.0;
|
||||
row.marker = Vec2{};
|
||||
row.swatch = Rect{};
|
||||
row.label_box = Rect{};
|
||||
row.value_box = Rect{};
|
||||
row.label.assign(trace.name.empty() ? crosshair_unnamed_label(i) : trace.name);
|
||||
// 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
|
||||
// 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.
|
||||
if (crosshair_value(i, crosshair_.x, row.y)) {
|
||||
// The value 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 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.
|
||||
//
|
||||
// The **point**, not just the number: `crosshair_point()` reports the x the value belongs to
|
||||
// as well, which in the default `NearestSample` mode is the quoted sample's own and not the
|
||||
// pointer's. Both coordinates come from that one call, so the mark and the text cannot
|
||||
// disagree about which datum is being read (see `CrosshairRow::marker`).
|
||||
if (crosshair_point(i, crosshair_.x, row.x, row.y)) {
|
||||
row.valid = true;
|
||||
char value[32];
|
||||
// The trace's *own* y axis formats the number, so a readout reads exactly like the
|
||||
@@ -1548,7 +1577,7 @@ void Plot::build_crosshair() {
|
||||
row.value.assign(value);
|
||||
// Placed through the trace's own axis pair, so the marker sits where that trace is
|
||||
// actually drawn however its axes are scaled.
|
||||
row.marker = Vec2{static_cast<f32>(x_transform(trace.axis_x).to_screen_x(crosshair_.x)),
|
||||
row.marker = Vec2{static_cast<f32>(x_transform(trace.axis_x).to_screen_x(row.x)),
|
||||
static_cast<f32>(y_transform(trace.axis_y).to_screen_y(row.y))};
|
||||
} else {
|
||||
row.value.assign(kCrosshairNoValue);
|
||||
|
||||
Reference in new issue
Block a user