feat(render): independent major and minor grid classes tied to the tick set
This commit is contained in:
1 parent
69495a3799
commit
eaae07d8e0
7 files changed
+635
-16
No files matched your search
@@ -56,6 +56,41 @@ bool has_horizontal(const plot::Grid& g, f64 y, bool major) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/// The same question in *screen* space: the renderer places a line by mapping its
|
||||
/// `x0`/`y0` through the axis' transform, so this is the coincidence the pixels show.
|
||||
bool has_vertical_screen(const plot::Grid& g, const plot::Transform& tx, f64 sx, bool major) {
|
||||
for (const plot::GridLine& l : g.lines()) {
|
||||
if (l.x0 == l.x1 && l.major == major && tx.to_screen_x(l.x0) == sx) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool has_horizontal_screen(const plot::Grid& g, const plot::Transform& ty, f64 sy, bool major) {
|
||||
for (const plot::GridLine& l : g.lines()) {
|
||||
if (l.y0 == l.y1 && l.major == major && ty.to_screen_y(l.y0) == sy) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// How many of `g`'s lines carry one class: `vertical` picks the direction, `major`
|
||||
/// the class.
|
||||
u32 count_lines(const plot::Grid& g, bool vertical, bool major) {
|
||||
u32 n = 0;
|
||||
for (const plot::GridLine& l : g.lines()) {
|
||||
if (((l.x0 == l.x1) == vertical) && l.major == major) ++n;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/// Whether `ticks` holds `value` -- the "and every tick has a line" half of the
|
||||
/// coincidence check, asked of the list the grid was built from.
|
||||
bool tick_at(const std::vector<plot::Tick>& ticks, f64 value) {
|
||||
for (const plot::Tick& t : ticks) {
|
||||
if (t.value == value) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PLOT_TEST(layout_without_text_backend_uses_default_label_areas) {
|
||||
@@ -574,6 +609,180 @@ PLOT_TEST(grid_lines_are_tick_aligned_data_space_segments) {
|
||||
PLOT_CHECK(minor > 0); // minor grid is generated too
|
||||
}
|
||||
|
||||
/// The grid's positions have one source -- the tick set -- and this pins that, in
|
||||
/// both directions and in both spaces: a line with no tick and a tick with no line
|
||||
/// are each a defect, and one direction alone is satisfied by a grid that drops
|
||||
/// ticks or by one that invents them. A labelled major tick and a major grid line
|
||||
/// therefore coincide *by construction* (spec §11), not by two derivations that
|
||||
/// happen to agree on the ranges this case tries.
|
||||
///
|
||||
/// Screen space is checked beside data space because the renderer places a line by
|
||||
/// mapping its endpoints through the axis' transform (`Renderer::draw_grid`), so
|
||||
/// screen coincidence is what the pixels show. A log axis is among the ranges: that
|
||||
/// transform is the one that is not the identity.
|
||||
PLOT_TEST(major_grid_lines_coincide_with_major_ticks) {
|
||||
/// Both directions, both spaces, one line per tick -- for one laid-out plot.
|
||||
const auto check_pair = [](const plot::Plot& p, u32 ax, u32 ay, const char* what) {
|
||||
const plot::Grid& g = p.grid();
|
||||
const plot::Transform tx = p.x_transform(ax);
|
||||
const plot::Transform ty = p.y_transform(ay);
|
||||
const std::vector<plot::Tick>& xt = p.x_axis(ax).ticks;
|
||||
const std::vector<plot::Tick>& yt = p.y_axis(ay).ticks;
|
||||
PLOT_CHECK(!xt.empty());
|
||||
PLOT_CHECK(!yt.empty());
|
||||
|
||||
// Exactly one line per tick of the tick set the grid was built from: none
|
||||
// dropped, none invented, and both classes carried through.
|
||||
PLOT_CHECK(count_lines(g, true, true) + count_lines(g, true, false) == xt.size());
|
||||
PLOT_CHECK(count_lines(g, false, true) + count_lines(g, false, false) == yt.size());
|
||||
PLOT_CHECK(count_lines(g, true, false) > 0u); // the range really has minors
|
||||
PLOT_CHECK(count_lines(g, false, false) > 0u);
|
||||
|
||||
// Every tick -> a line of its own class, and never of the other class.
|
||||
for (const plot::Tick& t : xt) {
|
||||
PLOT_CHECK(has_vertical(g, t.value, t.major));
|
||||
PLOT_CHECK(!has_vertical(g, t.value, !t.major));
|
||||
PLOT_CHECK(has_vertical_screen(g, tx, tx.to_screen_x(t.value), t.major));
|
||||
}
|
||||
for (const plot::Tick& t : yt) {
|
||||
PLOT_CHECK(has_horizontal(g, t.value, t.major));
|
||||
PLOT_CHECK(!has_horizontal(g, t.value, !t.major));
|
||||
PLOT_CHECK(has_horizontal_screen(g, ty, ty.to_screen_y(t.value), t.major));
|
||||
}
|
||||
// Every line -> a tick of its own class.
|
||||
for (const plot::GridLine& l : g.lines()) {
|
||||
if (l.x0 == l.x1) {
|
||||
PLOT_CHECK(tick_at(xt, l.x0));
|
||||
} else {
|
||||
PLOT_CHECK(tick_at(yt, l.y0));
|
||||
}
|
||||
}
|
||||
std::printf("[grid] %-22s x %u ticks (%u major) -> %u vertical lines; y %u ticks (%u major)"
|
||||
" -> %u horizontal, %s\n",
|
||||
what, static_cast<unsigned>(xt.size()), count_lines(g, true, true),
|
||||
count_lines(g, true, true) + count_lines(g, true, false),
|
||||
static_cast<unsigned>(yt.size()), count_lines(g, false, true),
|
||||
count_lines(g, false, true) + count_lines(g, false, false),
|
||||
(tx.log_x || ty.log_y) ? "log" : "linear");
|
||||
};
|
||||
|
||||
// Several ranges and axes widths, so the check is not one nice-number step's
|
||||
// accident.
|
||||
struct Scene {
|
||||
const char* what;
|
||||
f64 lo, hi;
|
||||
f32 w, h;
|
||||
};
|
||||
const Scene scenes[] = {
|
||||
{"[0, 10] 800x400", 0.0, 10.0, 800.0f, 400.0f},
|
||||
{"[0, 100] 800x400", 0.0, 100.0, 800.0f, 400.0f},
|
||||
{"[-5, 5] 640x360", -5.0, 5.0, 640.0f, 360.0f},
|
||||
{"[0, 3.3] 200x120", 0.0, 3.3, 200.0f, 120.0f}, // a narrow axes: few ticks
|
||||
};
|
||||
for (const Scene& s : scenes) {
|
||||
plot::Plot p;
|
||||
p.set_viewport({0.0f, 0.0f, s.w, s.h});
|
||||
const u32 ax = p.add_x_axis({"t", {s.lo, s.hi}, false, plot::AxisPlace::Bottom, false});
|
||||
const u32 ay = p.add_y_axis({"v", {s.lo, s.hi}, false, plot::AxisPlace::Left, false});
|
||||
(void)p.layout();
|
||||
check_pair(p, ax, ay, s.what);
|
||||
}
|
||||
|
||||
// A log pair: the same data-space coincidence, and the same once mapped through the
|
||||
// log10 pre-affine -- where an equal data step is *not* an equal screen step, so a
|
||||
// grid built on any second derivation would part company here first.
|
||||
{
|
||||
plot::Plot p;
|
||||
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
||||
const u32 ax = p.add_x_axis({"t", {1.0, 1000.0}, false, plot::AxisPlace::Bottom, true});
|
||||
const u32 ay = p.add_y_axis({"v", {1.0, 100.0}, false, plot::AxisPlace::Left, true});
|
||||
(void)p.layout();
|
||||
PLOT_CHECK(p.x_axis(ax).log);
|
||||
PLOT_CHECK(p.y_axis(ay).log);
|
||||
PLOT_CHECK(p.x_transform(ax).log_x);
|
||||
PLOT_CHECK(p.y_transform(ay).log_y);
|
||||
check_pair(p, ax, ay, "log [1, 1000]");
|
||||
}
|
||||
|
||||
// A tick set with no minors in it -- `generate_ticks(..., include_minors = false)`,
|
||||
// the labelled set a caller draws numbers for -- makes a grid with no minors: the
|
||||
// grid owns no subdivision of its own. Both directions still hold, and the minor
|
||||
// class is empty on *both* sides, which is asserted rather than skipped: an empty
|
||||
// class on one side only is exactly the drift this case exists to catch.
|
||||
{
|
||||
std::vector<plot::Tick> xt;
|
||||
std::vector<plot::Tick> yt;
|
||||
plot::generate_ticks({0.0, 10.0}, 400.0f, 60.0f, xt, false);
|
||||
plot::generate_ticks({0.0, 1.0}, 300.0f, 60.0f, yt, false);
|
||||
PLOT_CHECK(!xt.empty());
|
||||
PLOT_CHECK(!yt.empty());
|
||||
for (const plot::Tick& t : xt) PLOT_CHECK(t.major);
|
||||
for (const plot::Tick& t : yt) PLOT_CHECK(t.major);
|
||||
|
||||
plot::Grid g;
|
||||
g.build(xt, {0.0, 10.0}, yt, {0.0, 1.0});
|
||||
PLOT_CHECK(g.size() == xt.size() + yt.size());
|
||||
for (const plot::Tick& t : xt) {
|
||||
PLOT_CHECK(has_vertical(g, t.value, true));
|
||||
PLOT_CHECK(!has_vertical(g, t.value, false));
|
||||
}
|
||||
for (const plot::Tick& t : yt) {
|
||||
PLOT_CHECK(has_horizontal(g, t.value, true));
|
||||
PLOT_CHECK(!has_horizontal(g, t.value, false));
|
||||
}
|
||||
PLOT_CHECK(count_lines(g, true, false) == 0u);
|
||||
PLOT_CHECK(count_lines(g, false, false) == 0u);
|
||||
std::printf("[grid] majors-only tick set %u x ticks, %u y ticks -> %zu lines, 0 minors\n",
|
||||
static_cast<unsigned>(xt.size()), static_cast<unsigned>(yt.size()), g.size());
|
||||
}
|
||||
|
||||
// Two axis pairs. `Plot::build_grid()` builds the grid from the *primary* pair (x
|
||||
// axis 0, y axis 0) and `Renderer::draw_grid()` maps it with that pair's transforms
|
||||
// -- the comment there says so -- so a second pair's ticks get no lines at all.
|
||||
// Pinned rather than assumed: the coincidence above is with the primary pair's
|
||||
// ticks, and a value only the second axis ticks at has no line, in data space or on
|
||||
// screen. (A trace on the second pair is still drawn in the data area; it simply
|
||||
// has no grid of its own.)
|
||||
{
|
||||
plot::Plot p;
|
||||
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
||||
const u32 ax0 = p.add_x_axis({"a", {0.0, 100.0}, false, plot::AxisPlace::Bottom, false});
|
||||
const u32 ax1 = p.add_x_axis({"b", {0.0, 1.0}, false, plot::AxisPlace::Top, false});
|
||||
const u32 ay0 = p.add_y_axis({"v", {0.0, 100.0}, false, plot::AxisPlace::Left, false});
|
||||
const u32 ay1 = p.add_y_axis({"w", {0.0, 1.0}, false, plot::AxisPlace::Right, false});
|
||||
(void)p.layout();
|
||||
check_pair(p, ax0, ay0, "primary pair only");
|
||||
|
||||
PLOT_CHECK(p.x_axis_count() == 2u);
|
||||
PLOT_CHECK(p.y_axis_count() == 2u);
|
||||
const plot::Grid& g = p.grid();
|
||||
const plot::Transform tx1 = p.x_transform(ax1);
|
||||
const plot::Transform ty1 = p.y_transform(ay1);
|
||||
u32 x_only_secondary = 0u;
|
||||
u32 y_only_secondary = 0u;
|
||||
for (const plot::Tick& t : p.x_axis(ax1).ticks) {
|
||||
if (tick_at(p.x_axis(ax0).ticks, t.value)) continue; // 0 is on both ranges
|
||||
++x_only_secondary;
|
||||
PLOT_CHECK(!has_vertical(g, t.value, t.major));
|
||||
PLOT_CHECK(!has_vertical_screen(g, tx1, tx1.to_screen_x(t.value), t.major));
|
||||
}
|
||||
for (const plot::Tick& t : p.y_axis(ay1).ticks) {
|
||||
if (tick_at(p.y_axis(ay0).ticks, t.value)) continue;
|
||||
++y_only_secondary;
|
||||
PLOT_CHECK(!has_horizontal(g, t.value, t.major));
|
||||
PLOT_CHECK(!has_horizontal_screen(g, ty1, ty1.to_screen_y(t.value), t.major));
|
||||
}
|
||||
// The case is not vacuous: the second pair really does tick where the first does
|
||||
// not, so "no line there" is a statement about the build and not about the ranges.
|
||||
PLOT_CHECK(x_only_secondary > 0u);
|
||||
PLOT_CHECK(y_only_secondary > 0u);
|
||||
std::printf("[grid] primary pair only %u secondary x ticks with no line, %u secondary y"
|
||||
" ticks with no line; the grid is the primary pair's %zu lines\n",
|
||||
static_cast<unsigned>(x_only_secondary),
|
||||
static_cast<unsigned>(y_only_secondary), g.size());
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------- legend
|
||||
|
||||
namespace {
|
||||
|
||||
Reference in new issue
Block a user