feat(plot): a figure can keep an empty band to the right of its data
CI / debug (tests + all adapters) (push) Canceled after 0s
CI / coverage (floor + measured table) (push) Canceled after 0s
CI / asan (AddressSanitizer) (push) Canceled after 0s

`layout_.axes` reached the viewport's right edge, so the rightmost x tick
label -- centred on its tick -- had half of itself outside the plot, and
the newest sample's ink landed on the frame. `Plot::set_right_gutter()`
reserves a band there, out of the same `1 - kMinAxesShare` allowance the
label and title strips are served from, so it can no more squeeze `axes`
below the half-viewport floor than they can. `Figure` sets one value on
every row, the left gutter's reason: the rows' axes have to end on a
common column as well as start on one.

Nothing is drawn in the band, so unlike the strips it is a want and never
a measurement: no measuring pass, and it cannot feed the layout's fixed
point.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-10-05 22:24:27 +02:00
1 parent 58473db384
commit bee318870f
5 files changed
+93 -1

No files matched your search

+46
View File
@@ -425,6 +425,52 @@ PLOT_TEST(figure_gutter_never_squeezes_the_axes_below_the_floor) {
}
}
/// The other side of the same band. `axes` reached the viewport's right edge, which left
/// the rightmost x tick label -- centred on it -- half outside the plot, so a figure can
/// ask for an empty band there. One value for every row, like the left gutter, because
/// the rows' axes have to end on a common column as well as start on one.
PLOT_TEST(figure_rows_share_one_right_gutter) {
plot::Figure figure = different_magnitude_figure();
figure.layout();
const plot::f32 flush = figure.plot(0u).layout().axes.right();
PLOT_CHECK_NEAR(static_cast<plot::f64>(figure.right_gutter()), 0.0, 1e-12);
// The default is the layout as it always was: nothing between the data and the frame.
PLOT_CHECK_NEAR(static_cast<plot::f64>(flush), static_cast<plot::f64>(kW), 1e-4);
figure.set_right_gutter(30.0f);
figure.layout();
const plot::Rect first = figure.plot(0u).layout().axes;
for (plot::u32 row = 0; row < 3u; ++row) {
const plot::PlotLayout& rl = figure.plot(row).layout();
PLOT_CHECK_NEAR(static_cast<plot::f64>(rl.right_gutter), 30.0, 1e-4);
PLOT_CHECK_NEAR(static_cast<plot::f64>(rl.axes.right()), static_cast<plot::f64>(kW) - 30.0,
1e-4);
// The band comes off the right alone: the left gutter the rows agreed on is untouched,
// so a host can balance the two sides without the one moving when it sets the other.
PLOT_CHECK_NEAR(static_cast<plot::f64>(rl.axes.x), static_cast<plot::f64>(first.x), 1e-4);
PLOT_CHECK_NEAR(static_cast<plot::f64>(rl.axes.w), static_cast<plot::f64>(first.w), 1e-4);
// ... and the x strip follows the data area rather than the viewport, or the shared
// axis would be labelled over a band that holds no data.
PLOT_CHECK_NEAR(static_cast<plot::f64>(rl.x_label_area.w), static_cast<plot::f64>(rl.axes.w),
1e-4);
}
// Clamped by the same cap as the strips: a band wider than the whole figure cannot
// squeeze `axes` below the floor `Plot::layout()` guarantees.
figure.set_right_gutter(10.0f * kW);
figure.layout();
for (plot::u32 row = 0; row < 3u; ++row) {
const plot::Rect& axes = figure.plot(row).layout().axes;
PLOT_CHECK(axes.w >= 0.5f * kW - 1e-3f);
}
// Negative means "none", like the gutter and the row gap, and the data area goes back
// to the viewport's edge.
figure.set_right_gutter(-5.0f);
PLOT_CHECK_NEAR(static_cast<plot::f64>(figure.right_gutter()), 0.0, 1e-12);
figure.layout();
PLOT_CHECK_NEAR(static_cast<plot::f64>(figure.plot(0u).layout().axes.right()),
static_cast<plot::f64>(flush), 1e-4);
}
/// The degenerate neighbour: a figure of one row imposes exactly what that row
/// measured, so it lays out as the plot would on its own -- the gutter cannot move a
/// row that has no sibling to line up with.