feat(plot): crosshair with an interpolated per-signal hover readout
This commit is contained in:
1 parent
a153b73ead
commit
fd0c471430
11 files changed
+809
-22
No files matched your search
@@ -6,6 +6,7 @@
|
||||
#include <plot/math/transform.hpp>
|
||||
#include <plot/plot/annotation.hpp>
|
||||
#include <plot/plot/axis.hpp>
|
||||
#include <plot/plot/crosshair.hpp>
|
||||
#include <plot/plot/cursor.hpp>
|
||||
#include <plot/plot/grid.hpp>
|
||||
#include <plot/plot/legend.hpp>
|
||||
@@ -96,6 +97,13 @@ struct PlotLayout {
|
||||
/// controller's hit test reads the very same rects, so where a region is drawn and
|
||||
/// where a pointer can grab it cannot drift apart.
|
||||
std::vector<RegionLayout> regions;
|
||||
/// 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
|
||||
/// is over. 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
|
||||
/// 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.
|
||||
CrosshairLayout crosshair;
|
||||
};
|
||||
|
||||
/// What a pointer is on, as of the last pointer event: the cursor marker under it, the
|
||||
@@ -530,6 +538,56 @@ class Plot {
|
||||
/// a caller can move the legend -- or switch it off -- between frames.
|
||||
void set_legend(const Legend& legend) noexcept { legend_ = legend; }
|
||||
|
||||
// --- crosshair --------------------------------------------------------
|
||||
|
||||
/// Places a **vertical-only** crosshair at a data x: the column of `x`, a marker on
|
||||
/// every visible trace at that x, and the readout panel beside it (spec §15).
|
||||
///
|
||||
/// This is the form for a caller that has a coordinate and not a pointer -- "show me
|
||||
/// every signal at this instant" -- and for `Figure::set_crosshair()`, which applies one
|
||||
/// x to every row: a figure's rows share x and not y, so a horizontal line across them
|
||||
/// would be a claim about y that the figure does not make.
|
||||
///
|
||||
/// **Refused** -- nothing changes -- when `x` is not a number: a crosshair there has no
|
||||
/// column to draw and no value to read, and refusing it leaves the previous position
|
||||
/// rather than a crosshair no gesture could undo. `active == false` clears it exactly as
|
||||
/// `clear_crosshair()` does.
|
||||
void set_crosshair(f64 x, bool active);
|
||||
|
||||
/// Places a crosshair at both coordinates: the vertical line and the readout of the
|
||||
/// first form, plus the horizontal line at `y`. This is the form `input::Controller`
|
||||
/// uses, because a pointer over the axes has both coordinates.
|
||||
///
|
||||
/// Refused, changing nothing, when either coordinate is not a number (see the first
|
||||
/// form).
|
||||
void set_crosshair(f64 x, f64 y, bool active);
|
||||
|
||||
/// Hides the crosshair, leaving where it was: the same state a plot has before any
|
||||
/// crosshair is placed, so the next `set_crosshair()` re-anchors it. `input::Controller`
|
||||
/// calls this when the pointer leaves the axes.
|
||||
void clear_crosshair() noexcept { crosshair_.active = false; }
|
||||
|
||||
/// 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 **interpolated** value of `trace`'s series at a data `x`: `crosshair_span()`
|
||||
/// between the two adjacent samples, then `crosshair_interpolate()` -- 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.
|
||||
///
|
||||
/// 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
|
||||
/// two samples into a buffer the plot owns. It is called on every layout -- and therefore
|
||||
/// 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;
|
||||
|
||||
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
|
||||
@@ -636,6 +694,12 @@ class Plot {
|
||||
/// to list -- an empty legend is what keeps the feature free for a caller who
|
||||
/// never names a trace.
|
||||
void build_legend();
|
||||
/// 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 --
|
||||
/// 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
|
||||
/// lines: the rows are the readout's, not the crosshair's.
|
||||
void build_crosshair();
|
||||
|
||||
Rect viewport_{};
|
||||
render::Style style_{};
|
||||
@@ -664,6 +728,20 @@ class Plot {
|
||||
/// about to drop. A member rather than a local, so a steady-state frame's legend
|
||||
/// build allocates nothing.
|
||||
std::vector<f32> legend_name_px_;
|
||||
/// The crosshair's state (see Crosshair): at most one per plot, placed by
|
||||
/// `set_crosshair()` and read back by `crosshair()` and `layout()`.
|
||||
Crosshair crosshair_{};
|
||||
/// The samples `crosshair_value()` reads a span through: `Series::gather()` appends
|
||||
/// `(x, y)` pairs and never clears, so the lookup clears this and refills it with the at
|
||||
/// most two samples a span covers. Sized to hold them from construction -- four floats is
|
||||
/// two pairs -- so a lookup on the pointer path reallocates nothing even the first time,
|
||||
/// and `mutable` for the same reason `measures_` is: a lookup is not a mutation a caller
|
||||
/// can observe.
|
||||
mutable std::vector<f32> crosshair_scratch_{0.0f, 0.0f, 0.0f, 0.0f};
|
||||
/// Scratch for build_crosshair(): the content width (label + gutter + value) of every row,
|
||||
/// prefix-maxed in place, for `legend_name_px_`'s reason -- the panel's width after a row
|
||||
/// was dropped from the end is the width of a row still listed, read in O(1).
|
||||
std::vector<f32> crosshair_content_px_;
|
||||
|
||||
PlotLayout layout_{};
|
||||
Grid grid_;
|
||||
|
||||
Reference in new issue
Block a user