fix(plot): a figure gives every row the same y gutter so the axes line up

This commit is contained in:
Martino Ferrari committed 2026-09-28 20:11:17 +02:00
1 parent 663b57c679
commit cae672bc99
6 files changed
+348 -5

No files matched your search

+31 -2
View File
@@ -393,12 +393,41 @@ const FigureLayout& Figure::layout() {
// its transforms describe this frame's rects; and before the rows lay themselves
// out below, so a following group's window is the range they lay out with.
sync_x_links();
// Every row is laid out against its own rect here, so after this call each row's
// Every row is laid out against its own rect, so after this call each row's
// chrome is current: `plot(i).layout()` reads back what the figure computed rather
// than what the row last held. That is the contract `Renderer::draw(Figure&)`
// relies on -- it draws each row with the layout this produced, and `draw(Plot&)`
// is the one that computes its own.
for (u32 row = 0; row < rows; ++row) plots_[row].layout();
//
// The rows' axes have to line up (spec §16), so this is where the figure's **common
// left gutter** is decided -- a row cannot see its siblings' tick labels, so the one
// width they can agree on has to be handed to them from here.
//
// Two passes, and no more. The first lays every row out with no floor, so each
// reports the gutter its own labels and title column need; the figure keeps the
// maximum. The second re-lays every row with that maximum imposed on its
// `Plot::set_min_y_gutter()`, which widens a row's strip to fill it, so all the rows'
// `axes` start and end on the same columns. A fixed-point loop -- re-measuring until
// the rows stop moving -- is the tempting wrong answer and is not needed: the gutter
// is a *maximum* over rows, and a row's own strip width does not depend on any other
// row's or on the width imposed on it (y tick labels are chosen from the row's own
// range and height, and a title column is one text line per titled axis), so one read
// per row settles the answer exactly. Measuring with a floor from the previous frame
// would be the only reason to iterate, and clearing it before the measurement pass is
// what removes it.
f32 gutter = 0.0f;
for (u32 row = 0; row < rows; ++row) {
plots_[row].set_min_y_gutter(0.0f);
const PlotLayout& row_layout = plots_[row].layout();
gutter = std::max(gutter, row_layout.y_axis_width + row_layout.y_title_width);
}
// A one-row figure imposes exactly what that row measured, so it lays out as the plot
// would on its own; a figure whose rows all need the same strip imposes that, which is
// also what each would have done alone.
for (u32 row = 0; row < rows; ++row) {
plots_[row].set_min_y_gutter(gutter);
plots_[row].layout();
}
// The figure's one x axis is the bottom row's, so the strip it is drawn in is that
// row's own label area -- the rect the controller hit-tests and the renderer stages