feat(plot): the figure crosshair marks the hovered row, and the stroke is thin, dashed and configurable

This commit is contained in:
Martino Ferrari committed 2026-09-28 20:17:01 +02:00
1 parent cae672bc99
commit eee7c34dd5
12 files changed
+166 -12

No files matched your search

+30
View File
@@ -9,6 +9,22 @@
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.
///
@@ -46,6 +62,10 @@ struct Crosshair {
/// 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
@@ -177,6 +197,16 @@ struct CrosshairLayout {
/// 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
+23
View File
@@ -161,10 +161,33 @@ class Figure {
/// 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 host that *does* know which row the pointer is in -- every adapter does, from its own
/// pointer position -- uses the four-argument overload below instead, which adds the pointer
/// row's own horizontal line; this overload is for a caller that has only a coordinate.
///
/// 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);
/// Places the crosshair with the pointer's **row** and that row's local y (spec §17): the
/// shared x goes to every row -- vertical lines everywhere, as the plain overload does -- and
/// the horizontal line is drawn **only in `hovered_row`**, where its y is a value of *that*
/// row's own scale. It is the fix for the reported defect -- a figure showing no horizontal
/// line at all -- without inventing a figure-wide y, which would mean a different value in
/// every row: the line appears where the pointer is, and no row ever shows another row's y.
///
/// `y_in_row` is a data coordinate on `hovered_row`'s y axis 0, exactly as `Crosshair::y`'s
/// two-coordinate placement is; a host reads it from the row's own y transform at the
/// pointer's pixel row. `hovered_row` outside `[0, row_count())` is treated as "no row under
/// the pointer": every row gets the vertical line and none gets a horizontal one, the same
/// state the single-coordinate overload produces.
///
/// Refused, changing nothing, when `x` is not a number -- matching `set_crosshair(x, active)`
/// and `Plot::set_crosshair()`'s own rule. A non-finite `y_in_row` leaves `hovered_row` with
/// the vertical line only (the y is dropped, the hover is not), because a row with no valid y
/// is still a row the pointer is in.
void set_crosshair(f64 x, u32 hovered_row, f64 y_in_row, bool active);
/// Clears the crosshair on every row. A row that never had one is unaffected.
void clear_crosshair();
+11
View File
@@ -591,6 +591,17 @@ class Plot {
/// **not** part of `set_crosshair()`, so placing a hover cannot silently change it.
void set_crosshair_readout(bool on) noexcept { crosshair_.show_readout = on; }
/// Overrides this plot's crosshair stroke and dash, in logical pixels, without touching the
/// global style every other plot draws with (spec §17): each field left negative in `s`
/// defers to the matching `Style::crosshair_*` metric, so `CrosshairStyle{}` (the default)
/// is exactly the global style. Set it once, like a colour -- it is not part of
/// `set_crosshair()`, so placing a hover cannot silently change how the stroke looks. A
/// `dash_off` that resolves to `0` draws a solid line.
void set_crosshair_style(const CrosshairStyle& s) noexcept { crosshair_.style = s; }
/// The crosshair's style override, as set -- read back rather than the *resolved* stroke and
/// dash, which live on `CrosshairLayout` because they depend on `ui_scale`.
const CrosshairStyle& crosshair_style() const noexcept { return crosshair_.style; }
/// 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.
+5
View File
@@ -27,6 +27,11 @@ class LineProgram {
void set_viewport(Vec2 px) const noexcept;
/// `u_width`: stroke width in pixels.
void set_width(f32 px) const noexcept;
/// `u_dash`: the dash pattern in pixels -- `x` the ink length, `y` the gap, and a
/// non-positive `y` (the default a caller passes) means **solid**. It is a fragment-shader
/// discard on the interpolated `v_along`, so it costs no geometry, and it is the one uniform
/// that distinguishes the crosshair's stroke from every other line in the library.
void set_dash(Vec2 on_off) const noexcept;
/// `u_color`: stroke colour, alpha included.
void set_color(Color color) const noexcept;
+6 -1
View File
@@ -365,7 +365,12 @@ class Renderer {
/// Uploads the staged line batch, maps it with (`scale`, `offset`), rasterises
/// it in one colour and empties the batch. A no-op for an empty batch, so the
/// caller can flush unconditionally.
void flush_lines(const Mapping& map, Color color, f32 width) noexcept;
///
/// `dash` is the pattern in pixels for the fragment shader's discard: `x` the ink
/// length, `y` the gap, and a non-positive `y` -- the default every caller but the
/// crosshair passes -- means **solid**. It is set on every flush, so one draw's dash
/// can never leak into the next.
void flush_lines(const Mapping& map, Color color, f32 width, Vec2 dash = Vec2{}) noexcept;
/// Draws every staged glyph in `style().axis_text` and empties the batch. Text
/// is texture-bound, so this runs once per frame, after all line geometry.
void flush_text() noexcept;
+15
View File
@@ -54,6 +54,21 @@ struct Style {
/// already distinct, so no dashes.
Color crosshair = Color::hex(0xE6EBF2);
/// The crosshair's stroke and dash, in logical pixels (spec §17). The crosshair is a
/// *passive* hover instrument drawn over the data, so its default **recedes**: a
/// thinner stroke than a trace's (`Trace::width`'s 1.5) so it reads as chrome rather
/// than as data, and a dash so it is distinguishable from the grid and the traces at
/// a glance.
///
/// `crosshair_dash_on` is the ink length and `crosshair_dash_off` the gap; **`off == 0`
/// means solid**, which is the value every *other* line in the library is drawn at, so
/// the one pipeline serves them unchanged (the dash is a fragment-shader discard on
/// `v_along`, so no geometry is added). A `Crosshair` may override all three for one
/// plot without touching this global style.
f32 crosshair_width = 0.75f;
f32 crosshair_dash_on = 4.0f;
f32 crosshair_dash_off = 4.0f;
// --- the scale ----------------------------------------------------------
/// Device pixels per logical unit. 1.0 is the 1x display every default here was