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

+8
View File
@@ -136,6 +136,14 @@ class Figure {
/// This is also where a row's `PlotLayout` becomes well-defined: a row's chrome is
/// laid out against `rows[i]` by its own `Plot::layout()`, which is why the figure
/// computes rects and not axes.
///
/// It is also the one place the rows' left chrome can be made to agree (spec §16):
/// each row is laid out once to learn the gutter its own labels and title column
/// need, the figure takes the maximum and imposes it on every row through
/// `Plot::set_min_y_gutter()` before laying the rows out once more -- so every row's
/// `axes.x` and `axes.w` are equal and a vertical feature lands in the same pixel
/// column in each. Two passes per row per call, not a loop: the gutter is a maximum
/// over rows and a row's own strip width does not depend on it.
const FigureLayout& layout();
const FigureLayout& layout() const noexcept { return layout_; }
+21
View File
@@ -524,6 +524,24 @@ class Plot {
/// a lookup on a const Plot cannot move the axes out from under the caller.
const PlotLayout& layout() const noexcept { return layout_; }
/// A floor on the left gutter `layout()` reserves, in device pixels: the label
/// strip and the title column together are never narrower than this, so `axes`
/// never starts left of `viewport().x + min_y_gutter()`. 0.0 -- the default --
/// imposes nothing and the plot sizes its strip from its own widest tick label,
/// which is what a plot standing on its own has always done.
///
/// It exists for `Figure`, which is the only thing that can know the answer: a row
/// cannot see its siblings' tick labels, so the figure lays each row out once to
/// learn the gutter it needs, takes the maximum and sets it here on every row --
/// after which all the rows' `axes.x` and `axes.w` agree and a vertical feature
/// lands in the same pixel column in each. The floor is applied inside the same cap
/// as the measured strip, so a figure's gutter can never squeeze `axes` below the
/// half-viewport floor either. Negative values are clamped to 0.
void set_min_y_gutter(f32 device_px) noexcept {
min_y_gutter_ = device_px > 0.0f ? device_px : 0.0f;
}
f32 min_y_gutter() const noexcept { return min_y_gutter_; }
/// Grid segments for the primary axis pair (x axis 0, y axis 0), in data space.
const Grid& grid() const noexcept { return grid_; }
@@ -713,6 +731,9 @@ class Plot {
Legend legend_{};
/// Which parts of the bottom x chrome are reserved at all; see set_x_strip_mode().
XStrip x_strip_ = XStrip::Both;
/// The floor a figure imposes on the left gutter; see set_min_y_gutter(). 0.0 means
/// none, which is what a plot standing on its own keeps.
f32 min_y_gutter_ = 0.0f;
/// Which handles a pointer is on, published by the input layer (see HotHandles).
HotHandles hot_{};