feat(plot): horizontal rulers alongside the vertical cursors
This commit is contained in:
1 parent
2d819c4228
commit
63f9655ae1
11 files changed
+514
-29
No files matched your search
@@ -81,7 +81,15 @@ using plot::RegionGrab;
|
||||
/// nearest one winning. A marker is drawn on top of everything and is hit-tested
|
||||
/// the same way: the shaded band between two measurement cursors is grabbed by
|
||||
/// dragging either of the cursors that bound it, because its edges *are* those
|
||||
/// cursors -- no second path, no second tolerance.
|
||||
/// cursors -- no second path, no second tolerance. A *horizontal* ruler is the same
|
||||
/// target turned a quarter turn: the distance measured is the pointer's to the
|
||||
/// ruler's row rather than to a marker's column, and it is the same radius, so a
|
||||
/// point within reach of one of each resolves to the nearer and neither orientation
|
||||
/// widens the other's reach. The drag that follows writes only the coordinate the
|
||||
/// grabbed cursor's orientation owns -- `set_cursor_x()` for a marker,
|
||||
/// `set_cursor_y()` for a ruler -- and a dragged ruler is clamped to its own y
|
||||
/// axis' range, because a pointer past the top or bottom of `axes` has no data
|
||||
/// under it to follow (see `move_cursor()`).
|
||||
/// 2. **Regions next**, resolved nearest-first against the geometry `Plot::layout()`
|
||||
/// measured: a bound within the grab radius beats the body (so the bound of a
|
||||
/// wide region is grabbable even though the pointer is inside the fill), the
|
||||
@@ -184,8 +192,9 @@ class Controller {
|
||||
DragMode drag_mode() const noexcept { return mode_; }
|
||||
/// The current box-zoom selection in pixels; meaningful while BoxZoom.
|
||||
Rect selection_rect() const noexcept { return selection_; }
|
||||
/// The cursor under the pointer, or `kInvalid` when the pointer is outside the
|
||||
/// plot area or no cursor is within a few pixels.
|
||||
/// The cursor under the pointer -- a vertical marker or a horizontal ruler, whichever
|
||||
/// line the pointer is nearer -- or `kInvalid` when the pointer is outside the plot area
|
||||
/// or no cursor is within a few pixels.
|
||||
u64 hovered_cursor() const;
|
||||
/// The cursor a drag is moving, or `kInvalid` when no cursor drag is open. The two
|
||||
/// are the same answer the plot publishes for the renderer (see HotHandles), for a
|
||||
|
||||
@@ -84,6 +84,14 @@ struct Axis {
|
||||
/// `format` -- `TickFormat::Time` relabels the majors with `format_time`, which
|
||||
/// `generate_ticks` cannot know about.
|
||||
void refresh(f32 px, f32 min_px, bool include_minors = true);
|
||||
|
||||
/// Writes `v` the way this axis' own tick labels are written: `format_time` on a
|
||||
/// `Time` axis, `format_si` otherwise. The one rule for "how a value on this axis
|
||||
/// reads", exposed so a consumer that has to caption a value the axis carries -- a
|
||||
/// horizontal ruler's label -- writes it exactly as the tick beside it is written.
|
||||
/// `buf` is NUL-terminated and never written past `n` bytes (both formatters
|
||||
/// guarantee that).
|
||||
void format_value(f64 v, char* buf, usize n) const;
|
||||
};
|
||||
|
||||
} // namespace plot
|
||||
@@ -12,14 +12,59 @@ enum class SnapMode {
|
||||
NearestSample, ///< the cursor is pulled onto the nearest sample of the trace's x grid.
|
||||
};
|
||||
|
||||
/// A vertical marker at data x. `axis_x` names the x axis whose data coordinate
|
||||
/// `x` is in; `label` is an optional caption drawn next to the marker.
|
||||
/// Which of a cursor's two coordinates is live, and how it reads: the instrument
|
||||
/// the plot has always had, and the same one turned a quarter turn (spec §14).
|
||||
///
|
||||
/// `Vertical` is a marker at a data `x` spanning the axes' *height*; `Horizontal` is
|
||||
/// a ruler at a data `y` spanning their *width*. One type rather than two, because
|
||||
/// the two differ in exactly one thing -- which coordinate a drag writes -- and every
|
||||
/// other part of the machinery (the hit radius, the drag, the hover highlight, the
|
||||
/// label path) is then one path instead of two that would have to be kept in step.
|
||||
///
|
||||
/// The orientation is what says which fields of `Cursor` mean anything:
|
||||
///
|
||||
/// | orientation | live coordinate | live snap | dead fields |
|
||||
/// |--------------|-------------------|------------------------------|-------------------------|
|
||||
/// | `Vertical` | `x`, on `axis_x` | `snap` (`SnapMode`) | `y`, `h_snap`, `axis_y` |
|
||||
/// | `Horizontal` | `y`, on `axis_y` | `h_snap` (`HRulerSnap`) | `x`, `snap`, `axis_x` |
|
||||
///
|
||||
/// A dead field is *ignored*, not honoured: `Plot::set_cursor_y()` on a vertical
|
||||
/// cursor and `Plot::set_cursor_x()` on a horizontal ruler change nothing, and neither
|
||||
/// does the other orientation's snap setter (see `Plot::set_cursor_h_snap()`). A dead
|
||||
/// field that could be written would leave the struct holding two coordinates that
|
||||
/// could disagree about where the cursor is, and the two snaps free to drift into
|
||||
/// meaning each other's job -- `SnapMode::NearestSample` is a sample of an *x* grid,
|
||||
/// while `HRulerSnap::NearestTick` is a tick of a *y* axis.
|
||||
enum class CursorOrientation {
|
||||
Vertical, ///< a marker at a data x, spanning the axes' height.
|
||||
Horizontal, ///< a ruler at a data y, spanning the axes' width.
|
||||
};
|
||||
|
||||
/// How a horizontal ruler's `y` relates to its y axis' tick set.
|
||||
///
|
||||
/// Deliberately not a `SnapMode`: a vertical cursor snaps onto a *sample* of the
|
||||
/// trace's x grid, and a series is sampled in x, so "the nearest y sample" is not a
|
||||
/// thing that exists. What a y ruler has to land on is a *tick* -- one of the values
|
||||
/// the y axis is labelled with.
|
||||
enum class HRulerSnap {
|
||||
Free, ///< the ruler stays exactly where the caller or the drag put it.
|
||||
NearestTick, ///< the ruler is pulled onto the nearest tick of its y axis' tick set.
|
||||
};
|
||||
|
||||
/// A marker at a data coordinate. Which coordinate -- and which snap -- is live, and
|
||||
/// which fields are dead, is `orientation`'s to say (see CursorOrientation); `label`
|
||||
/// is an optional caption drawn beside the marker, and a horizontal ruler
|
||||
/// additionally draws its own value, written in its y axis' own tick format.
|
||||
struct Cursor {
|
||||
f64 x = 0.0;
|
||||
SnapMode snap = SnapMode::Free;
|
||||
CursorOrientation orientation = CursorOrientation::Vertical;
|
||||
f64 x = 0.0; ///< the data x. Live for `Vertical`, dead for `Horizontal`.
|
||||
f64 y = 0.0; ///< the data y. Live for `Horizontal`, dead for `Vertical`.
|
||||
SnapMode snap = SnapMode::Free; ///< the vertical snap. Dead for `Horizontal`.
|
||||
HRulerSnap h_snap = HRulerSnap::Free; ///< the horizontal snap. Dead for `Vertical`.
|
||||
Color color = Color::hex(0xFFB020);
|
||||
std::string label;
|
||||
u32 axis_x = 0;
|
||||
u32 axis_x = 0; ///< the x axis `x` is in. Dead for `Horizontal`.
|
||||
u32 axis_y = 0; ///< the y axis `y` is in. Dead for `Vertical`.
|
||||
bool visible = true;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -452,7 +452,19 @@ class Renderer {
|
||||
/// The data helpers take it too; they read the axes rect and the transforms it
|
||||
/// pins.
|
||||
void draw_traces(const Plot& plot, const PlotLayout& layout);
|
||||
/// The cursors, both orientations: one stroke per cursor, placed at the coordinate its own
|
||||
/// orientation owns (`Plot::cursor_screen_coordinate()`) and spanning the other axis' whole
|
||||
/// range, in its own colour -- or in the accent at handle width while it is hovered or
|
||||
/// grabbed, which is the same cue a region's hot bound gets. A cursor at the far edge of
|
||||
/// its range is skipped rather than drawn as half a stroke over the strip beside it.
|
||||
void draw_cursors(const Plot& plot, const PlotLayout& layout);
|
||||
/// One cursor's label: the caller's caption for either orientation, and for a horizontal
|
||||
/// ruler the value itself, written in its y axis' own tick format (`Axis::format_value()`)
|
||||
/// -- the number a ruler exists to be read off. A vertical cursor's caption starts at the
|
||||
/// top of the axes beside its marker; a ruler's text starts at the axes' left edge on its
|
||||
/// own row, with the caption first on that line when there is one.
|
||||
void draw_cursor_label(const Plot& plot, const PlotLayout& layout, const Cursor& cursor,
|
||||
f32 at);
|
||||
/// The regions: the fill, one bound per edge, the optional label. Every rect comes
|
||||
/// from `PlotLayout::regions` -- the same geometry the controller hit-tests -- so
|
||||
/// a region is drawn where it can be grabbed, and neither side derives the other's
|
||||
|
||||
Reference in new issue
Block a user