fix(plot): a figure row's strip shows labels and title independently, and the shared axis is the bottom row's strip
This commit is contained in:
1 parent
8f1e60a105
commit
770da4c2b2
9 files changed
+415
-175
No files matched your search
@@ -1,6 +1,7 @@
|
||||
#include "harness.hpp"
|
||||
|
||||
#include <plot/input/controller.hpp>
|
||||
#include <plot/plot/figure.hpp>
|
||||
#include <plot/plot/plot.hpp>
|
||||
#include <plot/data/array_series.hpp>
|
||||
#include <plot/data/stream_series.hpp>
|
||||
@@ -1306,3 +1307,84 @@ PLOT_TEST(hot_handles_follow_a_gesture_that_moves_the_geometry) {
|
||||
PLOT_CHECK(f.plot.hot_handles().zone == f.controller.region_grab(rid));
|
||||
PLOT_CHECK(f.plot.hot_handles().zone != plot::RegionGrab::Left);
|
||||
}
|
||||
|
||||
/// A `Figure`'s shared x axis is the **bottom row's own**, so the axis zoom is
|
||||
/// reachable there through the one hit test the controller has always used: the press
|
||||
/// that lands in the bottom row's x label strip starts `AxisZoomX` exactly as it does on
|
||||
/// a stand-alone plot, and because the rows share one x link the zoom moves all of them.
|
||||
///
|
||||
/// A figure that put its shared axis *outside* every row would leave this gesture
|
||||
/// unreachable on every row at once: `x_label_area` would be zero-height, `contains()`
|
||||
/// would never be true, and a middle drag would fall through to `DragMode::None`.
|
||||
///
|
||||
/// The other half is what makes that a statement about *which* row: the same drag on an
|
||||
/// interior row -- at the point a label strip would land if that row had one -- is
|
||||
/// deliberately not the gesture. The row reserved no x chrome, so there is no strip for
|
||||
/// the press to be inside, and the zoom the bottom row's strip can start cannot be
|
||||
/// started from a row that has none. If that were not so the shared axis would be one
|
||||
/// strip among several, each of which could zoom the one range they all share.
|
||||
PLOT_TEST(a_middle_drag_on_a_figures_bottom_row_zooms_the_shared_x_axis) {
|
||||
plot::Figure figure(3u);
|
||||
figure.set_viewport({0, 0, 800, 600});
|
||||
for (u32 row = 0; row < 3u; ++row) {
|
||||
plot::Plot& p = figure.plot(row);
|
||||
p.add_x_axis({"t", {0.0, 100.0}, false, plot::AxisPlace::Bottom, false});
|
||||
p.add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
||||
}
|
||||
const u32 group = figure.add_x_link();
|
||||
for (u32 row = 0; row < 3u; ++row) PLOT_CHECK(figure.link_x(row, group));
|
||||
const plot::Range before = figure.x_link_range(group);
|
||||
|
||||
figure.layout();
|
||||
const plot::PlotLayout& bottom = figure.plot(2u).layout();
|
||||
PLOT_CHECK(bottom.x_label_area.h > 0.0f);
|
||||
const f32 strip_x = bottom.axes.x + 0.5f * bottom.axes.w;
|
||||
const f32 strip_y = bottom.x_label_area.y + 0.5f * bottom.x_label_area.h;
|
||||
PLOT_CHECK(bottom.x_label_area.contains(strip_x, strip_y));
|
||||
|
||||
plot::input::Controller c(figure.plot(2u));
|
||||
c.set_viewport(figure.viewport());
|
||||
c.on_pointer({strip_x, strip_y, 0, 0, plot::input::Button::Middle, 0, true, true, false});
|
||||
PLOT_CHECK(c.drag_mode() == plot::input::DragMode::AxisZoomX);
|
||||
// Same sign and magnitude as on a single plot: a positive dy zooms in, 20 px being a
|
||||
// tenth of a decade.
|
||||
c.on_pointer({strip_x, strip_y + 20, 0, 20, plot::input::Button::Middle, 0, true, false, false});
|
||||
c.on_pointer({strip_x, strip_y + 20, 0, 0, plot::input::Button::Middle, 0, false, false, true});
|
||||
PLOT_CHECK(c.drag_mode() == plot::input::DragMode::None);
|
||||
|
||||
// The gesture reached the row's own axis...
|
||||
PLOT_CHECK_NEAR(figure.plot(2u).x_range(0u).span(), 100.0 * std::pow(10.0, -20.0 / 200.0), 1e-6);
|
||||
// ... and, through the link, every other row with it: one x axis, zoomed once.
|
||||
figure.layout();
|
||||
for (u32 row = 0; row < 3u; ++row) {
|
||||
PLOT_CHECK_NEAR(figure.plot(row).x_range(0u).span(), figure.x_link_range(group).span(), 1e-9);
|
||||
PLOT_CHECK_NEAR(figure.plot(row).x_range(0u).span(), 100.0 * std::pow(10.0, -20.0 / 200.0),
|
||||
1e-6);
|
||||
}
|
||||
PLOT_CHECK(figure.x_link_range(group).span() < before.span());
|
||||
|
||||
// An interior row is not the same gesture. A press at the point a label strip *would*
|
||||
// land in -- the band this row's chrome would occupy if the figure gave it one -- is
|
||||
// not inside anything: the row reserved no x chrome, so its `x_label_area` is
|
||||
// zero-height and no press can be inside it. The middle drag therefore falls through
|
||||
// to `DragMode::None`, and because `update_drag()` does nothing in that mode the
|
||||
// group's window -- the one range every row shares -- is untouched by it.
|
||||
const plot::PlotLayout& interior = figure.plot(0u).layout();
|
||||
PLOT_CHECK(interior.x_label_area.h == 0.0f);
|
||||
const f32 interior_x = interior.axes.x + 0.5f * interior.axes.w;
|
||||
const f32 interior_y = figure.layout().rows[0].bottom() - 0.5f * bottom.x_label_area.h;
|
||||
PLOT_CHECK(!interior.x_label_area.contains(interior_x, interior_y));
|
||||
const plot::Range shared_before = figure.x_link_range(group);
|
||||
|
||||
plot::input::Controller interior_controller(figure.plot(0u));
|
||||
interior_controller.on_pointer(
|
||||
{interior_x, interior_y, 0, 0, plot::input::Button::Middle, 0, true, true, false});
|
||||
PLOT_CHECK(interior_controller.drag_mode() == plot::input::DragMode::None);
|
||||
interior_controller.on_pointer(
|
||||
{interior_x, interior_y + 20, 0, 20, plot::input::Button::Middle, 0, true, false, false});
|
||||
interior_controller.on_pointer(
|
||||
{interior_x, interior_y + 20, 0, 0, plot::input::Button::Middle, 0, false, false, true});
|
||||
PLOT_CHECK(interior_controller.drag_mode() == plot::input::DragMode::None);
|
||||
PLOT_CHECK_NEAR(figure.plot(0u).x_range(0u).span(), shared_before.span(), 1e-12);
|
||||
PLOT_CHECK_NEAR(figure.x_link_range(group).span(), shared_before.span(), 1e-12);
|
||||
}
|
||||
+37
-20
@@ -22,8 +22,9 @@ constexpr plot::f32 kH = 480.0f;
|
||||
plot::Rect whole_figure() { return plot::Rect{0.0f, 0.0f, kW, kH}; }
|
||||
|
||||
/// A figure whose every row has a y axis and an x axis, so a row is a plot in the
|
||||
/// shape the layout tests care about: it loses width to a y strip and height to
|
||||
/// nothing else (the x strip belongs to the figure under `labels_bottom_only`).
|
||||
/// shape the layout tests care about: it loses width to a y strip and, on the bottom
|
||||
/// row alone, height to the shared x strip -- which lives inside that row rather than
|
||||
/// in a band of its own.
|
||||
plot::Figure figure_with_axes(plot::u32 rows) {
|
||||
plot::Figure figure(rows);
|
||||
figure.set_viewport(whole_figure());
|
||||
@@ -65,21 +66,32 @@ PLOT_TEST(row_rects_tile_the_figure_without_overlap) {
|
||||
PLOT_CHECK(row.h > 0.0f);
|
||||
}
|
||||
// Top to bottom, in row order, touching: each row starts where the one above it
|
||||
// ended, and the last one ends on the rows' region's bottom edge (the figure's
|
||||
// height less the shared strip beneath it).
|
||||
// ended, and the last one ends on the figure's bottom edge -- the rows tile the
|
||||
// whole figure, because the shared x axis is the bottom row's own rather than a band
|
||||
// taken off the rows.
|
||||
const plot::FigureLayout layout = figure.layout();
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(rows[0].y), 0.0, 1e-6);
|
||||
for (std::size_t i = 1; i < rows.size(); ++i) {
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(rows[i].y), static_cast<plot::f64>(rows[i - 1].bottom()),
|
||||
1e-6);
|
||||
}
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(rows.back().bottom()),
|
||||
static_cast<plot::f64>(layout.x_axis_strip.y), 1e-6);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(layout.x_axis_strip.bottom()), static_cast<plot::f64>(kH),
|
||||
1e-6);
|
||||
// ... and the strip shares the figure's width with the rows above it.
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(layout.x_axis_strip.w), static_cast<plot::f64>(kW), 1e-6);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(rows.back().bottom()), static_cast<plot::f64>(kH), 1e-6);
|
||||
// The shared strip *is* the bottom row's own x label area: inside that row, below the
|
||||
// row's axes and above the row's title strip, and as wide as its axes -- which is what
|
||||
// puts it where the controller can hit-test it and where the renderer stages the one
|
||||
// set of labels.
|
||||
const plot::PlotLayout& bottom = figure.plot(2u).layout();
|
||||
PLOT_CHECK(layout.x_axis_strip.h > 0.0f);
|
||||
PLOT_CHECK(layout.x_axis_strip.y >= layout.rows.back().y);
|
||||
PLOT_CHECK(layout.x_axis_strip.bottom() <= layout.rows.back().bottom() + 1e-6f);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(layout.x_axis_strip.h),
|
||||
static_cast<plot::f64>(bottom.x_label_area.h), 1e-6);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(layout.x_axis_strip.x),
|
||||
static_cast<plot::f64>(bottom.x_label_area.x), 1e-6);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(layout.x_axis_strip.y),
|
||||
static_cast<plot::f64>(bottom.x_label_area.y), 1e-6);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(layout.x_axis_strip.w),
|
||||
static_cast<plot::f64>(bottom.x_label_area.w), 1e-6);
|
||||
|
||||
for (std::size_t i = 0; i < rows.size(); ++i) {
|
||||
for (std::size_t j = i + 1; j < rows.size(); ++j) {
|
||||
@@ -95,8 +107,11 @@ PLOT_TEST(row_rects_tile_the_figure_without_overlap) {
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(spaced[1].y - spaced[0].bottom()), 10.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(spaced[2].y - spaced[1].bottom()), 10.0, 1e-6);
|
||||
PLOT_CHECK(spaced[0].h < rows[0].h); // the gaps came out of the rows
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(spaced.back().bottom()),
|
||||
static_cast<plot::f64>(figure.layout().x_axis_strip.y), 1e-6);
|
||||
// The last spaced row still ends on the figure's bottom edge -- the gaps came out of
|
||||
// the rows' share of the figure, not out of the figure -- and so does the strip.
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(spaced.back().bottom()), static_cast<plot::f64>(kH), 1e-6);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(figure.layout().x_axis_strip.bottom()),
|
||||
static_cast<plot::f64>(kH), 1e-6);
|
||||
|
||||
// A figure with no rows has no geometry -- and no row to reach for while computing
|
||||
// it, which is the case that would read past the end of its own plot list.
|
||||
@@ -110,8 +125,9 @@ PLOT_TEST(row_rects_tile_the_figure_without_overlap) {
|
||||
|
||||
/// Weights change how the figure's height is divided -- a taller main plot, shorter
|
||||
/// rows under it -- and the rows still account for all of it: the sum of the heights
|
||||
/// plus the gaps is the figure less the shared strip, so no weight can leave a band
|
||||
/// unclaimed or push a row past the bottom.
|
||||
/// plus the gaps is the whole figure (the shared strip lives inside the bottom row, so
|
||||
/// it costs the division nothing), so no weight can leave a band unclaimed or push a
|
||||
/// row past the bottom.
|
||||
PLOT_TEST(row_weights_change_relative_heights_and_sum_to_the_viewport) {
|
||||
plot::Figure figure = figure_with_axes(3u);
|
||||
const std::vector<plot::Rect> equal = rows_of(figure);
|
||||
@@ -145,7 +161,7 @@ PLOT_TEST(row_weights_change_relative_heights_and_sum_to_the_viewport) {
|
||||
|
||||
plot::f32 total = 0.0f;
|
||||
for (const plot::Rect& row : rows) total += row.h;
|
||||
total += layout.row_gap * 2.0f + layout.x_axis_strip.h;
|
||||
total += layout.row_gap * 2.0f;
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(total), static_cast<plot::f64>(kH), 1e-3);
|
||||
|
||||
// Any positive scale is the same division: normalised, not absolute.
|
||||
@@ -206,8 +222,8 @@ PLOT_TEST(degenerate_row_weights_are_rejected) {
|
||||
|
||||
/// Every row keeps a positive height, whatever the figure's size and whatever the
|
||||
/// weights say -- a row that is not there is a plot that is not there, silently. The
|
||||
/// shared strip yields to the rows for the same reason: chrome may shrink a figure,
|
||||
/// never erase one.
|
||||
/// shared strip cannot take a row's height for the same reason: it is the bottom row's
|
||||
/// own, so chrome may shrink one row's *axes*, never erase a row.
|
||||
PLOT_TEST(every_row_keeps_a_positive_height) {
|
||||
// Five rows in a figure shorter than the strip one row would ask for.
|
||||
plot::Figure tiny(5u);
|
||||
@@ -220,9 +236,10 @@ PLOT_TEST(every_row_keeps_a_positive_height) {
|
||||
PLOT_CHECK(row.h > 0.0f);
|
||||
total += row.h;
|
||||
}
|
||||
// The rows took what they needed from the gap: a gap that does not fit is dropped,
|
||||
// so what is left of the figure is the rows' and the strip's, not the gap's.
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(total + layout.x_axis_strip.h), 12.0, 1e-3);
|
||||
// The rows took the whole figure: a gap that does not fit is dropped, and the shared
|
||||
// strip is inside the bottom row rather than a band beside it, so it costs the
|
||||
// division nothing.
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(total), 12.0, 1e-3);
|
||||
|
||||
// A wildly unbalanced weight set still cannot squeeze a row out. The tiny weights
|
||||
// here are small enough that a row's *share* of the leftover underflows to zero
|
||||
|
||||
+159
-38
@@ -3530,11 +3530,15 @@ PLOT_TEST(two_rows_render_distinct_content) {
|
||||
predicate_pixels(frame, kW, row1, magenta_pixel));
|
||||
|
||||
// Each trace is drawn inside its row's *axes*, not merely somewhere in the row: the
|
||||
// row's y strip is the other half of the rect and holds no data.
|
||||
// row's y strip is the other half of the rect and holds no data. The top row is
|
||||
// interior and reserves no x chrome; the bottom row reserves its own label and title
|
||||
// strips, because the figure's one shared x axis is that row's.
|
||||
const plot::PlotLayout& layout0 = figure.plot(0u).layout();
|
||||
const plot::PlotLayout& layout1 = figure.plot(1u).layout();
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(layout0.axes.h), static_cast<plot::f64>(row0.h), 1e-3);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(layout1.axes.h), static_cast<plot::f64>(row1.h), 1e-3);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(layout1.axes.bottom() + layout1.x_label_area.h +
|
||||
layout1.x_title_height),
|
||||
static_cast<plot::f64>(row1.bottom()), 1e-3);
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, layout0.axes, trace_pixel) > 0u);
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, layout1.axes, magenta_pixel) > 0u);
|
||||
// The rows are stacked, not overlapped: the second starts below the first.
|
||||
@@ -3544,12 +3548,13 @@ PLOT_TEST(two_rows_render_distinct_content) {
|
||||
}
|
||||
|
||||
/// The figure draws *one* x axis. With labels on the bottom only -- the default --
|
||||
/// the rows reserve no x label strip of their own and their labels appear once, in
|
||||
/// the strip the figure measured under the last row; the interior row's labels
|
||||
/// are not in the frame at all. Switching the figure to label every row puts them in
|
||||
/// every row, so the difference between the two frames is exactly the ink the shared
|
||||
/// axis exists to prevent -- which is what makes the count below a statement rather
|
||||
/// than a scan of somewhere nothing is ever drawn.
|
||||
/// the rows above the bottom reserve no x chrome and their labels appear once, in the
|
||||
/// **bottom row's own** strip, which is where the figure's one x axis lives; the
|
||||
/// interior rows' labels are not in the frame at all. Switching the figure to label
|
||||
/// every row puts them in every row, and the count below -- of x-label ink inside the
|
||||
/// rows' own strips, so it compares x chrome with x chrome rather than the whole frames,
|
||||
/// whose row heights differ between the two modes -- says which happened: one row's
|
||||
/// worth against three.
|
||||
PLOT_TEST(shared_x_axis_is_drawn_once) {
|
||||
plottest::HeadlessContext ctx(kW, kH);
|
||||
plot::Status st;
|
||||
@@ -3577,25 +3582,40 @@ PLOT_TEST(shared_x_axis_is_drawn_once) {
|
||||
const plot::Color ink = r.style().axis_text;
|
||||
|
||||
// --- labels on the bottom only: one set, and it is the figure's -----------------
|
||||
const std::vector<plot::u8> shared = draw_frame();
|
||||
draw_frame();
|
||||
dump_if_requested(r, "figure_shared_x.ppm");
|
||||
const plot::FigureLayout layout = figure.layout();
|
||||
PLOT_CHECK(layout.rows.size() == 3u);
|
||||
PLOT_CHECK(layout.x_axis_strip.h > 0.0f);
|
||||
// The strip is the figure's, under the last row: the rows end where it starts, and
|
||||
// it ends on the figure's bottom edge.
|
||||
// The strip is the *bottom row's own* label area, inside that row and at the bottom
|
||||
// of it: the rows tile the figure to its bottom edge, and the shared axis is the
|
||||
// last band of the last row -- the rect the controller hit-tests and the renderer
|
||||
// stages the one set of labels in.
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(layout.rows.back().bottom()),
|
||||
static_cast<plot::f64>(layout.x_axis_strip.y), 1e-3);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(layout.x_axis_strip.bottom()),
|
||||
static_cast<plot::f64>(kH), 1e-3);
|
||||
// The shared strip *is* the bottom row's x label area, to the pixel -- the rect the
|
||||
// controller hit-tests and the renderer stages the one set of labels in -- and it
|
||||
// sits inside that row, above the row's own x title strip.
|
||||
const plot::PlotLayout& bottom_row_layout = figure.plot(2u).layout();
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(layout.x_axis_strip.x),
|
||||
static_cast<plot::f64>(bottom_row_layout.x_label_area.x), 1e-3);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(layout.x_axis_strip.y),
|
||||
static_cast<plot::f64>(bottom_row_layout.x_label_area.y), 1e-3);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(layout.x_axis_strip.w),
|
||||
static_cast<plot::f64>(bottom_row_layout.x_label_area.w), 1e-3);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(layout.x_axis_strip.h),
|
||||
static_cast<plot::f64>(bottom_row_layout.x_label_area.h), 1e-3);
|
||||
PLOT_CHECK(layout.x_axis_strip.bottom() <= layout.rows.back().bottom() + 1e-3f);
|
||||
const plot::u32 strip_ink = scan_rect_ink(ctx, layout.x_axis_strip, ink).pixels;
|
||||
PLOT_CHECK(strip_ink > 0u);
|
||||
|
||||
// Every row gave its x label strip away: a row's axes reaches its rect's bottom edge
|
||||
// and the row has no band inside itself for a second set of labels to be drawn in.
|
||||
for (plot::u32 row = 0; row < 3u; ++row) {
|
||||
// Every row *above* the bottom gave its x label strip away: such a row's axes
|
||||
// reaches its rect's bottom edge and the row has no band inside itself for a second
|
||||
// set of labels to be drawn in. The bottom row is the exception -- it keeps its own
|
||||
// strip, which is where the figure's one x axis is -- so its strip is not zero.
|
||||
for (plot::u32 row = 0; row + 1u < 3u; ++row) {
|
||||
const plot::PlotLayout& row_layout = figure.plot(row).layout();
|
||||
PLOT_CHECK(!figure.plot(row).x_label_strip());
|
||||
PLOT_CHECK(!plot::x_strip_has_labels(figure.plot(row).x_strip_mode()));
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(row_layout.x_axis_height), 0.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(row_layout.x_label_area.h), 0.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(row_layout.axes.bottom()),
|
||||
@@ -3617,31 +3637,42 @@ PLOT_TEST(shared_x_axis_is_drawn_once) {
|
||||
PLOT_CHECK(band.h > 0.0f);
|
||||
PLOT_CHECK(scan_rect_ink(ctx, band, ink).pixels == 0u);
|
||||
}
|
||||
const plot::u32 shared_ink = frame_text_ink(shared, kW, kH, ink);
|
||||
|
||||
// --- every row labels itself: the same labels, in three places -----------------
|
||||
figure.set_x_labels_only_on_bottom(false);
|
||||
const std::vector<plot::u8> per_row = draw_frame();
|
||||
draw_frame();
|
||||
const plot::FigureLayout per_row_layout = figure.layout();
|
||||
PLOT_CHECK(per_row_layout.x_axis_strip.h == 0.0f); // no shared strip any more
|
||||
// With every row labelling itself there is no shared axis to speak of: the strip
|
||||
// reads back the bottom row's own label area again, as one of three rather than as
|
||||
// the figure's one -- the figure added no band outside its rows either way.
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(per_row_layout.x_axis_strip.h),
|
||||
static_cast<plot::f64>(figure.plot(2u).layout().x_label_area.h), 1e-3);
|
||||
PLOT_CHECK(per_row_layout.rows.size() == 3u);
|
||||
for (plot::u32 row = 0; row < 3u; ++row) {
|
||||
const plot::PlotLayout& own = figure.plot(row).layout();
|
||||
PLOT_CHECK(figure.plot(row).x_label_strip());
|
||||
PLOT_CHECK(plot::x_strip_has_labels(figure.plot(row).x_strip_mode()));
|
||||
PLOT_CHECK(own.x_label_area.h > 0.0f);
|
||||
// A row's own labels are in its own strip, inside its own rect.
|
||||
PLOT_CHECK(own.x_label_area.y >= per_row_layout.rows[row].y);
|
||||
PLOT_CHECK(own.x_label_area.bottom() <= per_row_layout.rows[row].bottom() + 1e-3f);
|
||||
PLOT_CHECK(scan_rect_ink(ctx, own.x_label_area, ink).pixels > 0u);
|
||||
}
|
||||
const plot::u32 per_row_ink = frame_text_ink(per_row, kW, kH, ink);
|
||||
|
||||
// The two interior row's labels are the difference -- they are what the shared axis
|
||||
// replaced, and in the default frame they are simply not there. The y labels, the
|
||||
// chrome and the traces are in both frames, and the shared strip's labels are one
|
||||
// row's worth, so a frame that drew the x labels twice per row cannot land here.
|
||||
PLOT_CHECK(per_row_ink > shared_ink);
|
||||
PLOT_CHECK(per_row_ink - shared_ink >= strip_ink / 2u);
|
||||
// The two interior rows' labels are the difference -- they are what the shared axis
|
||||
// replaced, and in the default frame they are simply not there. Counted inside the
|
||||
// rows' own x label strips, which is the only like-for-like count here: the two frames
|
||||
// do not give the rows the same heights, because a shared frame lets its interior rows
|
||||
// keep the band the per-row frame spends on their x strips (the shared axis costs them
|
||||
// nothing), so a whole-frame text count compares y chrome against y chrome and decides
|
||||
// nothing. The bottom row's strip holds one row's worth of x labels in both frames --
|
||||
// the same rect carrying the same labels -- while the interior rows' strips hold none
|
||||
// in the shared frame and a row's worth each in the per-row one, so the per-row strips
|
||||
// together hold three times the shared one's. A frame that drew the interior rows'
|
||||
// labels but only one set of them, or that spent no strip on the bottom row, cannot
|
||||
// land here.
|
||||
plot::u32 per_row_x_ink = 0u;
|
||||
for (plot::u32 row = 0; row < 3u; ++row) {
|
||||
per_row_x_ink += scan_rect_ink(ctx, figure.plot(row).layout().x_label_area, ink).pixels;
|
||||
}
|
||||
PLOT_CHECK(per_row_x_ink >= 2u * strip_ink);
|
||||
|
||||
ctx.destroy();
|
||||
}
|
||||
@@ -3694,20 +3725,24 @@ PLOT_TEST(a_row_resize_recomputes_rather_than_rescaling_a_stale_layout) {
|
||||
const plot::Rect& row1 = layout.rows[1];
|
||||
|
||||
// The rows are the *new* figure's: twice the first frame's height, to the pixel,
|
||||
// and the second row reaches the shared strip rather than stopping where the short
|
||||
// frame's strip was.
|
||||
// and the second row reaches the figure's bottom edge rather than stopping where
|
||||
// the short frame's shared strip was.
|
||||
PLOT_CHECK(row1.h > 1.5f * short_layout.rows[1].h);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(row1.bottom()),
|
||||
static_cast<plot::f64>(layout.x_axis_strip.y), 1e-3);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(layout.x_axis_strip.bottom()),
|
||||
static_cast<plot::f64>(kH), 1e-3);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(row1.bottom()), static_cast<plot::f64>(kH), 1e-3);
|
||||
PLOT_CHECK(row1.y > short_layout.rows[1].y);
|
||||
|
||||
// The transforms the frame drew with are the new rects': the second row's axes
|
||||
// spans its rect exactly, and its trace is in there -- not at the old coordinates.
|
||||
// The transforms the frame drew with are the new rects': the second row's own chrome
|
||||
// stacks inside its rect -- the axes, then the shared-axis label strip, then the
|
||||
// row's title strip, the three ending on the row's bottom edge -- and its trace is in
|
||||
// the axes, not at the old coordinates.
|
||||
const plot::PlotLayout& row1_layout = figure.plot(1u).layout();
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(row1_layout.axes.y), static_cast<plot::f64>(row1.y), 1e-3);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(layout.x_axis_strip.y),
|
||||
static_cast<plot::f64>(row1_layout.x_label_area.y), 1e-3);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(row1_layout.axes.bottom()),
|
||||
static_cast<plot::f64>(row1_layout.x_label_area.y), 1e-3);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(row1_layout.x_label_area.bottom() +
|
||||
row1_layout.x_title_height),
|
||||
static_cast<plot::f64>(row1.bottom()), 1e-3);
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, row1_layout.axes, magenta_pixel) > 0u);
|
||||
// Nothing of the second row's trace is above it, where the short frame's row 1 was:
|
||||
@@ -3724,3 +3759,89 @@ PLOT_TEST(a_row_resize_recomputes_rather_than_rescaling_a_stale_layout) {
|
||||
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
/// A `Figure`'s x title is drawn where a figure's x axis is: the bottom row's own
|
||||
/// title strip, under that row's own labels. An interior row reserves no x chrome at
|
||||
/// all -- its `axes` reaches its rect's bottom edge, because the figure has one x axis
|
||||
/// and the bottom row owns it -- so a title on an interior row has nowhere to go and
|
||||
/// is not drawn. The second half of the frame pair is what makes that a statement:
|
||||
/// told to label every row, the same figure gives every row its own strip and the
|
||||
/// interior rows' titles appear, so the ink the default frame withholds is exactly the
|
||||
/// ink the interior rows' x chrome would have spent.
|
||||
PLOT_TEST(figure_x_title_is_drawn_on_the_bottom_row_only) {
|
||||
plottest::HeadlessContext ctx(kW, kH);
|
||||
plot::Status st;
|
||||
PLOT_CHECK(ctx.create(&st));
|
||||
PLOT_CHECK(plot::gl::load(nullptr));
|
||||
plot::Renderer r;
|
||||
PLOT_CHECK(r.init(&st));
|
||||
|
||||
const std::vector<plot::f32> samples = wave(640u, 0.0f);
|
||||
plot::data::ArraySeries series(samples, 0.0, 1.0);
|
||||
|
||||
plot::Figure figure(3u);
|
||||
figure.set_viewport({0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
|
||||
for (plot::u32 row = 0; row < 3u; ++row) {
|
||||
plot::Plot& p = figure.plot(row);
|
||||
// Every row gets a title, and a title of its own, so which strip the ink is in says
|
||||
// which row drew it.
|
||||
const plot::u32 ax =
|
||||
p.add_x_axis({row + 1u == 3u ? "bottom time" : "row time",
|
||||
series.x_bounds(), false, plot::AxisPlace::Bottom, false});
|
||||
const plot::u32 ay = p.add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
||||
p.add_trace({&series, kRowBlue, 1.5f, ax, ay});
|
||||
}
|
||||
|
||||
const auto draw_frame = [&] {
|
||||
r.begin_frame(kW, kH, {0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
|
||||
r.draw(figure);
|
||||
r.end_frame();
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
return read_frame(ctx);
|
||||
};
|
||||
|
||||
const plot::Color ink = r.style().axis_text;
|
||||
const std::vector<plot::u8> bottom_only = draw_frame();
|
||||
dump_if_requested(r, "figure_x_title.ppm");
|
||||
const plot::FigureLayout layout = figure.layout();
|
||||
|
||||
// The bottom row keeps a real title strip, inside its own rect, and its title is in
|
||||
// it -- so the title feature still works on a figure.
|
||||
const plot::PlotLayout& above = figure.plot(2u).layout();
|
||||
PLOT_CHECK(above.x_title_area.h > 0.0f);
|
||||
PLOT_CHECK(above.x_title_area.y >= above.x_label_area.bottom() - 1e-3f);
|
||||
PLOT_CHECK(above.x_title_area.bottom() <= layout.rows[2].bottom() + 1e-3f);
|
||||
PLOT_CHECK(scan_rect_ink(ctx, above.x_title_area, ink).pixels > 0u);
|
||||
// The labels are above it and the data above them: the three strips stack, they do
|
||||
// not overlap.
|
||||
PLOT_CHECK(above.axes.bottom() <= above.x_label_area.y + 1e-3f);
|
||||
PLOT_CHECK(above.x_label_area.bottom() <= above.x_title_area.y + 1e-3f);
|
||||
|
||||
// An interior row reserves none of that: no label strip, no title strip, and its
|
||||
// axes takes the whole rect.
|
||||
for (plot::u32 row = 0; row + 1u < 3u; ++row) {
|
||||
const plot::PlotLayout& interior = figure.plot(row).layout();
|
||||
PLOT_CHECK(interior.x_label_area.h == 0.0f);
|
||||
PLOT_CHECK(interior.x_title_area.h == 0.0f);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(interior.axes.bottom()),
|
||||
static_cast<plot::f64>(layout.rows[row].bottom()), 1e-3);
|
||||
}
|
||||
const plot::u32 withheld = frame_text_ink(bottom_only, kW, kH, ink);
|
||||
|
||||
// Told to label every row, the same figure puts every row's x chrome back -- label
|
||||
// strip and title strip, one per row, each inside its own rect -- and the interior
|
||||
// rows' titles are drawn where the default frame drew nothing.
|
||||
figure.set_x_labels_only_on_bottom(false);
|
||||
const std::vector<plot::u8> every_row = draw_frame();
|
||||
const plot::FigureLayout per_row = figure.layout();
|
||||
for (plot::u32 row = 0; row < 3u; ++row) {
|
||||
const plot::PlotLayout& own = figure.plot(row).layout();
|
||||
PLOT_CHECK(own.x_title_area.h > 0.0f);
|
||||
PLOT_CHECK(own.x_title_area.bottom() <= per_row.rows[row].bottom() + 1e-3f);
|
||||
PLOT_CHECK(scan_rect_ink(ctx, own.x_title_area, ink).pixels > 0u);
|
||||
PLOT_CHECK(scan_rect_ink(ctx, own.x_label_area, ink).pixels > 0u);
|
||||
}
|
||||
PLOT_CHECK(frame_text_ink(every_row, kW, kH, ink) > withheld);
|
||||
|
||||
ctx.destroy();
|
||||
}
|
||||
Reference in new issue
Block a user