test(plot): pin the crosshair's lookup, readout, placement, sync and clip
This commit is contained in:
1 parent
fd0c471430
commit
8e27e22ebe
6 files changed
+798
-17
No files matched your search
@@ -10,6 +10,7 @@
|
||||
#include <plot/data/array_series.hpp>
|
||||
#include <plot/data/stream_series.hpp>
|
||||
#include <plot/plot/figure.hpp>
|
||||
#include <plot/render/embedded_mono_font.hpp>
|
||||
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
@@ -572,3 +573,120 @@ PLOT_TEST(a_following_group_advances_all_rows_together) {
|
||||
PLOT_CHECK_NEAR(figure.x_link_range(group).max, frozen.max, 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------- the crosshair (spec §15)
|
||||
|
||||
namespace {
|
||||
|
||||
/// The figure the crosshair cases hover: three rows at deliberately *different* sample rates on
|
||||
/// the one shared x, plus a fourth whose data does not reach the hovered x at all. Rows that
|
||||
/// sample at different rates are the point -- one x, three different interpolations -- and the
|
||||
/// last row is the case a readout must not invent a value for.
|
||||
plot::Figure crosshair_figure() {
|
||||
plot::Figure figure(4u);
|
||||
figure.set_viewport(whole_figure());
|
||||
static plot::data::ArraySeries fast(std::vector<plot::f32>{0.0f, 10.0f, 20.0f, 30.0f, 40.0f,
|
||||
50.0f, 60.0f, 70.0f, 80.0f, 90.0f},
|
||||
0.0, 1.0);
|
||||
static plot::data::ArraySeries slow(std::vector<plot::f32>{0.0f, 100.0f, 200.0f, 300.0f,
|
||||
400.0f},
|
||||
0.0, 2.0);
|
||||
static plot::data::ArraySeries elsewhere(std::vector<plot::f32>{5.0f, 6.0f, 7.0f}, 100.0, 1.0);
|
||||
static plot::render::EmbeddedMonoFont font;
|
||||
const plot::data::Series* const series[4] = {&fast, &slow, nullptr, &elsewhere};
|
||||
const char* const names[4] = {"fast", "slow", "pending", "elsewhere"};
|
||||
for (plot::u32 row = 0; row < 4u; ++row) {
|
||||
plot::Plot& p = figure.plot(row);
|
||||
p.set_text_backend(&font);
|
||||
p.add_x_axis({"t", {0.0, 99.0}, false, plot::AxisPlace::Bottom, false});
|
||||
p.add_y_axis({"v", {0.0, 400.0}, false, plot::AxisPlace::Left, false});
|
||||
p.add_trace({series[row], plot::Color::hex(0x40C0FF), 1.5f, 0u, 0u, names[row]});
|
||||
}
|
||||
return figure;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/// Spec §15, the sync: `Figure::set_crosshair()` applies **one** data x to every row, so one
|
||||
/// hover is one instant and N readouts rather than N independent ones. Rows at different sample
|
||||
/// rates therefore report *their own* interpolations of the same x -- the shared axis is x, and
|
||||
/// each row's series is read on its own grid -- while a row whose series does not reach that x
|
||||
/// reports no value at all instead of a fabricated one.
|
||||
PLOT_TEST(figure_crosshair_applies_one_x_to_every_row) {
|
||||
plot::Figure figure = crosshair_figure();
|
||||
figure.layout();
|
||||
|
||||
figure.set_crosshair(3.0, true);
|
||||
for (plot::u32 row = 0; row < 4u; ++row) {
|
||||
PLOT_CHECK(figure.plot(row).crosshair().active);
|
||||
PLOT_CHECK_NEAR(figure.plot(row).crosshair().x, 3.0, 1e-12);
|
||||
}
|
||||
// Laid out per row, so each row's readout is the one it would have on its own -- and the rows
|
||||
// are their own plots' layouts, which is where a renderer reads them.
|
||||
figure.layout();
|
||||
|
||||
// Row 0 samples every unit: x = 3 is sample 3, y = 30.
|
||||
const plot::CrosshairLayout& first = figure.plot(0u).layout().crosshair;
|
||||
PLOT_CHECK(first.active);
|
||||
PLOT_CHECK(first.rows.size() == 1u);
|
||||
PLOT_CHECK(first.rows[0].valid);
|
||||
PLOT_CHECK(first.rows[0].label == "fast");
|
||||
PLOT_CHECK_NEAR(first.rows[0].y, 30.0, 1e-9);
|
||||
|
||||
// Row 1 samples every *two* units: x = 3 is halfway between sample 1 (y = 100) and sample 2
|
||||
// (y = 200), so it reports 150 -- a different number for the same x, which is what makes this
|
||||
// case about the rows rather than about one series read four times.
|
||||
const plot::CrosshairLayout& second = figure.plot(1u).layout().crosshair;
|
||||
PLOT_CHECK(second.rows.size() == 1u);
|
||||
PLOT_CHECK(second.rows[0].valid);
|
||||
PLOT_CHECK(second.rows[0].label == "slow");
|
||||
PLOT_CHECK_NEAR(second.rows[0].y, 150.0, 1e-9);
|
||||
PLOT_CHECK(second.rows[0].value != first.rows[0].value);
|
||||
|
||||
// Row 2's trace has no series yet: no row at all -- it is not a signal under the pointer.
|
||||
const plot::CrosshairLayout& third = figure.plot(2u).layout().crosshair;
|
||||
PLOT_CHECK(third.active);
|
||||
PLOT_CHECK(third.rows.empty());
|
||||
PLOT_CHECK(third.panel.w == 0.0f);
|
||||
|
||||
// Row 3's series starts at x = 100: it is a visible signal with nothing at x = 3, so it is
|
||||
// listed with **no value** rather than a value clamped to the end of its window.
|
||||
const plot::CrosshairLayout& fourth = figure.plot(3u).layout().crosshair;
|
||||
PLOT_CHECK(fourth.rows.size() == 1u);
|
||||
PLOT_CHECK(fourth.rows[0].trace == 0u);
|
||||
PLOT_CHECK(fourth.rows[0].label == "elsewhere");
|
||||
PLOT_CHECK(!fourth.rows[0].valid);
|
||||
PLOT_CHECK(fourth.rows[0].value.empty());
|
||||
// ... and it is still listed, so the row's presence does not depend on having a value.
|
||||
PLOT_CHECK(fourth.rows[0].listed);
|
||||
PLOT_CHECK(fourth.rows[0].value_box.w == 0.0f);
|
||||
|
||||
// The crosshair is a data coordinate, so the x is the *same* whether or not the rows sample
|
||||
// there -- moving it moves every row, and the rows that have data report their own value at
|
||||
// the new x.
|
||||
figure.set_crosshair(1.0, true);
|
||||
figure.layout();
|
||||
for (plot::u32 row = 0; row < 4u; ++row) {
|
||||
PLOT_CHECK_NEAR(figure.plot(row).crosshair().x, 1.0, 1e-12);
|
||||
}
|
||||
PLOT_CHECK_NEAR(figure.plot(0u).layout().crosshair.rows[0].y, 10.0, 1e-9);
|
||||
PLOT_CHECK_NEAR(figure.plot(1u).layout().crosshair.rows[0].y, 50.0, 1e-9); // 0 + 100 * 0.5
|
||||
PLOT_CHECK(!figure.plot(3u).layout().crosshair.rows[0].valid);
|
||||
|
||||
// On *every* row at once, and only the rows free it.
|
||||
figure.clear_crosshair();
|
||||
figure.layout();
|
||||
for (plot::u32 row = 0; row < 4u; ++row) {
|
||||
PLOT_CHECK(!figure.plot(row).crosshair().active);
|
||||
PLOT_CHECK(figure.plot(row).layout().crosshair.rows.empty());
|
||||
}
|
||||
|
||||
// A coordinate that is not a number is refused by every row's `Plot::set_crosshair()`, so a
|
||||
// figure handed one keeps the crosshair it had rather than clearing some rows and not others.
|
||||
figure.set_crosshair(2.0, true);
|
||||
figure.set_crosshair(std::nan(""), true);
|
||||
for (plot::u32 row = 0; row < 4u; ++row) {
|
||||
PLOT_CHECK(figure.plot(row).crosshair().active);
|
||||
PLOT_CHECK_NEAR(figure.plot(row).crosshair().x, 2.0, 1e-12);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user