figure: x axes follow the column structure; split rows keep the right gutter on the last cell only
This commit is contained in:
1 parent
69f0083fc0
commit
2cb31cbb91
6 files changed
+366
-53
No files matched your search
+79
-23
@@ -73,6 +73,38 @@ f32 place_cells(const Rect& r, u32 n, const std::vector<f32>& weights, f32 col_g
|
||||
return gap;
|
||||
}
|
||||
|
||||
/// The share of the row each of its `n` cells is given by `weights`, exactly as `place_cells`
|
||||
/// reads them: an empty (or wrongly sized, or non-positive-total) vector means equal shares.
|
||||
std::vector<f32> normalised_shares(const std::vector<f32>& weights, u32 n) {
|
||||
std::vector<f32> shares(n, 1.0f / static_cast<f32>(n));
|
||||
f32 total = 0.0f;
|
||||
if (weights.size() == n) {
|
||||
for (const f32 w : weights) total += w;
|
||||
}
|
||||
if (total > 0.0f) {
|
||||
for (u32 i = 0; i < n; ++i) shares[i] = weights[i] / total;
|
||||
}
|
||||
return shares;
|
||||
}
|
||||
|
||||
/// Whether two rows have the **same columns**: the same number of cells and the same
|
||||
/// normalised cell weights (within 1e-4). The rows are all the figure's width, so such rows
|
||||
/// get cells with identical x extents -- the same y gutter, gap, floors and right gutter
|
||||
/// apply to both -- and one row's x axis serves the other. Structural, never content: it
|
||||
/// reads the cell counts and the weights the host set and nothing a plot holds.
|
||||
bool same_columns(const std::vector<u32>& cells_per_row,
|
||||
const std::vector<std::vector<f32>>& cell_weights, u32 row_a, u32 row_b) {
|
||||
const u32 n = cells_per_row[row_a];
|
||||
if (n != cells_per_row[row_b]) return false;
|
||||
if (n == 1u) return true;
|
||||
const std::vector<f32> a = normalised_shares(cell_weights[row_a], n);
|
||||
const std::vector<f32> b = normalised_shares(cell_weights[row_b], n);
|
||||
for (u32 i = 0; i < n; ++i) {
|
||||
if (std::fabs(a[i] - b[i]) > 1e-4f) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// A coordinate in the shared axis' **axis space**: `log10` when the figure's one
|
||||
/// x axis -- the bottom row's, the row the shared strip belongs to -- is
|
||||
/// logarithmic, the value itself otherwise. A following group's span and offset
|
||||
@@ -558,12 +590,13 @@ const FigureLayout& Figure::layout() {
|
||||
}
|
||||
|
||||
const f32 figure_h = std::max(0.0f, viewport_.h);
|
||||
// The rows tile the figure itself: the shared x axis is the *bottom row's own*, so
|
||||
// nothing is taken off the figure for it. That is what keeps the axis reachable --
|
||||
// its strip is a real `PlotLayout::x_label_area` inside the bottom row's rect, which
|
||||
// is the rect the controller's axis-zoom hit test reads and the strip an x title is
|
||||
// drawn in -- and it is why an interior row reserves no x chrome at all instead of
|
||||
// leaving a band no one owns.
|
||||
// The rows tile the figure itself: an x axis is a row's *own* strip (the bottom row's, and
|
||||
// the strip of any row whose columns differ from the row below), so nothing is taken off
|
||||
// the figure for it. That is what keeps the axis reachable -- its strip is a real
|
||||
// `PlotLayout::x_label_area` inside a row's rect, which is the rect the controller's
|
||||
// axis-zoom hit test reads and the strip an x title is drawn in -- and it is why a row
|
||||
// that hands its axis to the one below reserves no x chrome at all instead of leaving a
|
||||
// band no one owns.
|
||||
const f32 content_h = figure_h;
|
||||
// The gap is dropped, rather than squeezing the rows, when the figure cannot hold
|
||||
// it: a figure one pixel tall with four rows still gives every row a positive
|
||||
@@ -633,18 +666,31 @@ const FigureLayout& Figure::layout() {
|
||||
if (n > 1u && used < col_gap_) layout_.col_gap = 0.0f;
|
||||
}
|
||||
|
||||
const u32 bottom_row = rows - 1u;
|
||||
// Which rows carry their own x strip, decided structurally and before the first
|
||||
// measurement pass so that pass and the final one see the same strips. The bottom row
|
||||
// always has one. A row above hides its strip only when the row directly below has the
|
||||
// same columns (see `same_columns()`), because then the cells of the two rows have the
|
||||
// same x extents and the axis below serves this row too; the rule is transitive, since
|
||||
// that row is itself either labelled or hidden in favour of one that matches it. A row
|
||||
// whose columns differ from the one below -- `[1][2]` over `[3]` -- keeps its own strip,
|
||||
// drawn under each of its cells, because the axis below is at another scale and width.
|
||||
// A figure told to label every row keeps the strips everywhere.
|
||||
std::vector<XStrip> strips(rows, XStrip::Both);
|
||||
if (labels_bottom_only_) {
|
||||
for (u32 row = rows - 1u; row-- > 0u;) {
|
||||
// By induction the row below is labelled or itself hidden in favour of a matching
|
||||
// row, so hiding this one on a match with it is the transitive rule.
|
||||
if (same_columns(cells_per_row_, cell_weights_, row, row + 1u)) strips[row] = XStrip::None;
|
||||
}
|
||||
}
|
||||
for (u32 row = 0; row < rows; ++row) {
|
||||
// One x axis for the whole figure, and it is the bottom row's: every plot keeps its
|
||||
// x chrome -- labels, title and the strip the controller's axis zoom is found in --
|
||||
// and every plot above the bottom row gives all of it to that row, so the labels and
|
||||
// the title are not drawn once per row. A figure told to label every row keeps them
|
||||
// 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;
|
||||
const XStrip strip = strips[row];
|
||||
// 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;
|
||||
// ... and its right gutter is the last cell's alone, so the gap between two plots of the
|
||||
// row is the column gap and not a gutter more (see the placement block below).
|
||||
const u32 last = row_first_[row + 1u] - 1u;
|
||||
for (u32 i = row_first_[row]; i < row_first_[row + 1u]; ++i) {
|
||||
Plot& p = plots_[i];
|
||||
p.set_viewport(layout_.cells[i]);
|
||||
@@ -653,7 +699,7 @@ const FigureLayout& Figure::layout() {
|
||||
// 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.
|
||||
p.set_right_gutter(right_gutter_);
|
||||
p.set_right_gutter(shared_y && i != last ? 0.0f : right_gutter_);
|
||||
}
|
||||
}
|
||||
// After the viewports, so a row's ticks and grid -- built from its axes rect -- and
|
||||
@@ -708,18 +754,28 @@ const FigureLayout& Figure::layout() {
|
||||
// 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.
|
||||
//
|
||||
// The right gutter is taken off the same way and belongs to the **last** cell alone (the
|
||||
// other cells were told none above): left to every cell, the visible gap between two
|
||||
// plots would be the column gap plus a whole right gutter. Together with the y gutter it is
|
||||
// capped at half the row, and never exceeds the figure's own right gutter.
|
||||
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));
|
||||
const f32 rg_eff = std::max(0.0f, std::min(right_gutter_, r.w / 2.0f - g_eff));
|
||||
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);
|
||||
const f32 used = place_cells(Rect{r.x + g_eff, r.y, r.w - g_eff - rg_eff, r.h}, n,
|
||||
cell_weights_[row], col_gap_, floor_scale, cells);
|
||||
cells[0].x -= g_eff;
|
||||
cells[0].w += g_eff;
|
||||
cells[n - 1u].w += rg_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]);
|
||||
// What the cell actually reserved, so a row too narrow for the whole gutter is not asked
|
||||
// for more than it was given (the same value whenever the row has room).
|
||||
plots_[row_first_[row + 1u] - 1u].set_right_gutter(rg_eff);
|
||||
}
|
||||
// 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
|
||||
@@ -729,12 +785,12 @@ const FigureLayout& Figure::layout() {
|
||||
p.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
|
||||
// its labels in, read back here rather than derived again, so a host and the frame
|
||||
// cannot disagree about where the shared axis is. It is zero-sized only when there
|
||||
// are no rows at all: with `set_x_labels_only_on_bottom(false)` it is simply the last
|
||||
// row's own strip, one of several.
|
||||
// `x_axis_strip` is the bottom row's own label area (the last plot's) -- the rect the
|
||||
// controller hit-tests and the renderer stages its labels in, read back here rather than
|
||||
// derived again, so a host and the frame cannot disagree about where it is. Rows above
|
||||
// that carry their own strip (their columns differ from the row below, or every row is
|
||||
// labelled) have it in their plots' own `layout().x_label_area`. It is zero-sized only
|
||||
// when there are no rows at all.
|
||||
layout_.x_axis_strip = plots_.back().layout().x_label_area;
|
||||
return layout_;
|
||||
}
|
||||
|
||||
+2
-2
@@ -1312,8 +1312,8 @@ const PlotLayout& Plot::layout() {
|
||||
y_width = y_axes_.empty() ? 0.0f : 2.0f * pad;
|
||||
x_height = text_->height() + 2.0f * pad;
|
||||
}
|
||||
// A row of a `Figure` gives its x strip to the figure's one shared axis -- the
|
||||
// bottom row's own strip, where that axis lives -- so it reserves none: the seed and
|
||||
// A row of a `Figure` that hands its x axis to the row below (which has the same
|
||||
// columns, so the same x extents) reserves no strip: the seed and
|
||||
// the fixed point below both start from zero, and `axes` then reaches the viewport's
|
||||
// bottom edge. Set here rather than in the loop, because the loop's `need_x` is what
|
||||
// the strip wants and this overrides the want, not the measurement.
|
||||
|
||||
@@ -480,10 +480,11 @@ void Renderer::draw(Figure& figure) {
|
||||
// than laying each row out a second time. Nothing draws before this: a row's
|
||||
// viewport is the figure's to set.
|
||||
//
|
||||
// The figure's one x axis needs no pass of its own: it is the bottom row's own, so
|
||||
// that row stages it in its own strip through `draw_tick_labels()` like any other
|
||||
// plot's axis -- and every row above it reserved no x chrome, which is what keeps
|
||||
// the labels from appearing once per row without a second staging here.
|
||||
// A shared x axis needs no pass of its own: it is the bottom row's own (of a run of rows
|
||||
// with the same columns), so that row stages it in its own strip through
|
||||
// `draw_tick_labels()` like any other plot's axis -- and every row of the run above it
|
||||
// reserved no x chrome, which is what keeps the labels from appearing once per row without
|
||||
// a second staging here. A row whose columns differ stages its own.
|
||||
figure.layout();
|
||||
for (u32 row = 0; row < rows; ++row) {
|
||||
Plot& plot = figure.plot(row);
|
||||
@@ -878,8 +879,8 @@ void Renderer::draw_tick_labels(const Plot& plot, const PlotLayout& layout) {
|
||||
const f32 line_h = font_backend_.height();
|
||||
|
||||
// X labels: unless the plot gave its x label strip away -- a row of a `Figure`
|
||||
// that reserves no x chrome, because the figure draws one shared axis in the bottom
|
||||
// row's own strip instead -- in which case it has no strip to put them in and their
|
||||
// that reserves no x chrome, because the row below has the same columns and draws the
|
||||
// axis in its own strip instead -- in which case it has no strip to put them in and their
|
||||
// absence is the point. A plot with no strip would otherwise stage its labels just
|
||||
// above `axes`' bottom edge, i.e. over its own data. Centred under their tick on the
|
||||
// strip's single text line, which is the one rule for a plot's labels and a figure's
|
||||
|
||||
Reference in new issue
Block a user