feat(plot): horizontal rulers alongside the vertical cursors

This commit is contained in:
Martino Ferrari committed 2026-09-28 17:18:43 +02:00
1 parent 2d819c4228
commit 63f9655ae1
11 files changed
+514 -29

No files matched your search

+59 -3
View File
@@ -197,6 +197,16 @@ class Plot {
u32 add_y_axis(const AxisDesc& desc);
u32 add_trace(const TraceDesc& desc);
u32 add_cursor(f64 x, Color color);
/// Adds a horizontal ruler at a data y (spec §14): the same instrument as `add_cursor()`
/// turned a quarter turn. It spans the axes' width at `y`, is grabbable and draggable
/// along y with the very same hit radius, and reports `CursorOrientation::Horizontal`.
///
/// `y` is on y axis 0, as `add_cursor()`'s `x` is on x axis 0. Its snap is `HRulerSnap`,
/// not `SnapMode`: a series is sampled in x, so the nearest *tick* of the y axis is the
/// only grid a y ruler can land on (`set_cursor_h_snap()`). It takes the next free id in
/// the one `cursors_` sequence both orientations share, so a vertical cursor added after
/// it is that id plus one.
u32 add_h_ruler(f64 y, Color color);
/// Measures the interval between two cursors over the series of `trace`.
u32 add_measure(u32 cursor_a, u32 cursor_b, u32 trace = 0);
u32 add_annotation(const Annotation& annotation);
@@ -233,6 +243,14 @@ class Plot {
const Axis& y_axis(u32 axis) const;
const Trace& trace(u32 id) const;
const Cursor& cursor(u32 id) const;
/// The device coordinate a cursor's marker sits at: the column of its `x` for a vertical
/// cursor, the row of its `y` for a horizontal ruler -- the one coordinate its own
/// orientation owns, mapped through the axis that coordinate is in. The one place that
/// choice is made, so the hit test the controller runs and the stroke the renderer stages
/// cannot disagree about where a cursor is (see CursorOrientation), and a host can place
/// its own tooltip or status line on the same number. 0.0f for an id that names no cursor,
/// like every other accessor here.
f32 cursor_screen_coordinate(u32 id) const;
const Annotation& annotation(u32 id) const;
/// The region with that id, or a default-constructed one: as everywhere else on
/// this class, a stale id reads a default rather than out of bounds.
@@ -361,12 +379,43 @@ class Plot {
// --- cursors and measurements -----------------------------------------
/// Moves a cursor and invalidates the measurements that depend on cursors.
/// A SnapMode::NearestSample cursor is pulled onto the nearest sample of the
/// first visible trace on its x axis.
/// Moves a **vertical** cursor along x, and invalidates the measurements that depend
/// on cursors. A `SnapMode::NearestSample` cursor is pulled onto the nearest sample of
/// the first visible trace on its x axis.
///
/// A horizontal ruler's `x` is not a coordinate of its: this is *ignored* for one, so
/// the dead field of an orientation can never be written behind its back (see
/// CursorOrientation). `set_cursor_y()` is the ruler's half of the pair.
void set_cursor_x(u32 id, f64 x);
/// Sets the snap of a **vertical** cursor, re-placing it under the new rule at once.
/// Ignored for a horizontal ruler, whose snap is `HRulerSnap` -- see
/// `set_cursor_h_snap()`.
void set_cursor_snap(u32 id, SnapMode mode);
/// Moves a **horizontal** ruler along y, re-placing it under its own snap rule.
///
/// Ignored for a vertical cursor -- `y` is not its coordinate -- and stored verbatim for
/// a ruler, exactly as `set_cursor_x()` stores a `Free` cursor's x: a y outside the axis'
/// range is a position the *renderer* skips, not one this store refuses, which is what
/// lets a caller park a ruler off range on purpose. The *drag* is what clamps: a pointer
/// that has left the axes has no data under it, so `input::Controller` keeps a dragged
/// ruler on its axis' range rather than storing a coordinate no pointer could reach
/// (see `Controller::move_cursor()`).
void set_cursor_y(u32 id, f64 y);
/// Sets the snap of a **horizontal** ruler: `NearestTick` pulls its y onto the nearest
/// tick of its y axis' tick set -- majors and minors alike, the one list the axis
/// refreshed -- and `Free` leaves it where it is.
///
/// The tick set it snaps to is the *last* `layout()`'s, because ticks are generated there
/// and nowhere else: snapping does not recompute them, so the library keeps one tick
/// computation rather than two. On the drag path that is the frame's own set --
/// `input::Controller::on_pointer()` lays the plot out before every event it applies --
/// while a caller that moves a y range and then sets a ruler's y *without* laying out
/// snaps against the previous set, one frame stale. Laying the plot out refreshes it.
///
/// Ignored for a vertical cursor, whose snap is `SnapMode` -- see `set_cursor_snap()`.
void set_cursor_h_snap(u32 id, HRulerSnap mode);
/// The measurement for `id`. Re-evaluated only when a cursor moved, when the
/// measure was added, or when the series underneath it changed: a different
/// series, a longer one, or a sliding window whose sample count stayed the same
@@ -559,6 +608,13 @@ class Plot {
/// `c.x` pulled onto the nearest sample when the cursor asks to snap: the same one
/// rule, applied to the axis the cursor names.
f64 snapped_x(const Cursor& c) const;
/// `c.y` pulled onto the nearest tick of `c.axis_y`'s tick set when the ruler asks to
/// snap, else `c.y` -- `HRulerSnap`'s one rule and the only place a ruler's snap is
/// decided. The tick set is the last `layout()`'s (see `set_cursor_h_snap()`), and the
/// nearest of it wins with a tie going to the lower tick; an axis that has not been laid
/// out yet, one holding no ticks, and a `y` that is not a number all leave the value as
/// it is rather than inventing a tick to land on.
f64 snapped_y(const Cursor& c) const;
/// Measures every visible region into `layout_.regions`: the fill's clipped
/// interval, one grab tab per bound, and the label's line box. Also the place a
/// region's pixel geometry is derived -- the controller hit-tests what this