feat(plot): nice-number ticks, axes, grid, traces, cursors, measurement queries

This commit is contained in:
Martino Ferrari committed 2026-09-27 22:21:22 +02:00
1 parent b4e88fe0c1
commit 93fc06f050
18 files changed
+1582

No files matched your search

+195
View File
@@ -0,0 +1,195 @@
#pragma once
#include <vector>
#include <plot/core/rect.hpp>
#include <plot/core/types.hpp>
#include <plot/math/transform.hpp>
#include <plot/plot/annotation.hpp>
#include <plot/plot/axis.hpp>
#include <plot/plot/cursor.hpp>
#include <plot/plot/grid.hpp>
#include <plot/plot/measure.hpp>
#include <plot/plot/trace.hpp>
#include <plot/render/style.hpp>
#include <plot/render/text.hpp>
namespace plot {
/// Pixel geometry of a plot's chrome, all of it in the same pixel space as
/// `Rect` (origin top-left). `axes` is the area data is drawn into; the two label
/// areas are the strips the tick labels live in.
struct PlotLayout {
Rect axes;
Rect y_label_area;
Rect x_label_area;
/// Left strip width. The default is used when no text backend has been set.
f32 y_axis_width = 56.0f;
/// Bottom strip height. The default is used when no text backend has been set.
f32 x_axis_height = 22.0f;
};
/// The public façade: axes, traces, cursors, annotations and their layout.
///
/// It is pure CPU state -- no GL context, no font of its own. The renderer hands
/// it the font to measure labels with (set_text_backend) and reads layout(),
/// the transforms and the measurement results back out. Everything the renderer
/// draws (tick geometry, grid segments, cursor x in data space) is described in
/// data coordinates and pixels here; nothing is uploaded anywhere.
///
/// Owned collections are indexed by the `u32` id the matching `add_*` returned.
/// A stale or unknown id is not fatal: accessors return a default-constructed
/// object and measurement() returns an invalid result.
class Plot {
public:
Plot() = default;
// --- viewport, style, text metrics ------------------------------------
/// The rectangle this plot occupies on screen, in framebuffer pixels.
void set_viewport(Rect px) noexcept { viewport_ = px; }
const Rect& viewport() const noexcept { return viewport_; }
void set_style(const render::Style& s) noexcept { style_ = s; }
const render::Style& style() const noexcept { return style_; }
/// The font layout() measures tick labels with. Borrowed, not owned, and may
/// be null -- then the label areas fall back to PlotLayout's defaults and no
/// string is measured. The caller (the renderer) owns the backend; Plot never
/// creates one and never touches GL.
void set_text_backend(const render::TextBackend* text) noexcept { text_ = text; }
// --- construction -----------------------------------------------------
u32 add_x_axis(const AxisDesc& desc);
u32 add_y_axis(const AxisDesc& desc);
u32 add_trace(const TraceDesc& desc);
u32 add_cursor(f64 x, 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);
// --- accessors --------------------------------------------------------
// An id that does not name an object yields a default-constructed one, so a
// stale id can never read out of bounds.
u32 x_axis_count() const noexcept { return static_cast<u32>(x_axes_.size()); }
u32 y_axis_count() const noexcept { return static_cast<u32>(y_axes_.size()); }
u32 trace_count() const noexcept { return static_cast<u32>(traces_.size()); }
u32 cursor_count() const noexcept { return static_cast<u32>(cursors_.size()); }
u32 measure_count() const noexcept { return static_cast<u32>(measures_.size()); }
u32 annotation_count() const noexcept { return static_cast<u32>(annotations_.size()); }
const Axis& x_axis(u32 axis) const;
const Axis& y_axis(u32 axis) const;
const Trace& trace(u32 id) const;
const Cursor& cursor(u32 id) const;
const Annotation& annotation(u32 id) const;
// --- ranges and transforms --------------------------------------------
Range x_range(u32 axis) const;
Range y_range(u32 axis) const;
/// Sets an explicit range. Choosing a range is a user decision, so it also
/// clears that axis' auto_scale: a later layout() must not snap the range back.
/// auto_fit_x()/auto_fit_y() re-arm it.
void set_x_range(u32 axis, Range r);
void set_y_range(u32 axis, Range r);
/// Re-arms (or disarms) the per-layout auto scaling of one axis.
void set_x_auto_scale(u32 axis, bool on);
void set_y_auto_scale(u32 axis, bool on);
/// Data -> pixels for one axis, built from the axis' range, log flag and the
/// current layout. Always invertible: a collapsed range or a collapsed axes
/// rect is widened by math::Transform, so to_data_*(to_screen_*(v)) == v.
Transform x_transform(u32 axis) const;
Transform y_transform(u32 axis) const;
/// Fits every x axis to the union of the series bounds of the traces bound to
/// it, padded by 2% of the span, and re-arms auto scale on the axes it fitted.
/// An axis with no visible trace is left exactly as it was.
void auto_fit_x();
/// Same for one y axis, over that axis' traces' y bounds.
void auto_fit_y(u32 axis);
// --- 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.
void set_cursor_x(u32 id, f64 x);
void set_cursor_snap(u32 id, SnapMode mode);
/// The measurement for `id`. Re-evaluated only when a cursor moved, when the
/// measure was added, or when its series changed (its pointer or its sample
/// count) -- never once per frame. An unknown id, a null-series trace, an
/// inverted cursor pair or an empty series yields `valid == false`. const
/// because recomputing fills a cache: nothing a caller can observe changes.
const Measurement& measurement(u32 id) const;
/// Drops every cached measurement. Needed after a series is mutated in place
/// (data::ArraySeries::commit() with the same sample count), which a pointer
/// and size check cannot detect.
void invalidate_measurements() noexcept;
// --- layout -----------------------------------------------------------
/// Recomputes the layout, every axis' ticks and the grid, then returns the
/// layout. The label strips come from the text backend's metrics when one is
/// set (`measure()` of the widest y tick label, `height()` for the x strip) and
/// from PlotLayout's defaults otherwise.
const PlotLayout& layout();
/// The layout as of the last refresh. The const overload never recomputes, so
/// a lookup on a const Plot cannot move the axes out from under the caller.
const PlotLayout& layout() const noexcept { return layout_; }
/// Grid segments for the primary axis pair (x axis 0, y axis 0), in data space.
const Grid& grid() const noexcept { return grid_; }
private:
/// One measure plus its cache. The dirty flag is what keeps measurement()
/// from re-evaluating every frame; `last_series`/`last_size` catch a trace
/// being pointed at a different (or grown) series without anyone telling us.
struct MeasureSlot {
u32 cursor_a = 0;
u32 cursor_b = 0;
u32 trace = 0;
mutable Measurement result;
mutable bool dirty = true;
mutable const data::Series* last_series = nullptr;
mutable u64 last_size = 0;
};
/// Fit one axis to its traces' bounds, leaving auto_scale alone. False when no
/// visible trace is bound to the axis, in which case the range is untouched.
bool fit_x_axis(u32 axis);
bool fit_y_axis(u32 axis);
/// Refit every axis that is still on auto scale. layout() runs this, so a
/// growing stream stays framed until a caller gesture turns auto scale off.
void autoscale_axes();
void refresh_ticks();
void build_grid();
void mark_measures_dirty() noexcept;
/// `c.x` pulled onto the nearest sample when the cursor asks to snap.
f64 snapped_x(const Cursor& c) const;
/// Widest y tick label plus the label padding on both sides: the left strip the
/// text backend asks for. Zero with no backend or no y axis.
f32 y_label_width() const;
Rect viewport_{};
render::Style style_{};
const render::TextBackend* text_ = nullptr;
std::vector<Axis> x_axes_;
std::vector<Axis> y_axes_;
std::vector<Trace> traces_;
std::vector<Cursor> cursors_;
std::vector<MeasureSlot> measures_;
std::vector<Annotation> annotations_;
PlotLayout layout_{};
Grid grid_;
};
} // namespace plot