244 lines
14 KiB
C++
244 lines
14 KiB
C++
#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 {
|
|
|
|
/// A per-`Crosshair` override of the three style knobs spec §17 adds, in **logical**
|
|
/// pixels, so one plot's crosshair can differ -- be thicker, or fatter-dashed -- without a
|
|
/// caller touching the global `render::Style` the rest of the frame draws with.
|
|
///
|
|
/// A negative value means "not overridden": the field falls back to the matching
|
|
/// `Style::crosshair_*` metric, so a default-constructed `CrosshairStyle` (every field
|
|
/// `-1`) is exactly the global style and the common case costs nothing. `width`, `dash_on`
|
|
/// and `dash_off` are independent, so a caller may override only the dash, or only the
|
|
/// width, and leave the rest to the style.
|
|
struct CrosshairStyle {
|
|
f32 width = -1.0f; ///< logical stroke width; `< 0` takes `Style::crosshair_width`.
|
|
f32 dash_on = -1.0f; ///< logical ink length; `< 0` takes `Style::crosshair_dash_on`.
|
|
f32 dash_off = -1.0f; ///< logical gap; `< 0` takes `Style::crosshair_dash_off`, and `0`
|
|
///< (once resolved) means solid.
|
|
};
|
|
|
|
/// 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;
|
|
/// This plot's own stroke and dash, when it wants to differ from the global style; a
|
|
/// default-constructed `CrosshairStyle` defers every field to `render::Style` (see
|
|
/// `CrosshairStyle`). Set through `Plot::set_crosshair_style()`.
|
|
CrosshairStyle style{};
|
|
};
|
|
|
|
/// 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 truncation. 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;
|
|
/// What the row's value column shows. For a `valid` row it is the formatted number, written in
|
|
/// the trace's y axis' own tick format (`Axis::format_value()`, so a readout reads exactly like
|
|
/// the labels beside it); for a row whose trace has **no sample at the crosshair's x** it is
|
|
/// `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.
|
|
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, right-aligned on the panel's inner edge and one gutter's worth of
|
|
/// measurement away from the label's own box. Empty when `!listed`: a row that is not in the panel
|
|
/// stages neither run, while a listed row always stages something -- the number, or
|
|
/// `kCrosshairNoValue`.
|
|
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 stroke width the two lines are drawn at, in **device** pixels: the crosshair's own
|
|
/// `CrosshairStyle::width` when it overrides it, else `Style::crosshair_width`, both through
|
|
/// `Style::device()` -- resolved here once, so the stroke boxes above and the renderer's
|
|
/// strokes are one number rather than two that must agree.
|
|
f32 stroke = 0.0f;
|
|
/// The dash pattern in **device** pixels: `x` the ink length, `y` the gap. `y <= 0` means
|
|
/// solid -- the state whenever no dash is asked for, which is what keeps the shared line
|
|
/// pipeline drawing every other stroke in the library unchanged. The dash is a fragment-shader
|
|
/// effect on `v_along`, so this is the only CPU-side change it makes (spec §17).
|
|
Vec2 dash{};
|
|
/// 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; }
|
|
};
|
|
|
|
/// What a listed readout row shows in its value column when its trace has **no sample at the
|
|
/// crosshair's x**: an em dash, not a blank.
|
|
///
|
|
/// The row is listed either way -- the trace *is* under the pointer, and a panel that grew and
|
|
/// shrank with the pointer would be worse to read than one that says "nothing here" -- so the
|
|
/// absence has to be *said*: a blank value cell reads as a value that has not arrived yet. The em
|
|
/// dash is in the embedded font's coverage (spec §2 covers U+2013/U+2014), it is not a number, and
|
|
/// it cannot be mistaken for one.
|
|
inline constexpr const char* kCrosshairNoValue = "\u2014";
|
|
|
|
/// 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
|