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:
Martino Ferrari committed 2026-09-28 15:07:39 +02:00
1 parent 8f1e60a105
commit 770da4c2b2
9 files changed
+415 -175

No files matched your search

+37 -20
View File
@@ -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