fix(plot): a figure gives every row the same y gutter so the axes line up
This commit is contained in:
1 parent
663b57c679
commit
cae672bc99
6 files changed
+348
-5
No files matched your search
+31
-2
@@ -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
|
||||
|
||||
+16
-3
@@ -836,7 +836,14 @@ const PlotLayout& Plot::layout() {
|
||||
x_height = std::clamp(x_height, 0.0f, viewport_.h);
|
||||
const f32 y_budget = (1.0f - kMinAxesShare) * viewport_.w;
|
||||
const f32 x_budget = (1.0f - kMinAxesShare) * viewport_.h;
|
||||
y_width = std::min(y_width, y_budget);
|
||||
// The figure's common gutter, when one was imposed: a floor on `y_width + y_title_w`
|
||||
// so every row's axes starts at the same x. It is served by the *label* strip -- the
|
||||
// title column is one text line per titled axis and cannot be widened to fill a gutter
|
||||
// -- and it is applied before the title column below, so the pair still fits the cap
|
||||
// (the floor is clamped to the same budget) and the invariant `axes.w >= share` holds.
|
||||
// `min_y_gutter_` is 0.0 unless a figure set it, so this is a no-op for a lone plot.
|
||||
const f32 y_floor = std::min(min_y_gutter_, y_budget);
|
||||
y_width = std::min(std::max(y_width, y_floor - y_title_w), y_budget);
|
||||
x_height = std::min(x_height, x_budget);
|
||||
const f32 y_title_width = std::min(y_title_w, y_budget - y_width);
|
||||
const f32 x_title_height = std::min(x_title_h, x_budget - x_height);
|
||||
@@ -865,8 +872,14 @@ const PlotLayout& Plot::layout() {
|
||||
const f32 need_x =
|
||||
x_labels ? std::min(std::clamp(text_->height() + 2.0f * pad, 0.0f, viewport_.h), x_budget)
|
||||
: 0.0f;
|
||||
if (need_y == y_width && need_x == x_height) break; // strips are a fixed point
|
||||
y_width = need_y;
|
||||
// What the strip *wants* is the label's own width or the figure's floor, whichever is
|
||||
// greater -- the floor is a maximum over sources, settled by one `max` and not by
|
||||
// asking the loop to grow the strip towards it pass by pass. Comparing the measured
|
||||
// need alone would never agree once a floor sits above it, and the loop would run its
|
||||
// cap out; comparing the want is what keeps it a fixed point here too.
|
||||
const f32 want_y = std::min(std::max(need_y, y_floor - y_title_w), y_budget);
|
||||
if (want_y == y_width && need_x == x_height) break; // strips are a fixed point
|
||||
y_width = want_y;
|
||||
x_height = need_x;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user