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
@@ -124,6 +124,18 @@ using plot::RegionGrab;
|
||||
/// the same answer for a host that wants it directly -- a resize cursor, a tooltip,
|
||||
/// a status line.
|
||||
///
|
||||
/// ## The crosshair (spec §15)
|
||||
///
|
||||
/// A pointer over the data area places the plot's crosshair at the data coordinates under
|
||||
/// it (`Plot::set_crosshair()`), and a pointer that leaves `axes` clears it
|
||||
/// (`Plot::clear_crosshair()`). That is a *hover*, not a gesture: no button is involved, and
|
||||
/// a press, a wheel notch or a key stroke that moves the data under the pointer re-anchors
|
||||
/// the crosshair to the same pointer position rather than clearing it -- it follows the
|
||||
/// pointer, so anything the pointer did not move leaves it where the pointer is. The
|
||||
/// per-signal values a readout shows are the plot's (`PlotLayout::crosshair`), not this
|
||||
/// layer's: the controller says where the pointer is and nothing about what a crosshair
|
||||
/// means.
|
||||
///
|
||||
/// ## Zoom and pan on a live x axis (spec §12)
|
||||
///
|
||||
/// An x axis armed with `XFollow::Live` keeps following through every gesture
|
||||
@@ -262,6 +274,16 @@ class Controller {
|
||||
/// leave alone, the bounds and the grabbed data x a body move needs to preserve
|
||||
/// the width -- so a changed pointer motion never accumulates error.
|
||||
bool begin_handle_drag(const PointerEvent& e);
|
||||
/// Places the plot's crosshair under the pointer, or clears it when the pointer is not
|
||||
/// over the data area (spec §15). Called after every event that can move the pointer *or*
|
||||
/// the data under it -- a pointer event, a wheel notch, a key stroke, a viewport change --
|
||||
/// so the crosshair is where the pointer is and reads the data the frame will draw, and
|
||||
/// never at a coordinate a zoom has already moved out from under it.
|
||||
///
|
||||
/// A press does not clear it: the crosshair follows the pointer, and a pointer that is
|
||||
/// still over the axes is still pointing at whatever gesture just started. The pointer is
|
||||
/// the only thing that clears it, by leaving `axes`.
|
||||
void update_crosshair(f32 px, f32 py);
|
||||
/// Publishes what the pointer is on to the plot, for the renderer to highlight
|
||||
/// (see HotHandles). Called after every event that can move the geometry under the
|
||||
/// pointer -- a pointer event, a wheel notch, a key stroke -- so hover and grab are
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
|
||||
#include <plot/plot/annotation.hpp>
|
||||
#include <plot/plot/axis.hpp>
|
||||
#include <plot/plot/crosshair.hpp>
|
||||
#include <plot/plot/cursor.hpp>
|
||||
#include <plot/plot/figure.hpp>
|
||||
#include <plot/plot/grid.hpp>
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <plot/core/rect.hpp>
|
||||
#include <plot/core/types.hpp>
|
||||
#include <plot/data/series.hpp>
|
||||
#include <plot/math/vec2.hpp>
|
||||
|
||||
namespace plot {
|
||||
|
||||
/// The crosshair's state: where it is, which of its two lines it draws, and whether the
|
||||
/// per-signal readout is wanted.
|
||||
///
|
||||
/// It is a *hover* instrument, not a cursor: a cursor is put where the caller wants it and
|
||||
/// stays there, while the crosshair follows the pointer and exists only while the pointer
|
||||
/// is over the data area. So it is plain state rather than an element of a collection --
|
||||
/// there is at most one per plot, and `Plot::set_crosshair()` is what places it.
|
||||
///
|
||||
/// **Its coordinates are data coordinates of the primary axis pair** (x axis 0, y axis 0),
|
||||
/// like a `Region`'s bounds and the grid: one hover is one instant and one value, whatever
|
||||
/// the strips around the axes happen to be. `vertical`/`horizontal` say which lines are
|
||||
/// 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.
|
||||
///
|
||||
/// **The readout's values are interpolated, and deliberately so.** The y a row reports for
|
||||
/// a trace is *linear interpolation between the two samples adjacent to `x`*, not a sample
|
||||
/// and not an interval statistic -- see `crosshair_span()` for the rule stated in full, and
|
||||
/// for why it is the opposite convention from `Series::reduce()`/`sum()`/`sumsq()`.
|
||||
struct Crosshair {
|
||||
/// 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
|
||||
/// axes.
|
||||
bool active = false;
|
||||
/// Draw the vertical line (the column of `x`). On for both `set_crosshair()` overloads.
|
||||
bool vertical = true;
|
||||
/// 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.
|
||||
bool horizontal = true;
|
||||
/// List the visible traces and their interpolated 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
|
||||
/// measure and the panel is then not placed at all.
|
||||
bool show_readout = true;
|
||||
/// The data x of the vertical line, on x axis 0.
|
||||
f64 x = 0.0;
|
||||
/// The data y of the horizontal line, on y axis 0. Meaningless for a vertical-only
|
||||
/// crosshair, which draws no row and anchors its readout at the top of the axes.
|
||||
f64 y = 0.0;
|
||||
};
|
||||
|
||||
/// The two samples of a uniform-grid series that a data `x` lies between: what the
|
||||
/// crosshair's lookup needs, and all it needs.
|
||||
///
|
||||
/// `begin` is the index of the last sample at or before `x`, `end` the index of the first
|
||||
/// sample at or after it, and `t` where `x` sits between them (`0` at `begin`, `1` at
|
||||
/// `end`). `end - begin` is therefore 0 when `x` lands exactly on a sample -- including the
|
||||
/// two ends of the window, which are samples -- and 1 otherwise, so a caller reads **at
|
||||
/// most two samples** and never scans: the span is one division and one floor of index
|
||||
/// 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.
|
||||
///
|
||||
/// **Interpolated, not exact.** The value a caller builds from this span by
|
||||
/// `crosshair_interpolate()` between the two samples is the signal *at* `x`, which is
|
||||
/// generally between two samples -- the opposite convention from `Series::reduce()`,
|
||||
/// `sum()` and `sumsq()`, which are **exact over a sample range**. The two conventions sit
|
||||
/// next to each other in this library on purpose and they do not agree: `reduce()` answers
|
||||
/// "what are the samples' extremes over this interval", this answers "what is the signal at
|
||||
/// this instant". A readout that pretended to be exact would be quoting an extreme or a
|
||||
/// mean of a window as if it were the value under the pointer.
|
||||
///
|
||||
/// `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
|
||||
/// `[x0, x0 + (size-1)*dx]`. An out-of-window x yields *no* value rather than the nearest
|
||||
/// sample's, which would be a clamped lie about the data under the pointer; the readout row
|
||||
/// it belongs to says so instead (`CrosshairRow::valid`).
|
||||
struct CrosshairSpan {
|
||||
/// Index of the last sample whose x is at or before the query.
|
||||
u64 begin = 0;
|
||||
/// 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.
|
||||
u64 end = 0;
|
||||
/// The query's position between the two samples, in `[0, 1]`.
|
||||
f64 t = 0.0;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
/// 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`.
|
||||
///
|
||||
/// O(1) and allocation-free: a subtraction, a division and a floor. The series' own sample
|
||||
/// fetch is the caller's -- `Plot::crosshair_value()` reads the span through
|
||||
/// `Series::gather()` into a scratch buffer it owns, so no lookup on the pointer path
|
||||
/// allocates.
|
||||
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
|
||||
/// and the boxes the renderer stages.
|
||||
///
|
||||
/// **Every visible trace gets a row, named or not** -- the readout is about what is under
|
||||
/// the pointer, so the legend's rule of skipping unnamed traces does not apply here. An
|
||||
/// unnamed trace's row carries a stable positional label instead (`label`, built by
|
||||
/// `crosshair_unnamed_label()`), so the rows stay identifiable without a name being
|
||||
/// invented for the trace.
|
||||
///
|
||||
/// A row whose trace has no sample at the crosshair's x -- its series does not reach that
|
||||
/// far, or holds nothing -- is `valid == false` and carries **no value text**: the trace is
|
||||
/// still listed, because it *is* one of the signals under the pointer, but no number is
|
||||
/// fabricated for it and no marker is drawn at a y that does not exist.
|
||||
struct CrosshairRow {
|
||||
/// Index of the trace this row is about: `Plot::trace(trace)` is what the label, the
|
||||
/// swatch colour and the marker position come from.
|
||||
u32 trace = 0;
|
||||
/// False when the trace's series has no sample at the crosshair's x (see the struct
|
||||
/// comment). `y`, `value` and `marker` are then meaningless.
|
||||
bool valid = false;
|
||||
/// True when the row's chrome was placed inside the panel. The panel is clamped to the
|
||||
/// axes and drops rows from the end when they do not fit -- exactly as a legend drops
|
||||
/// 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.
|
||||
bool listed = false;
|
||||
/// The trace's interpolated y at the crosshair's x, on the trace's own y axis. Only
|
||||
/// meaningful when `valid`.
|
||||
f64 y = 0.0;
|
||||
/// The trace's name, or the positional placeholder of an unnamed one.
|
||||
std::string label;
|
||||
/// The formatted value, written in the trace's y axis' own tick format (`Axis::format_value()`,
|
||||
/// so a readout reads exactly like the labels beside it), and empty when `!valid`.
|
||||
std::string value;
|
||||
/// The trace's intersection with the crosshair, in framebuffer pixels: the pen the
|
||||
/// renderer draws the marker at. Placed through the *trace's own* axis pair, so the
|
||||
/// marker is where that trace is actually drawn. Meaningless when `!valid`.
|
||||
Vec2 marker{};
|
||||
/// The swatch's box, `Legend::swatch_w` x `Legend::swatch_h` centred in the row. Empty
|
||||
/// when `!listed`.
|
||||
Rect swatch{};
|
||||
/// The label's line box: its top-left is the pen the run is staged at and its height one
|
||||
/// text line. Empty when `!listed`.
|
||||
Rect label_box{};
|
||||
/// The value's line box, one gutter (the legend's padding) after the label's. Empty when
|
||||
/// `!listed` or `!valid`: a row with no value stages no run.
|
||||
Rect value_box{};
|
||||
};
|
||||
|
||||
/// The measured crosshair: the two lines' stroke boxes, the readout panel and one row per
|
||||
/// visible trace, in the same device-pixel space as `PlotLayout` and filled alongside it by
|
||||
/// `Plot::layout()`.
|
||||
///
|
||||
/// It is a member of `PlotLayout` rather than a second call, for the legend's reason: the
|
||||
/// numbers a test asserts and the numbers the renderer draws are one set. Every rect is
|
||||
/// **inside `PlotLayout::axes`** by construction -- a line's stroke box is held half a
|
||||
/// stroke inside the rect, and the panel is placed beside the pointer, flipped when it
|
||||
/// would cross an edge and then clamped, so no crosshair pixel can land on a label strip or
|
||||
/// a neighbouring figure row. Task 1's axes clip is the second line of defence, not the
|
||||
/// first.
|
||||
///
|
||||
/// Everything is default -- no lines, no panel, no rows -- when the crosshair is not
|
||||
/// active, and the rows alone are empty when there is no visible trace.
|
||||
struct CrosshairLayout {
|
||||
/// True when the crosshair was active for this layout.
|
||||
bool active = false;
|
||||
/// The vertical line's stroke box: `Style::line_width_px` wide in device pixels, as tall
|
||||
/// as `axes`, centred on the column of `Crosshair::x`. Zero-sized when the vertical line
|
||||
/// is not drawn.
|
||||
Rect vertical{};
|
||||
/// The horizontal line's stroke box, the same instrument turned a quarter turn. Zero-sized
|
||||
/// when the horizontal line is not drawn.
|
||||
Rect horizontal{};
|
||||
/// The readout panel, inside `axes`. Zero-sized when there is nothing to read out (no
|
||||
/// visible trace, nothing to measure text with, no room for even one row), when
|
||||
/// `Crosshair::show_readout` is off, or when the panel could not be placed inside the
|
||||
/// axes at all.
|
||||
Rect panel{};
|
||||
/// One row per visible trace, in trace order -- see `CrosshairRow`. Present whether or
|
||||
/// not the panel could list them, so the markers are drawn even in a plot too small to
|
||||
/// hold a panel.
|
||||
std::vector<CrosshairRow> rows;
|
||||
|
||||
bool empty() const noexcept { return !active; }
|
||||
};
|
||||
|
||||
/// The label an **unnamed** trace's readout row carries: `#` and the trace's position, e.g.
|
||||
/// `#2`.
|
||||
///
|
||||
/// Positional on purpose. The readout lists a trace because it is under the pointer, not
|
||||
/// because it has an identity, so an unnamed trace needs *some* stable caption or its rows
|
||||
/// would be indistinguishable -- and inventing a name for it (or borrowing a neighbour's)
|
||||
/// would be a claim about the data that nothing supports. The position is derived from the
|
||||
/// trace id alone, so the same trace keeps the same caption frame after frame.
|
||||
std::string crosshair_unnamed_label(u32 trace);
|
||||
|
||||
} // namespace plot
|
||||
@@ -139,6 +139,27 @@ class Figure {
|
||||
const FigureLayout& layout();
|
||||
const FigureLayout& layout() const noexcept { return layout_; }
|
||||
|
||||
// --- the crosshair ----------------------------------------------------
|
||||
|
||||
/// Places one crosshair x on **every** row (spec §15): the same data coordinate, so each
|
||||
/// row's readout lists its own traces at that instant and the rows read as one hover
|
||||
/// rather than N. That is what "synced if multiple plots available" means -- one pointer
|
||||
/// position, one x, N rows of values -- and it is the figure's to do rather than a host's
|
||||
/// because only the figure knows its rows.
|
||||
///
|
||||
/// Vertical-only, deliberately: the rows of a figure share *x* (`Figure`'s whole reason
|
||||
/// for stacking them and linking their x windows) and not y -- row 1 may be volts and row
|
||||
/// 2 degrees -- so a horizontal line across the figure would be a value that means
|
||||
/// different things in each row. A row whose series has no sample at `x` lists the trace
|
||||
/// with no value rather than inventing one.
|
||||
///
|
||||
/// A row is placed with `Plot::set_crosshair(x, active)`, which refuses a coordinate that
|
||||
/// is not a number; such a call therefore leaves every row's crosshair as it was.
|
||||
void set_crosshair(f64 x, bool active);
|
||||
|
||||
/// Clears the crosshair on every row. A row that never had one is unaffected.
|
||||
void clear_crosshair();
|
||||
|
||||
// --- the x link -------------------------------------------------------
|
||||
|
||||
/// A new link group, empty. Groups are numbered from 1 -- 0 is the unlinked
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -487,6 +487,18 @@ class Renderer {
|
||||
/// them again, so the panel a test asserts and the panel this draws are the same
|
||||
/// numbers.
|
||||
void draw_legend(const Plot& plot, const PlotLayout& layout);
|
||||
/// The crosshair: the two lines, a marker per trace, and the readout panel with a swatch and
|
||||
/// a value per listed row. Every rect -- the two stroke boxes, the panel, the markers'
|
||||
/// positions and the rows' boxes -- comes from `PlotLayout::crosshair`, which placed it
|
||||
/// inside `axes`; this stages exactly that geometry, so the crosshair a test measures and the
|
||||
/// crosshair a frame paints are one set of numbers. Clipped to `axes` with the rest of the
|
||||
/// interior passes, which is what keeps a marker for an off-scale sample inside the rect.
|
||||
///
|
||||
/// The panel is a backplate like the legend's, so it is drawn after the lines and the
|
||||
/// markers and before any of the frame's text. The rows' values were formatted by the plot
|
||||
/// (in the trace's own y axis' format) and are staged here as they stand -- this layer
|
||||
/// formats nothing.
|
||||
void draw_crosshair(const Plot& plot, const PlotLayout& layout);
|
||||
|
||||
/// The built-in backend the renderer owns, and the face the frames draw with:
|
||||
/// `set_text_backend()` writes `font_`, which starts here and comes back here
|
||||
|
||||
@@ -41,6 +41,13 @@ struct Style {
|
||||
Color axis_text = Color::hex(0xC8D0DC);
|
||||
Color cursor = Color::hex(0xFFB020);
|
||||
Color measure_fill = Color::hex(0xFFB020, 0.15f);
|
||||
/// The crosshair's colour: its two lines, its markers' own colour excepted, and the
|
||||
/// readout panel's border (spec §15). Deliberately **not** `cursor`: the cursor accent is
|
||||
/// an orange that a caller's own cursors, a region's bounds and its fill all already use,
|
||||
/// while the crosshair is a passive hover instrument that has to read as *chrome* over a
|
||||
/// dense trace rather than as another handle to grab. A pale cyan does that against the
|
||||
/// dark default theme, and it is a colour no default element of a frame shares.
|
||||
Color crosshair = Color::hex(0xB8E8FF);
|
||||
|
||||
// --- the scale ----------------------------------------------------------
|
||||
|
||||
|
||||
Reference in new issue
Block a user