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
@@ -501,6 +501,111 @@ PLOT_TEST(inverted_and_out_of_range_measurements_are_invalid) {
|
||||
PLOT_CHECK(!p.measurement(99u).valid); // unknown measure id
|
||||
}
|
||||
|
||||
/// Spec §14: a cursor is one instrument in two orientations, so it owns exactly one coordinate
|
||||
/// and exactly one snap. The dead field of the other orientation is *ignored*, not honoured -- a
|
||||
/// setter that wrote it would leave the struct holding two coordinates that could disagree about
|
||||
/// where the cursor is, and would let the two snaps drift into meaning each other's job
|
||||
/// (`SnapMode::NearestSample` is a sample of an x grid; `HRulerSnap::NearestTick` is a tick of a
|
||||
/// y axis). The store's half of the ruler, with no pointer and no renderer involved.
|
||||
PLOT_TEST(a_cursor_owns_only_the_coordinate_and_snap_of_its_orientation) {
|
||||
plot::data::ArraySeries s(ramp(100), 0.0, 1.0);
|
||||
plot::Plot p;
|
||||
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
||||
const u32 ax = p.add_x_axis({"t", {0.0, 99.0}, false, plot::AxisPlace::Bottom, false});
|
||||
const u32 ay = p.add_y_axis({"v", {0.0, 99.0}, false, plot::AxisPlace::Left, false});
|
||||
p.add_trace({&s, plot::Color::hex(0xFFFFFF), 1.0f, ax, ay});
|
||||
const u32 marker = p.add_cursor(10.0, plot::Color::hex(0xFFB020));
|
||||
const u32 ruler = p.add_h_ruler(50.0, plot::Color::hex(0x35D0A8));
|
||||
|
||||
// The orientation each constructor claims, and the coordinate it stored in.
|
||||
PLOT_CHECK(p.cursor(marker).orientation == plot::CursorOrientation::Vertical);
|
||||
PLOT_CHECK(p.cursor(ruler).orientation == plot::CursorOrientation::Horizontal);
|
||||
PLOT_CHECK_NEAR(p.cursor(marker).x, 10.0, 1e-12);
|
||||
PLOT_CHECK_NEAR(p.cursor(ruler).y, 50.0, 1e-12);
|
||||
|
||||
// The coordinate the other orientation owns, twice over: `y` is not a marker's to move and
|
||||
// `x` is not a ruler's, so neither field can be written behind the orientation's back.
|
||||
p.set_cursor_y(marker, 77.0);
|
||||
PLOT_CHECK_NEAR(p.cursor(marker).y, 0.0, 1e-12); // still the default it was born with
|
||||
PLOT_CHECK_NEAR(p.cursor(marker).x, 10.0, 1e-12);
|
||||
p.set_cursor_x(ruler, 77.0);
|
||||
PLOT_CHECK_NEAR(p.cursor(ruler).x, 0.0, 1e-12);
|
||||
PLOT_CHECK_NEAR(p.cursor(ruler).y, 50.0, 1e-12);
|
||||
|
||||
// `SnapMode::NearestSample` on a ruler is ignored too: 33.37 is no sample of the x grid and no
|
||||
// tick of the y axis, so an honoured sample-snap -- onto 33 or onto the nearest y tick -- would
|
||||
// show up as a move, and neither did.
|
||||
p.set_cursor_y(ruler, 33.37);
|
||||
p.set_cursor_snap(ruler, plot::SnapMode::NearestSample);
|
||||
PLOT_CHECK(p.cursor(ruler).snap == plot::SnapMode::Free);
|
||||
PLOT_CHECK_NEAR(p.cursor(ruler).y, 33.37, 1e-12);
|
||||
|
||||
// The marker's half of the same rule: `NearestTick` does not touch its x, while its own
|
||||
// `NearestSample` does -- so the two modes are live on exactly one orientation each.
|
||||
p.set_cursor_x(marker, 12.37);
|
||||
PLOT_CHECK_NEAR(p.cursor(marker).x, 12.37, 1e-12); // Free: stored verbatim
|
||||
p.set_cursor_h_snap(marker, plot::HRulerSnap::NearestTick);
|
||||
PLOT_CHECK(p.cursor(marker).h_snap == plot::HRulerSnap::Free);
|
||||
PLOT_CHECK_NEAR(p.cursor(marker).x, 12.37, 1e-12);
|
||||
p.set_cursor_snap(marker, plot::SnapMode::NearestSample);
|
||||
PLOT_CHECK(p.cursor(marker).snap == plot::SnapMode::NearestSample);
|
||||
PLOT_CHECK_NEAR(p.cursor(marker).x, 12.0, 1e-12);
|
||||
}
|
||||
|
||||
/// `NearestTick` lands on a tick of the ruler's own y axis, and `Free` stores the value it was
|
||||
/// handed -- so the two modes are told apart by what the store holds, not by a flag. The tick set
|
||||
/// is the one `layout()` refreshed: ticks are generated there and nowhere else, which is why a
|
||||
/// range moved without a layout snaps against the previous set. That one-frame staleness is
|
||||
/// pinned here, so a later change cannot quietly reintroduce a second tick computation.
|
||||
PLOT_TEST(horizontal_ruler_snaps_to_the_y_ticks_when_asked) {
|
||||
plot::data::ArraySeries s(ramp(100), 0.0, 1.0);
|
||||
plot::Plot p;
|
||||
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
||||
const u32 ay = p.add_y_axis({"v", {0.0, 100.0}, false, plot::AxisPlace::Left, false});
|
||||
const u32 ruler = p.add_h_ruler(0.0, plot::Color::hex(0x35D0A8));
|
||||
p.layout(); // the tick set every snap below reads
|
||||
|
||||
const std::vector<plot::Tick>& ticks = p.y_axis(ay).ticks;
|
||||
PLOT_CHECK(ticks.size() >= 3u);
|
||||
// Two *adjacent* ticks, so the value between them is equal to no entry of the list at all --
|
||||
// majors and minors alike: a ruler snaps onto the whole set, and no tick lies between two
|
||||
// neighbours.
|
||||
const f64 lo = ticks[1].value;
|
||||
const f64 hi = ticks[2].value;
|
||||
PLOT_CHECK(lo < hi);
|
||||
const f64 between = lo + 0.6 * (hi - lo); // nearer `hi`, and equal to neither
|
||||
PLOT_CHECK(!tick_at(ticks, between));
|
||||
|
||||
p.set_cursor_h_snap(ruler, plot::HRulerSnap::NearestTick);
|
||||
p.set_cursor_y(ruler, between);
|
||||
PLOT_CHECK_NEAR(p.cursor(ruler).y, hi, 1e-12); // pulled onto the nearer tick
|
||||
PLOT_CHECK(tick_at(ticks, p.cursor(ruler).y));
|
||||
|
||||
// The same value with `Free` is not moved, and is equal to no tick: the mode is the snap, not
|
||||
// the value.
|
||||
p.set_cursor_h_snap(ruler, plot::HRulerSnap::Free);
|
||||
p.set_cursor_y(ruler, between);
|
||||
PLOT_CHECK_NEAR(p.cursor(ruler).y, between, 1e-12);
|
||||
PLOT_CHECK(!tick_at(p.y_axis(ay).ticks, p.cursor(ruler).y));
|
||||
|
||||
// A range moved without a layout leaves the previous tick set in force -- ticks come from
|
||||
// `layout()` and these stores do not recompute them -- so a y the axis now *shows* still
|
||||
// snaps onto the old set. 550 lies in [500, 600] and the stale set ends at 100, so the
|
||||
// snap pulls it clear out of the range the axis is drawing; letting `layout()` refresh the
|
||||
// set is what makes the same y land where the axis is labelled.
|
||||
const std::vector<plot::Tick> previous = p.y_axis(ay).ticks;
|
||||
const f64 stale = previous.back().value;
|
||||
PLOT_CHECK(stale < 500.0); // the old set has nothing to offer near 550
|
||||
p.set_y_range(ay, {500.0, 600.0});
|
||||
p.set_cursor_h_snap(ruler, plot::HRulerSnap::NearestTick);
|
||||
p.set_cursor_y(ruler, 550.0);
|
||||
PLOT_CHECK_NEAR(p.cursor(ruler).y, stale, 1e-12); // the stale set, not the axis' range
|
||||
p.layout();
|
||||
p.set_cursor_y(ruler, 550.0);
|
||||
PLOT_CHECK(p.cursor(ruler).y >= 500.0 && p.cursor(ruler).y <= 600.0);
|
||||
PLOT_CHECK(tick_at(p.y_axis(ay).ticks, p.cursor(ruler).y));
|
||||
}
|
||||
|
||||
PLOT_TEST(auto_fit_log_axis_stays_positive_and_ordered) {
|
||||
// All-negative trace bounds: nothing to show on a log axis, so the fit is
|
||||
// refused and the caller's (positive) range survives untouched.
|
||||
|
||||
Reference in new issue
Block a user