figure: shared-y rows draw one y axis, on the first cell
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
25c3381551
commit
2b4b316ad8
6 files changed
+172
-5
No files matched your search
+29
-1
@@ -641,10 +641,14 @@ const FigureLayout& Figure::layout() {
|
||||
// everywhere, and then there is no shared axis to speak of: each plot has its own. A
|
||||
// split bottom row labels each of its cells, each against its own x axis.
|
||||
const XStrip strip = labels_bottom_only_ && row != bottom_row ? XStrip::None : XStrip::Both;
|
||||
// A split row whose cells share one y range also shares one y axis, drawn by its first
|
||||
// cell: every other cell reserves no y chrome at all (see `YStrip::None`).
|
||||
const bool shared_y = share_row_y_ && cells_per_row_[row] > 1u;
|
||||
for (u32 i = row_first_[row]; i < row_first_[row + 1u]; ++i) {
|
||||
Plot& p = plots_[i];
|
||||
p.set_viewport(layout_.cells[i]);
|
||||
p.set_x_strip_mode(strip);
|
||||
p.set_y_strip_mode(shared_y && i != row_first_[row] ? YStrip::None : YStrip::Both);
|
||||
// Before either pass below, and the same value for every plot: nothing is measured on
|
||||
// the right, so unlike the left gutter this needs no measurement pass -- the plots
|
||||
// end on a common column because they are all told the one width.
|
||||
@@ -687,16 +691,40 @@ const FigureLayout& Figure::layout() {
|
||||
// Over the flattened plots: every cell of every row takes part, so the y axes of a split
|
||||
// row line up with each other and with the rows above and below.
|
||||
f32 gutter = min_y_gutter_;
|
||||
//
|
||||
// A `YStrip::None` cell draws no y axis, so it has no width to contribute and takes no part
|
||||
// in the maximum: the gutter is the widest of the cells that do draw one.
|
||||
for (Plot& p : plots_) {
|
||||
p.set_min_y_gutter(0.0f);
|
||||
const PlotLayout& row_layout = p.layout();
|
||||
if (p.y_strip_mode() == YStrip::None) continue;
|
||||
gutter = std::max(gutter, row_layout.y_axis_width + row_layout.y_title_width);
|
||||
}
|
||||
// Rows that share one y axis divide their *plot* widths, not their cell widths: the first
|
||||
// cell's y strip would otherwise make its plot narrower than its neighbours'. The gutter is
|
||||
// taken off the row first, the remainder is split by `place_cells` (so the weights, the gap
|
||||
// and the floor all apply to plot widths), and the first cell then grows left by the gutter
|
||||
// to hold the axis. The gutter is capped at half the row, so a row too narrow for it
|
||||
// degrades to ordinary cells instead of producing a negative rect. The cells still tile the
|
||||
// row: the first starts on its left edge and the last still ends on its right edge.
|
||||
for (u32 row = 0; row < rows; ++row) {
|
||||
const u32 n = cells_per_row_[row];
|
||||
if (!share_row_y_ || n <= 1u) continue;
|
||||
const Rect& r = layout_.rows[row];
|
||||
const f32 g_eff = std::max(0.0f, std::min(gutter, r.w / 2.0f));
|
||||
Rect* cells = &layout_.cells[row_first_[row]];
|
||||
const f32 used = place_cells(Rect{r.x + g_eff, r.y, r.w - g_eff, r.h}, n, cell_weights_[row],
|
||||
col_gap_, floor_scale, cells);
|
||||
cells[0].x -= g_eff;
|
||||
cells[0].w += g_eff;
|
||||
layout_.col_gap = used < col_gap_ ? 0.0f : layout_.col_gap;
|
||||
for (u32 i = row_first_[row]; i < row_first_[row + 1u]; ++i) plots_[i].set_viewport(layout_.cells[i]);
|
||||
}
|
||||
// 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 (Plot& p : plots_) {
|
||||
p.set_min_y_gutter(gutter);
|
||||
p.set_min_y_gutter(p.y_strip_mode() == YStrip::None ? 0.0f : gutter);
|
||||
p.layout();
|
||||
}
|
||||
|
||||
|
||||
+9
-3
@@ -1319,6 +1319,11 @@ const PlotLayout& Plot::layout() {
|
||||
// the strip wants and this overrides the want, not the measurement.
|
||||
const bool x_labels = x_strip_has_labels(x_strip_);
|
||||
if (!x_labels) x_height = 0.0f;
|
||||
// The y strip likewise: a cell that gives its y axis to a neighbour (`YStrip::None`) reserves
|
||||
// no label width, no title column and no share of the figure's gutter, so every y-strip
|
||||
// quantity below collapses to zero and `axes` starts at the viewport's left edge.
|
||||
const bool y_none = y_strip_ == YStrip::None;
|
||||
if (y_none) y_width = 0.0f;
|
||||
// The title strips, though, are needed before the loop and never change, unlike
|
||||
// the label strips: a strip holds exactly one text line per titled axis and no
|
||||
// tick label can change that. Nothing at all is reserved without a text backend
|
||||
@@ -1332,7 +1337,7 @@ const PlotLayout& Plot::layout() {
|
||||
// no title reserves no row for a title either, and one that reserves the title and no
|
||||
// labels still gets its row -- below where the labels would have been.
|
||||
const u32 x_titles = x_strip_has_title(x_strip_) ? titled_x_axis_count() : 0u;
|
||||
const f32 y_title_w = text_ != nullptr ? y_title_strip_width() : 0.0f;
|
||||
const f32 y_title_w = text_ != nullptr && !y_none ? y_title_strip_width() : 0.0f;
|
||||
const f32 x_title_h = text_ != nullptr && x_titles != 0u
|
||||
? static_cast<f32>(x_titles) * text_->height() + 2.0f * pad
|
||||
: 0.0f;
|
||||
@@ -1353,7 +1358,7 @@ const PlotLayout& Plot::layout() {
|
||||
// -- 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);
|
||||
const f32 y_floor = y_none ? 0.0f : 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);
|
||||
@@ -1386,7 +1391,8 @@ const PlotLayout& Plot::layout() {
|
||||
if (text_ == nullptr || pass + 1 >= kMaxLayoutPasses) break;
|
||||
// The same caps as above, so this compares like with like and the loop still
|
||||
// reaches its fixed point on the second pass.
|
||||
const f32 need_y = std::min(std::clamp(y_label_width(), 0.0f, vw), y_budget);
|
||||
const f32 need_y =
|
||||
y_none ? 0.0f : std::min(std::clamp(y_label_width(), 0.0f, vw), y_budget);
|
||||
// Overridden, not merely unmeasured: the strip is not reserved, so the fixed
|
||||
// point has to be zero for a row that gave its strip away.
|
||||
const f32 need_x =
|
||||
|
||||
@@ -902,9 +902,14 @@ void Renderer::draw_tick_labels(const Plot& plot, const PlotLayout& layout) {
|
||||
// Y labels: right-aligned in their strip, centred on the tick and pulled back
|
||||
// inside the strip at the two extremes (a label centred on the top tick would
|
||||
// otherwise hang above the plot area).
|
||||
//
|
||||
// Unless the plot gave its y strip away (`YStrip::None`, a later cell of a split row that
|
||||
// shares one y axis): its label area is then zero wide, and labels right-aligned against it
|
||||
// would be drawn in the neighbouring cell. Keyed on the area rather than the mode so a strip
|
||||
// the layout clamped to nothing stages nothing either.
|
||||
const f32 top_limit =
|
||||
std::max(layout.y_label_area.y, layout.y_label_area.bottom() - line_h);
|
||||
for (u32 i = 0; i < plot.y_axis_count(); ++i) {
|
||||
for (u32 i = 0; layout.y_label_area.w > 0.0f && i < plot.y_axis_count(); ++i) {
|
||||
const Transform transform = plot.y_transform(i);
|
||||
for (const Tick& tick : plot.y_axis(i).ticks) {
|
||||
if (!tick.major || tick.label.empty()) continue;
|
||||
|
||||
Reference in new issue
Block a user