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
+83
-4
@@ -160,6 +160,8 @@ u32 Plot::add_trace(const TraceDesc& desc) {
|
||||
|
||||
u32 Plot::add_cursor(f64 x, Color color) {
|
||||
Cursor cursor;
|
||||
// The default orientation: a marker at a data x. `set_cursor_x()` writes `x` for this
|
||||
// orientation and refuses it for the other (see CursorOrientation).
|
||||
cursor.x = x;
|
||||
cursor.color = color;
|
||||
cursor.x = snapped_x(cursor);
|
||||
@@ -168,6 +170,19 @@ u32 Plot::add_cursor(f64 x, Color color) {
|
||||
return static_cast<u32>(cursors_.size() - 1);
|
||||
}
|
||||
|
||||
u32 Plot::add_h_ruler(f64 y, Color color) {
|
||||
Cursor ruler;
|
||||
ruler.orientation = CursorOrientation::Horizontal;
|
||||
ruler.y = y;
|
||||
ruler.color = color;
|
||||
ruler.y = snapped_y(ruler);
|
||||
cursors_.push_back(ruler);
|
||||
// No mark_measures_dirty(): a measurement is read from two *x* coordinates (see
|
||||
// MeasureSlot, and `measurement()`'s own sample fetch), and a ruler holds neither, so
|
||||
// no cached result can have gone stale by adding one.
|
||||
return static_cast<u32>(cursors_.size() - 1);
|
||||
}
|
||||
|
||||
u32 Plot::add_measure(u32 cursor_a, u32 cursor_b, u32 trace) {
|
||||
MeasureSlot slot;
|
||||
slot.cursor_a = cursor_a;
|
||||
@@ -221,6 +236,18 @@ const Cursor& Plot::cursor(u32 id) const {
|
||||
return id < cursors_.size() ? cursors_[id] : fallback;
|
||||
}
|
||||
|
||||
f32 Plot::cursor_screen_coordinate(u32 id) const {
|
||||
if (id >= cursors_.size()) return 0.0f;
|
||||
const Cursor& cursor = cursors_[id];
|
||||
// Which coordinate is the marker's, decided once, here: the axis reached for is the one
|
||||
// the cursor's own orientation names, and the other orientation's coordinate is not
|
||||
// consulted at all -- a ruler is never placed by a dead x.
|
||||
if (cursor.orientation == CursorOrientation::Horizontal) {
|
||||
return static_cast<f32>(y_transform(cursor.axis_y).to_screen_y(cursor.y));
|
||||
}
|
||||
return static_cast<f32>(x_transform(cursor.axis_x).to_screen_x(cursor.x));
|
||||
}
|
||||
|
||||
const Annotation& Plot::annotation(u32 id) const {
|
||||
static const Annotation fallback{};
|
||||
return id < annotations_.size() ? annotations_[id] : fallback;
|
||||
@@ -473,18 +500,46 @@ void Plot::autoscale_axes() {
|
||||
|
||||
void Plot::set_cursor_x(u32 id, f64 x) {
|
||||
if (id >= cursors_.size()) return;
|
||||
cursors_[id].x = x;
|
||||
cursors_[id].x = snapped_x(cursors_[id]);
|
||||
Cursor& cursor = cursors_[id];
|
||||
// `x` is the vertical cursor's coordinate and nobody else's: a ruler's `x` is dead, and
|
||||
// writing it would leave the struct holding two coordinates that disagree about where the
|
||||
// cursor is (see CursorOrientation).
|
||||
if (cursor.orientation != CursorOrientation::Vertical) return;
|
||||
cursor.x = x;
|
||||
cursor.x = snapped_x(cursor);
|
||||
mark_measures_dirty();
|
||||
}
|
||||
|
||||
void Plot::set_cursor_snap(u32 id, SnapMode mode) {
|
||||
if (id >= cursors_.size()) return;
|
||||
cursors_[id].snap = mode;
|
||||
cursors_[id].x = snapped_x(cursors_[id]);
|
||||
Cursor& cursor = cursors_[id];
|
||||
// The vertical snap, on a vertical cursor: a ruler's snap is `HRulerSnap`, and the two
|
||||
// must not drift into meaning each other's job -- `NearestSample` is a sample of an x
|
||||
// grid, which a ruler has no notion of.
|
||||
if (cursor.orientation != CursorOrientation::Vertical) return;
|
||||
cursor.snap = mode;
|
||||
cursor.x = snapped_x(cursor);
|
||||
mark_measures_dirty();
|
||||
}
|
||||
|
||||
void Plot::set_cursor_y(u32 id, f64 y) {
|
||||
if (id >= cursors_.size()) return;
|
||||
Cursor& ruler = cursors_[id];
|
||||
if (ruler.orientation != CursorOrientation::Horizontal) return;
|
||||
ruler.y = y;
|
||||
ruler.y = snapped_y(ruler);
|
||||
// No mark_measures_dirty(), for `add_h_ruler()`'s reason: a measurement is read from two
|
||||
// x coordinates, and a ruler's y is not one of them.
|
||||
}
|
||||
|
||||
void Plot::set_cursor_h_snap(u32 id, HRulerSnap mode) {
|
||||
if (id >= cursors_.size()) return;
|
||||
Cursor& ruler = cursors_[id];
|
||||
if (ruler.orientation != CursorOrientation::Horizontal) return;
|
||||
ruler.h_snap = mode;
|
||||
ruler.y = snapped_y(ruler);
|
||||
}
|
||||
|
||||
const Measurement& Plot::measurement(u32 id) const {
|
||||
if (id >= measures_.size()) return invalid_measurement();
|
||||
const MeasureSlot& slot = measures_[id];
|
||||
@@ -799,6 +854,30 @@ f64 Plot::snapped_x(const Cursor& c) const {
|
||||
return c.snap == SnapMode::NearestSample ? snapped_x(c.x, c.axis_x) : c.x;
|
||||
}
|
||||
|
||||
f64 Plot::snapped_y(const Cursor& c) const {
|
||||
if (c.h_snap != HRulerSnap::NearestTick) return c.y;
|
||||
if (!std::isfinite(c.y)) return c.y; // no tick is nearer than a value that is not one
|
||||
if (c.axis_y >= y_axes_.size()) return c.y;
|
||||
// The tick set of the *last* layout(), never a second tick computation: `refresh_ticks()`
|
||||
// is the one place ticks are generated, and the controller lays the plot out before every
|
||||
// pointer event, so on the drag path this is the frame's own set (see the header's note on
|
||||
// the one-frame staleness a direct setter can see).
|
||||
const std::vector<Tick>& ticks = y_axes_[c.axis_y].ticks;
|
||||
if (ticks.empty()) return c.y;
|
||||
const Tick* nearest = &ticks.front();
|
||||
f64 best = std::fabs(nearest->value - c.y);
|
||||
for (const Tick& tick : ticks) {
|
||||
const f64 d = std::fabs(tick.value - c.y);
|
||||
// Strictly nearer, so a value exactly between two ticks lands on the lower one rather
|
||||
// than on whichever the iteration happened to visit last.
|
||||
if (d < best) {
|
||||
best = d;
|
||||
nearest = &tick;
|
||||
}
|
||||
}
|
||||
return nearest->value;
|
||||
}
|
||||
|
||||
u32 Plot::titled_x_axis_count() const noexcept {
|
||||
u32 n = 0;
|
||||
for (const Axis& a : x_axes_) {
|
||||
|
||||
Reference in new issue
Block a user