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
@@ -32,15 +32,16 @@ struct FigureLayout {
|
|||||||
/// rect here; a row of several tiles its rect left to right with `col_gap` between
|
/// rect here; a row of several tiles its rect left to right with `col_gap` between
|
||||||
/// cells, the cells' widths summing to the row's less the gaps.
|
/// cells, the cells' widths summing to the row's less the gaps.
|
||||||
std::vector<Rect> cells;
|
std::vector<Rect> cells;
|
||||||
/// The strip the figure's one shared x axis is drawn in: the bottom row's own
|
/// The strip the figure's bottom x axis is drawn in: the bottom row's own
|
||||||
/// `PlotLayout::x_label_area` (for a bottom row split into several cells, only the **last
|
/// `PlotLayout::x_label_area` (for a bottom row split into several cells, only the **last
|
||||||
/// cell's** strip -- the others' strips are their own `plot(i).layout().x_label_area`, which
|
/// cell's** strip -- the others' strips are their own `plot(i).layout().x_label_area`, which
|
||||||
/// is where the controller's axis-zoom hit test reads each cell's gestures from), at the bottom of the bottom row's rect and so as
|
/// is where the controller's axis-zoom hit test reads each cell's gestures from), at the
|
||||||
/// wide as that row's axes rather than the whole figure. Read back from the row
|
/// bottom of the bottom row's rect and so as wide as that row's axes rather than the whole
|
||||||
|
/// figure. Read back from the row
|
||||||
/// rather than derived here, so a host and the frame cannot disagree about where
|
/// rather than derived here, so a host and the frame cannot disagree about where
|
||||||
/// the axis is. It is nonzero whenever the figure has rows and the bottom row
|
/// the axis is. It is nonzero whenever the figure has rows and the bottom row
|
||||||
/// reserves a label strip -- including the case where every row labels itself, in
|
/// reserves a label strip. A row above whose columns differ from the row below keeps
|
||||||
/// which case it is simply the bottom row's own strip, one of several.
|
/// a strip of its own (see `Figure`), which is its plots' `x_label_area`, not this.
|
||||||
///
|
///
|
||||||
/// It is the rect the controller's axis-zoom hit test reads (`x_label_area` is what
|
/// It is the rect the controller's axis-zoom hit test reads (`x_label_area` is what
|
||||||
/// a middle drag in a label strip enters `DragMode::AxisZoomX` from), which is why
|
/// a middle drag in a label strip enters `DragMode::AxisZoomX` from), which is why
|
||||||
@@ -70,12 +71,15 @@ struct FigureLayout {
|
|||||||
/// **The shared x axis.** Row heights are an equal division of the figure by
|
/// **The shared x axis.** Row heights are an equal division of the figure by
|
||||||
/// default, `set_row_weights()` gives a row more of it, and every row keeps a floor
|
/// default, `set_row_weights()` gives a row more of it, and every row keeps a floor
|
||||||
/// height so chrome can never erase a plot. When `set_x_labels_only_on_bottom()` is
|
/// height so chrome can never erase a plot. When `set_x_labels_only_on_bottom()` is
|
||||||
/// on -- the default -- the figure's one x axis is the **bottom row's own**: the rows
|
/// on -- the default -- the x axis is the **bottom row's own** for every run of rows
|
||||||
/// above reserve no x chrome at all, and the bottom row keeps its own label and title
|
/// with the *same columns* (the same number of cells and the same normalised cell
|
||||||
/// strips, so those labels and that title are drawn once, from the bottom row's own
|
/// weights): those rows above reserve no x chrome at all, and the bottom row of the run
|
||||||
/// ticks and transform. That places the shared axis *inside* a row, where the
|
/// keeps its own label and title strips, so those labels and that title are drawn once,
|
||||||
/// controller's axis-zoom hit test can reach it, instead of in a band outside every
|
/// from its own ticks and transform. A row whose columns differ from the row below keeps
|
||||||
/// row that no gesture could land in.
|
/// its own strip, drawn under each of its cells, because the axis below is at another
|
||||||
|
/// scale and width. The rule is structural, never a function of what a plot holds. That
|
||||||
|
/// places a shared axis *inside* a row, where the controller's axis-zoom hit test can
|
||||||
|
/// reach it, instead of in a band outside every row that no gesture could land in.
|
||||||
///
|
///
|
||||||
/// **The x link.** `plot(i).set_x_range()` and a controller gesture both reach a
|
/// **The x link.** `plot(i).set_x_range()` and a controller gesture both reach a
|
||||||
/// member's x range the same way, so a figure cannot see either of them happen.
|
/// member's x range the same way, so a figure cannot see either of them happen.
|
||||||
@@ -186,14 +190,17 @@ class Figure {
|
|||||||
/// than row by row for the left gutter's reason: the rows' axes have to line up, and
|
/// than row by row for the left gutter's reason: the rows' axes have to line up, and
|
||||||
/// they end on a common column only if they are told one width. See
|
/// they end on a common column only if they are told one width. See
|
||||||
/// `Plot::set_right_gutter()` for what it is for. Negative values are clamped to 0.
|
/// `Plot::set_right_gutter()` for what it is for. Negative values are clamped to 0.
|
||||||
|
/// In a split row whose cells share one y (`set_share_row_y`) only the **last** cell
|
||||||
|
/// keeps it, taken off the row like the y gutter, so the gap between two plots of the
|
||||||
|
/// row is the column gap.
|
||||||
void set_right_gutter(f32 device_px) noexcept {
|
void set_right_gutter(f32 device_px) noexcept {
|
||||||
right_gutter_ = device_px > 0.0f ? device_px : 0.0f;
|
right_gutter_ = device_px > 0.0f ? device_px : 0.0f;
|
||||||
}
|
}
|
||||||
f32 right_gutter() const noexcept { return right_gutter_; }
|
f32 right_gutter() const noexcept { return right_gutter_; }
|
||||||
|
|
||||||
/// Whether the shared x axis is the figure's: every row reserves no x label strip
|
/// Whether rows with the same columns as the row below hand it their x axis (the
|
||||||
/// and one shared axis is drawn under the last row (the default), or every row
|
/// default; a row with different columns keeps its own), or every row draws its own
|
||||||
/// draws its own labels and the figure draws nothing extra.
|
/// labels and the figure draws nothing extra.
|
||||||
void set_x_labels_only_on_bottom(bool on) noexcept { labels_bottom_only_ = on; }
|
void set_x_labels_only_on_bottom(bool on) noexcept { labels_bottom_only_ = on; }
|
||||||
bool x_labels_only_on_bottom() const noexcept { return labels_bottom_only_; }
|
bool x_labels_only_on_bottom() const noexcept { return labels_bottom_only_; }
|
||||||
|
|
||||||
|
|||||||
@@ -238,12 +238,13 @@ class Renderer {
|
|||||||
/// exactly as a stand-alone plot is, and `draw(Plot&)` itself is unchanged for
|
/// exactly as a stand-alone plot is, and `draw(Plot&)` itself is unchanged for
|
||||||
/// single-plot hosts.
|
/// single-plot hosts.
|
||||||
|
|
||||||
/// The figure's one shared x axis is the **bottom row's own**, so it is drawn by
|
/// A shared x axis is the **bottom row's own** (of a run of rows with the same
|
||||||
/// that row's normal path: when the figure says labels belong on the bottom only
|
/// columns), so it is drawn by that row's normal path: when the figure says labels
|
||||||
/// (the default), the rows above reserve no x chrome and the bottom row stages its
|
/// belong on the bottom only (the default), the rows above that match the row below
|
||||||
/// own labels and title once, inside its own strip -- the rect
|
/// reserve no x chrome and the bottom row stages its own labels and title once,
|
||||||
/// `FigureLayout::x_axis_strip` reads back. With that switched off, every row keeps
|
/// inside its own strip -- the rect `FigureLayout::x_axis_strip` reads back. A row
|
||||||
/// its own chrome and each draws its own labels; there is then no shared axis.
|
/// whose columns differ keeps and draws its own; with that switched off, every row
|
||||||
|
/// does and there is no shared axis.
|
||||||
|
|
||||||
/// Styling, the text backend and `ui_scale` are installed on **every** row before
|
/// Styling, the text backend and `ui_scale` are installed on **every** row before
|
||||||
/// the figure computes its layout, so the strips the rows measure and the strokes
|
/// the figure computes its layout, so the strips the rows measure and the strokes
|
||||||
|
|||||||
+79
-23
@@ -73,6 +73,38 @@ f32 place_cells(const Rect& r, u32 n, const std::vector<f32>& weights, f32 col_g
|
|||||||
return gap;
|
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
|
/// 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
|
/// 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
|
/// 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);
|
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
|
// The rows tile the figure itself: an x axis is a row's *own* strip (the bottom row's, and
|
||||||
// nothing is taken off the figure for it. That is what keeps the axis reachable --
|
// the strip of any row whose columns differ from the row below), so nothing is taken off
|
||||||
// its strip is a real `PlotLayout::x_label_area` inside the bottom row's rect, which
|
// the figure for it. That is what keeps the axis reachable -- its strip is a real
|
||||||
// is the rect the controller's axis-zoom hit test reads and the strip an x title is
|
// `PlotLayout::x_label_area` inside a row's rect, which is the rect the controller's
|
||||||
// drawn in -- and it is why an interior row reserves no x chrome at all instead of
|
// axis-zoom hit test reads and the strip an x title is drawn in -- and it is why a row
|
||||||
// leaving a band no one owns.
|
// 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;
|
const f32 content_h = figure_h;
|
||||||
// The gap is dropped, rather than squeezing the rows, when the figure cannot hold
|
// 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
|
// 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;
|
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) {
|
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
|
const XStrip strip = strips[row];
|
||||||
// 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;
|
|
||||||
// A split row whose cells share one y range also shares one y axis, drawn by its first
|
// 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`).
|
// 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;
|
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) {
|
for (u32 i = row_first_[row]; i < row_first_[row + 1u]; ++i) {
|
||||||
Plot& p = plots_[i];
|
Plot& p = plots_[i];
|
||||||
p.set_viewport(layout_.cells[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
|
// 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
|
// 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.
|
// 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
|
// 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
|
// 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
|
// 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.
|
// 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) {
|
for (u32 row = 0; row < rows; ++row) {
|
||||||
const u32 n = cells_per_row_[row];
|
const u32 n = cells_per_row_[row];
|
||||||
if (!share_row_y_ || n <= 1u) continue;
|
if (!share_row_y_ || n <= 1u) continue;
|
||||||
const Rect& r = layout_.rows[row];
|
const Rect& r = layout_.rows[row];
|
||||||
const f32 g_eff = std::max(0.0f, std::min(gutter, r.w / 2.0f));
|
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]];
|
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],
|
const f32 used = place_cells(Rect{r.x + g_eff, r.y, r.w - g_eff - rg_eff, r.h}, n,
|
||||||
col_gap_, floor_scale, cells);
|
cell_weights_[row], col_gap_, floor_scale, cells);
|
||||||
cells[0].x -= g_eff;
|
cells[0].x -= g_eff;
|
||||||
cells[0].w += g_eff;
|
cells[0].w += g_eff;
|
||||||
|
cells[n - 1u].w += rg_eff;
|
||||||
layout_.col_gap = used < col_gap_ ? 0.0f : layout_.col_gap;
|
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]);
|
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
|
// 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
|
// 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();
|
p.layout();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The figure's one x axis is the bottom row's, so the strip it is drawn in is that
|
// `x_axis_strip` is the bottom row's own label area (the last plot's) -- the rect the
|
||||||
// row's own label area -- the rect the controller hit-tests and the renderer stages
|
// controller hit-tests and the renderer stages its labels in, read back here rather than
|
||||||
// its labels in, read back here rather than derived again, so a host and the frame
|
// derived again, so a host and the frame cannot disagree about where it is. Rows above
|
||||||
// cannot disagree about where the shared axis is. It is zero-sized only when there
|
// that carry their own strip (their columns differ from the row below, or every row is
|
||||||
// are no rows at all: with `set_x_labels_only_on_bottom(false)` it is simply the last
|
// labelled) have it in their plots' own `layout().x_label_area`. It is zero-sized only
|
||||||
// row's own strip, one of several.
|
// when there are no rows at all.
|
||||||
layout_.x_axis_strip = plots_.back().layout().x_label_area;
|
layout_.x_axis_strip = plots_.back().layout().x_label_area;
|
||||||
return layout_;
|
return layout_;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1312,8 +1312,8 @@ const PlotLayout& Plot::layout() {
|
|||||||
y_width = y_axes_.empty() ? 0.0f : 2.0f * pad;
|
y_width = y_axes_.empty() ? 0.0f : 2.0f * pad;
|
||||||
x_height = text_->height() + 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
|
// A row of a `Figure` that hands its x axis to the row below (which has the same
|
||||||
// bottom row's own strip, where that axis lives -- so it reserves none: the seed and
|
// 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
|
// 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
|
// 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.
|
// 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
|
// than laying each row out a second time. Nothing draws before this: a row's
|
||||||
// viewport is the figure's to set.
|
// 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
|
// A shared x axis needs no pass of its own: it is the bottom row's own (of a run of rows
|
||||||
// that row stages it in its own strip through `draw_tick_labels()` like any other
|
// with the same columns), so that row stages it in its own strip through
|
||||||
// plot's axis -- and every row above it reserved no x chrome, which is what keeps
|
// `draw_tick_labels()` like any other plot's axis -- and every row of the run above it
|
||||||
// the labels from appearing once per row without a second staging here.
|
// 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();
|
figure.layout();
|
||||||
for (u32 row = 0; row < rows; ++row) {
|
for (u32 row = 0; row < rows; ++row) {
|
||||||
Plot& plot = figure.plot(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();
|
const f32 line_h = font_backend_.height();
|
||||||
|
|
||||||
// X labels: unless the plot gave its x label strip away -- a row of a `Figure`
|
// 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
|
// that reserves no x chrome, because the row below has the same columns and draws the
|
||||||
// row's own strip instead -- in which case it has no strip to put them in and their
|
// 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
|
// 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
|
// 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
|
// strip's single text line, which is the one rule for a plot's labels and a figure's
|
||||||
|
|||||||
+250
-2
@@ -1257,10 +1257,11 @@ PLOT_TEST(figure_cells_share_x_link_and_crosshair) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PLOT_TEST(figure_cells_x_labels_bottom_row_only) {
|
PLOT_TEST(figure_cells_x_labels_bottom_row_only) {
|
||||||
plot::Figure figure = split_figure({1, 2});
|
// Rows with the same columns share the bottom one's axis: {1,1,1} labels the last row only.
|
||||||
|
plot::Figure figure = split_figure({1, 1, 1});
|
||||||
const plot::FigureLayout& fl = figure.layout();
|
const plot::FigureLayout& fl = figure.layout();
|
||||||
PLOT_CHECK(figure.plot(0u).layout().x_label_area.h == 0.0f);
|
PLOT_CHECK(figure.plot(0u).layout().x_label_area.h == 0.0f);
|
||||||
PLOT_CHECK(figure.plot(1u).layout().x_label_area.h > 0.0f);
|
PLOT_CHECK(figure.plot(1u).layout().x_label_area.h == 0.0f);
|
||||||
PLOT_CHECK(figure.plot(2u).layout().x_label_area.h > 0.0f);
|
PLOT_CHECK(figure.plot(2u).layout().x_label_area.h > 0.0f);
|
||||||
PLOT_CHECK_NEAR(fl.x_axis_strip.h, figure.plot(2u).layout().x_label_area.h, 0.001);
|
PLOT_CHECK_NEAR(fl.x_axis_strip.h, figure.plot(2u).layout().x_label_area.h, 0.001);
|
||||||
}
|
}
|
||||||
@@ -1408,3 +1409,250 @@ PLOT_TEST(figure_row_y_sharing_off_still_gives_every_cell_a_gutter) {
|
|||||||
figure.layout();
|
figure.layout();
|
||||||
PLOT_CHECK(figure.plot(1u).layout().y_axis_width > 0.0f);
|
PLOT_CHECK(figure.plot(1u).layout().y_axis_width > 0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------- x axes follow the columns
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
/// A split figure with a text backend and a y axis of a real magnitude on every plot (so the
|
||||||
|
/// shared y gutter is non-zero), the host's configuration: row-shared y, a 6 px column gap and
|
||||||
|
/// a 35 px right gutter.
|
||||||
|
plot::Figure host_like_figure(const std::vector<plot::u32>& cells, bool share_y = true) {
|
||||||
|
plot::Figure figure = split_figure(cells, false);
|
||||||
|
for (plot::u32 i = 0; i < figure.plot_count(); ++i) give_magnitude_row(figure.plot(i), i);
|
||||||
|
figure.set_col_gap(6.0f);
|
||||||
|
figure.set_right_gutter(35.0f);
|
||||||
|
figure.set_share_row_y(share_y);
|
||||||
|
return figure;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shows_x_axis(plot::Figure& figure, plot::u32 plot_index) {
|
||||||
|
const plot::PlotLayout& l = figure.plot(plot_index).layout();
|
||||||
|
return l.x_axis_height > 0.0f && l.x_label_area.h > 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hides_x_axis(plot::Figure& figure, plot::u32 plot_index) {
|
||||||
|
const plot::PlotLayout& l = figure.plot(plot_index).layout();
|
||||||
|
return l.x_axis_height == 0.0f && l.x_label_area.h == 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
PLOT_TEST(figure_x_axis_a_split_row_over_an_unsplit_one_keeps_its_own) {
|
||||||
|
plot::Figure figure = host_like_figure({2, 1});
|
||||||
|
figure.layout();
|
||||||
|
PLOT_CHECK(shows_x_axis(figure, 0u));
|
||||||
|
PLOT_CHECK(shows_x_axis(figure, 1u));
|
||||||
|
PLOT_CHECK(shows_x_axis(figure, 2u));
|
||||||
|
// ... and the other way round.
|
||||||
|
plot::Figure other = host_like_figure({1, 2});
|
||||||
|
other.layout();
|
||||||
|
PLOT_CHECK(shows_x_axis(other, 0u));
|
||||||
|
PLOT_CHECK(shows_x_axis(other, 1u));
|
||||||
|
PLOT_CHECK(shows_x_axis(other, 2u));
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(figure_x_axis_rows_with_the_same_columns_share_the_bottom_axis) {
|
||||||
|
plot::Figure figure = host_like_figure({2, 2});
|
||||||
|
figure.layout();
|
||||||
|
PLOT_CHECK(hides_x_axis(figure, 0u));
|
||||||
|
PLOT_CHECK(hides_x_axis(figure, 1u));
|
||||||
|
PLOT_CHECK(shows_x_axis(figure, 2u));
|
||||||
|
PLOT_CHECK(shows_x_axis(figure, 3u));
|
||||||
|
// The hidden row's cells have exactly the x extents of the cells below, so the axis below
|
||||||
|
// serves them.
|
||||||
|
const plot::FigureLayout& fl = figure.layout();
|
||||||
|
for (plot::u32 c = 0; c < 2u; ++c) {
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[c].x, fl.cells[2u + c].x, 1e-4);
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[c].w, fl.cells[2u + c].w, 1e-4);
|
||||||
|
const plot::PlotLayout& up = figure.plot(c).layout();
|
||||||
|
const plot::PlotLayout& down = figure.plot(2u + c).layout();
|
||||||
|
PLOT_CHECK_NEAR(up.axes.x, down.axes.x, 1e-4);
|
||||||
|
PLOT_CHECK_NEAR(up.axes.w, down.axes.w, 1e-4);
|
||||||
|
PLOT_CHECK_NEAR(up.y_axis_width, down.y_axis_width, 1e-4);
|
||||||
|
}
|
||||||
|
// The same without row-shared y (every cell has its own y strip).
|
||||||
|
plot::Figure own_y = host_like_figure({2, 2}, false);
|
||||||
|
own_y.layout();
|
||||||
|
PLOT_CHECK(hides_x_axis(own_y, 0u) && hides_x_axis(own_y, 1u));
|
||||||
|
PLOT_CHECK(shows_x_axis(own_y, 2u) && shows_x_axis(own_y, 3u));
|
||||||
|
for (plot::u32 c = 0; c < 2u; ++c) {
|
||||||
|
PLOT_CHECK_NEAR(own_y.plot(c).layout().axes.x, own_y.plot(2u + c).layout().axes.x, 1e-4);
|
||||||
|
PLOT_CHECK_NEAR(own_y.plot(c).layout().axes.w, own_y.plot(2u + c).layout().axes.w, 1e-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(figure_x_axis_matching_compares_normalised_weights) {
|
||||||
|
const std::vector<plot::f32> one_three{1.0f, 3.0f};
|
||||||
|
const std::vector<plot::f32> two_six{2.0f, 6.0f};
|
||||||
|
const std::vector<plot::f32> halves{1.0f, 1.0f};
|
||||||
|
{ // 1:3 over 1:3
|
||||||
|
plot::Figure figure = host_like_figure({2, 2});
|
||||||
|
PLOT_CHECK(figure.set_cell_weights(0u, one_three));
|
||||||
|
PLOT_CHECK(figure.set_cell_weights(1u, one_three));
|
||||||
|
figure.layout();
|
||||||
|
PLOT_CHECK(hides_x_axis(figure, 0u) && hides_x_axis(figure, 1u));
|
||||||
|
PLOT_CHECK(shows_x_axis(figure, 2u) && shows_x_axis(figure, 3u));
|
||||||
|
const plot::FigureLayout& fl = figure.layout();
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[0].w, fl.cells[2].w, 1e-3);
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(1u).layout().axes.x, figure.plot(3u).layout().axes.x, 1e-3);
|
||||||
|
}
|
||||||
|
{ // 1:3 over 2:6 -- normalised, so the same columns
|
||||||
|
plot::Figure figure = host_like_figure({2, 2});
|
||||||
|
PLOT_CHECK(figure.set_cell_weights(0u, one_three));
|
||||||
|
PLOT_CHECK(figure.set_cell_weights(1u, two_six));
|
||||||
|
figure.layout();
|
||||||
|
PLOT_CHECK(hides_x_axis(figure, 0u) && hides_x_axis(figure, 1u));
|
||||||
|
}
|
||||||
|
{ // 1:3 over 1:1
|
||||||
|
plot::Figure figure = host_like_figure({2, 2});
|
||||||
|
PLOT_CHECK(figure.set_cell_weights(0u, one_three));
|
||||||
|
PLOT_CHECK(figure.set_cell_weights(1u, halves));
|
||||||
|
figure.layout();
|
||||||
|
PLOT_CHECK(shows_x_axis(figure, 0u) && shows_x_axis(figure, 1u));
|
||||||
|
PLOT_CHECK(shows_x_axis(figure, 2u) && shows_x_axis(figure, 3u));
|
||||||
|
}
|
||||||
|
{ // 1:3 over (no weights = equal)
|
||||||
|
plot::Figure figure = host_like_figure({2, 2});
|
||||||
|
PLOT_CHECK(figure.set_cell_weights(0u, one_three));
|
||||||
|
figure.layout();
|
||||||
|
PLOT_CHECK(shows_x_axis(figure, 0u) && shows_x_axis(figure, 1u));
|
||||||
|
}
|
||||||
|
{ // explicit 1:1 over (no weights = equal)
|
||||||
|
plot::Figure figure = host_like_figure({2, 2});
|
||||||
|
PLOT_CHECK(figure.set_cell_weights(0u, halves));
|
||||||
|
figure.layout();
|
||||||
|
PLOT_CHECK(hides_x_axis(figure, 0u) && hides_x_axis(figure, 1u));
|
||||||
|
}
|
||||||
|
{ // a different number of cells never matches, whatever the weights
|
||||||
|
plot::Figure figure = host_like_figure({3, 2});
|
||||||
|
figure.layout();
|
||||||
|
PLOT_CHECK(shows_x_axis(figure, 0u) && shows_x_axis(figure, 2u));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(figure_x_axis_unsplit_rows_are_unchanged) {
|
||||||
|
plot::Figure figure = host_like_figure({1, 1, 1});
|
||||||
|
figure.layout();
|
||||||
|
PLOT_CHECK(hides_x_axis(figure, 0u));
|
||||||
|
PLOT_CHECK(hides_x_axis(figure, 1u));
|
||||||
|
PLOT_CHECK(shows_x_axis(figure, 2u));
|
||||||
|
PLOT_CHECK_NEAR(figure.layout().x_axis_strip.h, figure.plot(2u).layout().x_label_area.h, 1e-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(figure_x_axis_follows_a_chain_of_matching_rows) {
|
||||||
|
{ // [2]/[2]/[1]: row 0 matches row 1 and hides; row 1 does not match row 2 and shows.
|
||||||
|
plot::Figure figure = host_like_figure({2, 2, 1});
|
||||||
|
figure.layout();
|
||||||
|
PLOT_CHECK(hides_x_axis(figure, 0u) && hides_x_axis(figure, 1u));
|
||||||
|
PLOT_CHECK(shows_x_axis(figure, 2u) && shows_x_axis(figure, 3u));
|
||||||
|
PLOT_CHECK(shows_x_axis(figure, 4u));
|
||||||
|
}
|
||||||
|
{ // [2]/[2]/[2]: transitive -- only the last row carries the axis.
|
||||||
|
plot::Figure figure = host_like_figure({2, 2, 2});
|
||||||
|
figure.layout();
|
||||||
|
for (plot::u32 i = 0; i < 4u; ++i) PLOT_CHECK(hides_x_axis(figure, i));
|
||||||
|
PLOT_CHECK(shows_x_axis(figure, 4u) && shows_x_axis(figure, 5u));
|
||||||
|
}
|
||||||
|
{ // [1]/[2]/[2]: the unsplit row matches nothing below it.
|
||||||
|
plot::Figure figure = host_like_figure({1, 2, 2});
|
||||||
|
figure.layout();
|
||||||
|
PLOT_CHECK(shows_x_axis(figure, 0u));
|
||||||
|
PLOT_CHECK(hides_x_axis(figure, 1u) && hides_x_axis(figure, 2u));
|
||||||
|
PLOT_CHECK(shows_x_axis(figure, 3u) && shows_x_axis(figure, 4u));
|
||||||
|
}
|
||||||
|
{ // the figure's x_axis_strip is still the last plot's label area
|
||||||
|
plot::Figure figure = host_like_figure({2, 1});
|
||||||
|
const plot::FigureLayout& fl = figure.layout();
|
||||||
|
PLOT_CHECK(fl.x_axis_strip.h > 0.0f);
|
||||||
|
PLOT_CHECK_NEAR(fl.x_axis_strip.h, figure.plot(2u).layout().x_label_area.h, 1e-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(figure_x_axis_labels_on_every_row_when_asked) {
|
||||||
|
plot::Figure figure = host_like_figure({2, 2, 1});
|
||||||
|
figure.set_x_labels_only_on_bottom(false);
|
||||||
|
figure.layout();
|
||||||
|
for (plot::u32 i = 0; i < figure.plot_count(); ++i) PLOT_CHECK(shows_x_axis(figure, i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------- the gap between cells of a row
|
||||||
|
|
||||||
|
PLOT_TEST(figure_split_row_gap_between_plot_frames_is_the_column_gap) {
|
||||||
|
for (const plot::u32 n : {2u, 3u, 4u}) {
|
||||||
|
plot::Figure figure = host_like_figure({n});
|
||||||
|
const plot::FigureLayout& fl = figure.layout();
|
||||||
|
PLOT_CHECK_NEAR(fl.col_gap, 6.0, 1e-4);
|
||||||
|
for (plot::u32 k = 0; k + 1u < n; ++k) {
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(k + 1u).layout().axes.x - figure.plot(k).layout().axes.right(),
|
||||||
|
6.0, 1e-3);
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[k + 1u].x - fl.cells[k].right(), 6.0, 1e-3);
|
||||||
|
// Only the last cell holds the right gutter.
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(k).layout().right_gutter, 0.0, 1e-6);
|
||||||
|
}
|
||||||
|
// Equal weights give equal axes widths, the first cell included.
|
||||||
|
for (plot::u32 k = 1; k < n; ++k) {
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(k).layout().axes.w, figure.plot(0u).layout().axes.w, 0.6);
|
||||||
|
}
|
||||||
|
// The cells tile the row exactly.
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[0].x, fl.rows[0].x, 1e-4);
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[n - 1u].right(), fl.rows[0].right(), 1e-3);
|
||||||
|
// The right gutter is still there, on the last cell.
|
||||||
|
const plot::PlotLayout& last = figure.plot(n - 1u).layout();
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[n - 1u].right() - last.axes.right(), 35.0, 1e-3);
|
||||||
|
PLOT_CHECK_NEAR(last.right_gutter, 35.0, 1e-3);
|
||||||
|
// The y gutter still sits on the first cell alone.
|
||||||
|
PLOT_CHECK(figure.plot(0u).layout().axes.x > fl.cells[0].x);
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(1u).layout().axes.x, fl.cells[1].x, 1e-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(figure_split_row_gap_with_weights_and_a_y_gutter) {
|
||||||
|
plot::Figure figure = host_like_figure({1, 2});
|
||||||
|
const std::vector<plot::f32> w{3.0f, 1.0f};
|
||||||
|
PLOT_CHECK(figure.set_cell_weights(1u, w));
|
||||||
|
const plot::FigureLayout& fl = figure.layout();
|
||||||
|
const plot::Rect a0 = figure.plot(1u).layout().axes;
|
||||||
|
const plot::Rect a1 = figure.plot(2u).layout().axes;
|
||||||
|
PLOT_CHECK_NEAR(a1.x - a0.right(), 6.0, 1e-3);
|
||||||
|
PLOT_CHECK_NEAR(a0.w - 64.0, (a1.w - 64.0) * 3.0, 1.0);
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[2].right(), fl.rows[1].right(), 1e-3);
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[1].x, fl.rows[1].x, 1e-4);
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[2].right() - a1.right(), 35.0, 1e-3);
|
||||||
|
// The unsplit row above keeps its own right gutter on its one plot.
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[0].right() - figure.plot(0u).layout().axes.right(), 35.0, 1e-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(figure_split_row_gutters_degrade_on_a_narrow_row) {
|
||||||
|
plot::Figure figure = host_like_figure({3});
|
||||||
|
figure.set_viewport(plot::Rect{0.0f, 0.0f, 60.0f, 100.0f});
|
||||||
|
const plot::FigureLayout& fl = figure.layout();
|
||||||
|
double sum = 0.0;
|
||||||
|
for (const plot::Rect& c : fl.cells) {
|
||||||
|
PLOT_CHECK(c.w >= 0.0f && c.h >= 0.0f);
|
||||||
|
sum += c.w;
|
||||||
|
}
|
||||||
|
PLOT_CHECK_NEAR(sum + fl.col_gap * 2.0, 60.0, 0.05);
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[2].right(), 60.0, 0.05);
|
||||||
|
for (plot::u32 i = 0; i < 3u; ++i) PLOT_CHECK(figure.plot(i).layout().axes.w >= 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(figure_row_gutter_without_sharing_is_the_old_layout) {
|
||||||
|
plot::Figure figure = host_like_figure({2}, false);
|
||||||
|
const plot::FigureLayout& fl = figure.layout();
|
||||||
|
// Plain cells: equal halves of the row less the gap, every plot with its own right gutter.
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[0].w, (fl.rows[0].w - 6.0) / 2.0, 1e-3);
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[1].w, (fl.rows[0].w - 6.0) / 2.0, 1e-3);
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[1].x - fl.cells[0].right(), 6.0, 1e-4);
|
||||||
|
for (plot::u32 i = 0; i < 2u; ++i) {
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(i).layout().right_gutter, 35.0, 1e-3);
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[i].right() - figure.plot(i).layout().axes.right(), 35.0, 1e-3);
|
||||||
|
}
|
||||||
|
// An unsplit row is unchanged by sharing being on.
|
||||||
|
plot::Figure unsplit = host_like_figure({1, 1});
|
||||||
|
const plot::FigureLayout& ul = unsplit.layout();
|
||||||
|
for (plot::u32 i = 0; i < 2u; ++i) {
|
||||||
|
PLOT_CHECK_NEAR(ul.cells[i].w, ul.rows[i].w, 1e-4);
|
||||||
|
PLOT_CHECK_NEAR(ul.cells[i].right() - unsplit.plot(i).layout().axes.right(), 35.0, 1e-3);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user