feat(plot): nice-number ticks, axes, grid, traces, cursors, measurement queries
This commit is contained in:
1 parent
b4e88fe0c1
commit
93fc06f050
18 files changed
+1582
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user