figure: rows can hold several plots (cells), cell weights and column gap
This commit is contained in:
1 parent
f79a97be9b
commit
7d8c453232
4 files changed
+341
-31
No files matched your search
@@ -27,6 +27,11 @@ struct FigureLayout {
|
|||||||
/// strips and axes come from its own `Plot::layout()` against this rect, so a row
|
/// strips and axes come from its own `Plot::layout()` against this rect, so a row
|
||||||
/// with a y title keeps its column and no two rows can disagree about chrome.
|
/// with a y title keeps its column and no two rows can disagree about chrome.
|
||||||
std::vector<Rect> rows;
|
std::vector<Rect> rows;
|
||||||
|
/// One rect per plot, flattened row-major (row 0's cells left to right, then row 1's,
|
||||||
|
/// ...): the **viewport** each `Plot` was given. A row of one cell has exactly its row
|
||||||
|
/// 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.
|
||||||
|
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 one shared x axis is drawn in: the bottom row's own
|
||||||
/// `PlotLayout::x_label_area`, at the bottom of the bottom row's rect and so as
|
/// `PlotLayout::x_label_area`, at the 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
|
/// wide as that row's axes rather than the whole figure. Read back from the row
|
||||||
@@ -44,6 +49,10 @@ struct FigureLayout {
|
|||||||
/// figure edge to edge, which is the default; it is dropped for a figure too
|
/// figure edge to edge, which is the default; it is dropped for a figure too
|
||||||
/// short to hold it, so the rows never lose their height to a gap.
|
/// short to hold it, so the rows never lose their height to a gap.
|
||||||
f32 row_gap = 0.0f;
|
f32 row_gap = 0.0f;
|
||||||
|
/// The horizontal gap between two cells of a row, in device pixels. `set_col_gap()`'s
|
||||||
|
/// value, or 0 when any row holding several cells was too narrow to hold it and
|
||||||
|
/// dropped it, so the cells never lose their width to a gap.
|
||||||
|
f32 col_gap = 0.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Several plots stacked in one column, sharing one x axis.
|
/// Several plots stacked in one column, sharing one x axis.
|
||||||
@@ -94,13 +103,30 @@ class Figure {
|
|||||||
/// which is the *unlinked* group: rows that share nothing, each with the range
|
/// which is the *unlinked* group: rows that share nothing, each with the range
|
||||||
/// its own plot was given.
|
/// its own plot was given.
|
||||||
explicit Figure(u32 rows);
|
explicit Figure(u32 rows);
|
||||||
|
/// A figure of `cells_per_row.size()` rows, row `r` holding `cells_per_row[r]` plots side
|
||||||
|
/// by side (a count of 0 is clamped to 1). Every row of one cell makes this the same
|
||||||
|
/// figure as `Figure(rows)`. There is deliberately no `initializer_list` constructor, so
|
||||||
|
/// `Figure{3u}` keeps meaning three rows.
|
||||||
|
explicit Figure(std::span<const u32> cells_per_row);
|
||||||
|
|
||||||
u32 row_count() const noexcept { return static_cast<u32>(plots_.size()); }
|
/// The number of figure rows -- the bands stacked top to bottom -- which equals
|
||||||
/// One row's plot. A row that does not exist yields a default-constructed `Plot`
|
/// `plot_count()` for a figure no row of which is split.
|
||||||
/// rather than reading out of bounds -- the rule every accessor on `Plot` follows
|
u32 row_count() const noexcept { return static_cast<u32>(cells_per_row_.size()); }
|
||||||
/// -- so a stale row can only ever affect itself.
|
/// The number of plots, i.e. cells over all rows. `plot()`, `link_x()`, `x_link()` and
|
||||||
Plot& plot(u32 row) noexcept;
|
/// the hovered plot of `set_crosshair()` index this flattened (row-major) range.
|
||||||
const Plot& plot(u32 row) const noexcept;
|
u32 plot_count() const noexcept { return static_cast<u32>(plots_.size()); }
|
||||||
|
/// How many cells row `row` holds; 0 for a row that does not exist.
|
||||||
|
u32 cells_in_row(u32 row) const noexcept;
|
||||||
|
/// The flattened index of row `row`'s first cell; `plot_count()` for `row ==
|
||||||
|
/// row_count()` and for anything beyond.
|
||||||
|
u32 first_plot_of_row(u32 row) const noexcept;
|
||||||
|
/// The row that holds flattened plot `plot` (the last row for an index past the end).
|
||||||
|
u32 row_of_plot(u32 plot) const noexcept;
|
||||||
|
/// One plot, by flattened cell index. A plot that does not exist yields a
|
||||||
|
/// default-constructed `Plot` rather than reading out of bounds -- the rule every
|
||||||
|
/// accessor on `Plot` follows -- so a stale index can only ever affect itself.
|
||||||
|
Plot& plot(u32 index) noexcept;
|
||||||
|
const Plot& plot(u32 index) const noexcept;
|
||||||
|
|
||||||
// --- geometry ---------------------------------------------------------
|
// --- geometry ---------------------------------------------------------
|
||||||
|
|
||||||
@@ -119,6 +145,21 @@ class Figure {
|
|||||||
void clear_row_weights() noexcept { row_weights_.clear(); }
|
void clear_row_weights() noexcept { row_weights_.clear(); }
|
||||||
bool has_row_weights() const noexcept { return !row_weights_.empty(); }
|
bool has_row_weights() const noexcept { return !row_weights_.empty(); }
|
||||||
|
|
||||||
|
/// How much of row `row`'s width each of its cells gets: `weights[i]` is cell i's
|
||||||
|
/// share of what is left after every cell has its floor width (64 logical pixels,
|
||||||
|
/// capped at an equal share of a narrow row). Normalised. Refused, changing nothing,
|
||||||
|
/// when the row does not exist, the count is not the row's cell count, or a weight is
|
||||||
|
/// not a positive finite number. A row with no weights divides equally.
|
||||||
|
Status set_cell_weights(u32 row, std::span<const f32> weights);
|
||||||
|
/// Back to an equal division of row `row`; a row that does not exist is ignored.
|
||||||
|
void clear_cell_weights(u32 row) noexcept;
|
||||||
|
bool has_cell_weights(u32 row) const noexcept;
|
||||||
|
/// The horizontal gap left between two cells of a row, in device pixels (0.0 by
|
||||||
|
/// default). Negative values are clamped to 0, and a gap too wide for a row to hold is
|
||||||
|
/// dropped for that layout rather than squeezing the cells to nothing.
|
||||||
|
void set_col_gap(f32 device_px) noexcept { col_gap_ = device_px > 0.0f ? device_px : 0.0f; }
|
||||||
|
f32 col_gap() const noexcept { return col_gap_; }
|
||||||
|
|
||||||
/// The gap left between two rows, in device pixels (0.0 by default, so the rows
|
/// The gap left between two rows, in device pixels (0.0 by default, so the rows
|
||||||
/// tile the figure). Negative values are clamped to 0, and a gap too large for the
|
/// tile the figure). Negative values are clamped to 0, and a gap too large for the
|
||||||
/// figure to hold is dropped for that layout rather than squeezing the rows to
|
/// figure to hold is dropped for that layout rather than squeezing the rows to
|
||||||
@@ -325,7 +366,18 @@ class Figure {
|
|||||||
/// a following group's window, push both to all members.
|
/// a following group's window, push both to all members.
|
||||||
void sync_x_links();
|
void sync_x_links();
|
||||||
|
|
||||||
|
/// Fills the row bookkeeping (`cells_per_row_`, `row_first_`, `cell_weights_`) from a
|
||||||
|
/// per-row cell count, clamping each to at least one. Does not touch `plots_`, which the
|
||||||
|
/// constructors size from `total_cells()`.
|
||||||
|
void build_cells(std::span<const u32> cells);
|
||||||
|
|
||||||
std::vector<Plot> plots_;
|
std::vector<Plot> plots_;
|
||||||
|
std::vector<u32> cells_per_row_;
|
||||||
|
/// Prefix sums of `cells_per_row_`: `rows + 1` entries, `row_first_[r]` the flattened
|
||||||
|
/// index of row r's first cell and the last entry the plot count.
|
||||||
|
std::vector<u32> row_first_;
|
||||||
|
std::vector<std::vector<f32>> cell_weights_;
|
||||||
|
f32 col_gap_ = 0.0f;
|
||||||
/// What `plot()` yields for a row that does not exist, so a stale row can be used
|
/// What `plot()` yields for a row that does not exist, so a stale row can be used
|
||||||
/// without reading out of bounds. Owned rather than a static, so one figure's
|
/// without reading out of bounds. Owned rather than a static, so one figure's
|
||||||
/// stray row can never write into another's.
|
/// stray row can never write into another's.
|
||||||
|
|||||||
+148
-24
@@ -23,6 +23,56 @@ namespace {
|
|||||||
/// wants a taller floor gives the figure a taller viewport, or gives the rows weights.
|
/// wants a taller floor gives the figure a taller viewport, or gives the rows weights.
|
||||||
constexpr f32 kMinRowHeightLogical = 24.0f;
|
constexpr f32 kMinRowHeightLogical = 24.0f;
|
||||||
|
|
||||||
|
/// The width a cell never drops below, in logical pixels, converted exactly as the row
|
||||||
|
/// height floor is. It yields to the row -- capped at an equal share of the width there
|
||||||
|
/// is -- so a row too narrow for its cells still tiles instead of overflowing.
|
||||||
|
constexpr f32 kMinCellWidthLogical = 64.0f;
|
||||||
|
|
||||||
|
/// The number of plots a per-row cell count asks for, each row holding at least one.
|
||||||
|
u32 total_cells(std::span<const u32> cells) noexcept {
|
||||||
|
u32 total = 0;
|
||||||
|
for (const u32 c : cells) total += std::max<u32>(c, 1u);
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Splits the row rect `r` into `n` cells in `out[0..n)` and returns the gap actually used.
|
||||||
|
/// The cells tile `r` exactly: the gap is dropped when `r` cannot hold it, every cell first
|
||||||
|
/// gets the floor width and then its weight's share of what is left, and the last cell
|
||||||
|
/// closes the row on its right edge, absorbing the rounding of the ones before it.
|
||||||
|
f32 place_cells(const Rect& r, u32 n, const std::vector<f32>& weights, f32 col_gap,
|
||||||
|
f32 ui_scale, Rect* out) {
|
||||||
|
if (n == 1u) {
|
||||||
|
out[0] = r;
|
||||||
|
return col_gap;
|
||||||
|
}
|
||||||
|
f32 gap = col_gap;
|
||||||
|
f32 usable = r.w - gap * static_cast<f32>(n - 1u);
|
||||||
|
if (usable <= 0.0f) {
|
||||||
|
gap = 0.0f;
|
||||||
|
usable = r.w;
|
||||||
|
}
|
||||||
|
usable = std::max(0.0f, usable);
|
||||||
|
const f32 floor_device = ui_scale > 0.0f ? kMinCellWidthLogical * ui_scale
|
||||||
|
: kMinCellWidthLogical;
|
||||||
|
const f32 floor_px = std::min(floor_device, usable / static_cast<f32>(n));
|
||||||
|
const f32 extra = usable - floor_px * static_cast<f32>(n);
|
||||||
|
f32 total = 0.0f;
|
||||||
|
const bool weighted = weights.size() == n;
|
||||||
|
if (weighted) {
|
||||||
|
for (const f32 w : weights) total += w;
|
||||||
|
}
|
||||||
|
f32 x = r.x;
|
||||||
|
f32 consumed = 0.0f;
|
||||||
|
for (u32 i = 0; i < n; ++i) {
|
||||||
|
const f32 share = weighted && total > 0.0f ? weights[i] / total : 1.0f / static_cast<f32>(n);
|
||||||
|
const f32 end = i + 1u == n ? usable : consumed + floor_px + extra * share;
|
||||||
|
out[i] = Rect{x, r.y, end - consumed, r.h};
|
||||||
|
consumed = end;
|
||||||
|
x = out[i].right() + gap;
|
||||||
|
}
|
||||||
|
return gap;
|
||||||
|
}
|
||||||
|
|
||||||
/// 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
|
||||||
@@ -41,18 +91,72 @@ f64 from_link_space(f64 v, bool log) noexcept {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Figure::Figure(u32 rows) : plots_(rows) {}
|
Figure::Figure(u32 rows) : plots_(rows) {
|
||||||
|
const std::vector<u32> ones(rows, 1u);
|
||||||
Plot& Figure::plot(u32 row) noexcept {
|
build_cells(std::span<const u32>(ones));
|
||||||
return row < plots_.size() ? plots_[row] : fallback_;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Plot& Figure::plot(u32 row) const noexcept {
|
Figure::Figure(std::span<const u32> cells_per_row) : plots_(total_cells(cells_per_row)) {
|
||||||
return row < plots_.size() ? plots_[row] : fallback_;
|
build_cells(cells_per_row);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Figure::build_cells(std::span<const u32> cells) {
|
||||||
|
cells_per_row_.assign(cells.begin(), cells.end());
|
||||||
|
for (u32& c : cells_per_row_) c = std::max<u32>(c, 1u);
|
||||||
|
row_first_.assign(cells_per_row_.size() + 1u, 0u);
|
||||||
|
for (std::size_t r = 0; r < cells_per_row_.size(); ++r) {
|
||||||
|
row_first_[r + 1u] = row_first_[r] + cells_per_row_[r];
|
||||||
|
}
|
||||||
|
cell_weights_.assign(cells_per_row_.size(), {});
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 Figure::cells_in_row(u32 row) const noexcept {
|
||||||
|
return row < cells_per_row_.size() ? cells_per_row_[row] : 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 Figure::first_plot_of_row(u32 row) const noexcept {
|
||||||
|
return row < cells_per_row_.size() ? row_first_[row] : plot_count();
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 Figure::row_of_plot(u32 plot) const noexcept {
|
||||||
|
u32 r = 0;
|
||||||
|
while (r + 1u < cells_per_row_.size() && row_first_[r + 1u] <= plot) ++r;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
Plot& Figure::plot(u32 index) noexcept {
|
||||||
|
return index < plots_.size() ? plots_[index] : fallback_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Plot& Figure::plot(u32 index) const noexcept {
|
||||||
|
return index < plots_.size() ? plots_[index] : fallback_;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status Figure::set_cell_weights(u32 row, std::span<const f32> weights) {
|
||||||
|
if (row >= cells_per_row_.size() || weights.size() != cells_per_row_[row]) {
|
||||||
|
return Status{ErrorCode::InvalidArgument, "cell weights must name every cell of an existing row"};
|
||||||
|
}
|
||||||
|
for (const f32 w : weights) {
|
||||||
|
if (!std::isfinite(w) || !(w > 0.0f)) {
|
||||||
|
return Status{ErrorCode::InvalidArgument,
|
||||||
|
"cell weights must be positive finite numbers: a weight is a share of "
|
||||||
|
"the row, and a cell with none of it has no rect"};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cell_weights_[row].assign(weights.begin(), weights.end());
|
||||||
|
return Status{};
|
||||||
|
}
|
||||||
|
|
||||||
|
void Figure::clear_cell_weights(u32 row) noexcept {
|
||||||
|
if (row < cell_weights_.size()) cell_weights_[row].clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Figure::has_cell_weights(u32 row) const noexcept {
|
||||||
|
return row < cell_weights_.size() && !cell_weights_[row].empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
Status Figure::set_row_weights(std::span<const f32> weights) {
|
Status Figure::set_row_weights(std::span<const f32> weights) {
|
||||||
if (weights.size() != plots_.size()) {
|
if (weights.size() != cells_per_row_.size()) {
|
||||||
return Status{ErrorCode::InvalidArgument, "row weights must name every row"};
|
return Status{ErrorCode::InvalidArgument, "row weights must name every row"};
|
||||||
}
|
}
|
||||||
f32 total = 0.0f;
|
f32 total = 0.0f;
|
||||||
@@ -369,6 +473,8 @@ void Figure::sync_x_links() {
|
|||||||
const FigureLayout& Figure::layout() {
|
const FigureLayout& Figure::layout() {
|
||||||
const u32 rows = row_count();
|
const u32 rows = row_count();
|
||||||
layout_.rows.assign(rows, Rect{});
|
layout_.rows.assign(rows, Rect{});
|
||||||
|
layout_.cells.clear();
|
||||||
|
layout_.col_gap = 0.0f;
|
||||||
layout_.row_gap = row_gap_;
|
layout_.row_gap = row_gap_;
|
||||||
layout_.x_axis_strip = Rect{};
|
layout_.x_axis_strip = Rect{};
|
||||||
if (rows == 0u) {
|
if (rows == 0u) {
|
||||||
@@ -441,20 +547,35 @@ const FigureLayout& Figure::layout() {
|
|||||||
// short to hold the one asked for.
|
// short to hold the one asked for.
|
||||||
layout_.row_gap = gap;
|
layout_.row_gap = gap;
|
||||||
|
|
||||||
|
// Each row rect is split into its cells; a one-cell row's cell is the rect itself, so an
|
||||||
|
// unsplit figure lays out exactly as it always did.
|
||||||
|
layout_.cells.assign(plots_.size(), Rect{});
|
||||||
|
layout_.col_gap = col_gap_;
|
||||||
|
for (u32 row = 0; row < rows; ++row) {
|
||||||
|
const u32 n = cells_per_row_[row];
|
||||||
|
const f32 used = place_cells(layout_.rows[row], n, cell_weights_[row], col_gap_,
|
||||||
|
floor_scale, &layout_.cells[row_first_[row]]);
|
||||||
|
if (n > 1u && used < col_gap_) layout_.col_gap = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
const u32 bottom_row = rows - 1u;
|
const u32 bottom_row = rows - 1u;
|
||||||
for (u32 row = 0; row < rows; ++row) {
|
for (u32 row = 0; row < rows; ++row) {
|
||||||
Plot& p = plots_[row];
|
// One x axis for the whole figure, and it is the bottom row's: every plot keeps its
|
||||||
p.set_viewport(layout_.rows[row]);
|
|
||||||
// One x axis for the whole figure, and it is the bottom row's: every row keeps its
|
|
||||||
// x chrome -- labels, title and the strip the controller's axis zoom is found in --
|
// x chrome -- labels, title and the strip the controller's axis zoom is found in --
|
||||||
// and every other row gives all of it to that one, so the labels and the title are
|
// and every plot above the bottom row gives all of it to that row, so the labels and
|
||||||
// not drawn once per row. A figure told to label every row keeps them everywhere,
|
// the title are not drawn once per row. A figure told to label every row keeps them
|
||||||
// and then there is no shared axis to speak of: each row has its own.
|
// everywhere, and then there is no shared axis to speak of: each plot has its own. A
|
||||||
p.set_x_strip_mode(labels_bottom_only_ && row != bottom_row ? XStrip::None : XStrip::Both);
|
// split bottom row labels each of its cells, each against its own x axis.
|
||||||
// Before either pass below, and the same value for every row: nothing is measured on
|
const XStrip strip = labels_bottom_only_ && row != bottom_row ? XStrip::None : XStrip::Both;
|
||||||
// the right, so unlike the left gutter this needs no measurement pass -- the rows end
|
for (u32 i = row_first_[row]; i < row_first_[row + 1u]; ++i) {
|
||||||
// on a common column because they are all told the one width.
|
Plot& p = plots_[i];
|
||||||
p.set_right_gutter(right_gutter_);
|
p.set_viewport(layout_.cells[i]);
|
||||||
|
p.set_x_strip_mode(strip);
|
||||||
|
// 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_);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 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
|
||||||
// its transforms describe this frame's rects; and before the rows lay themselves
|
// its transforms describe this frame's rects; and before the rows lay themselves
|
||||||
@@ -486,18 +607,21 @@ const FigureLayout& Figure::layout() {
|
|||||||
// `set_min_y_gutter()` seeds the maximum rather than being imposed on a row before the
|
// `set_min_y_gutter()` seeds the maximum rather than being imposed on a row before the
|
||||||
// measurement, so a reserved floor cannot widen what a row reports and then be widened
|
// measurement, so a reserved floor cannot widen what a row reports and then be widened
|
||||||
// by its own reservation on the next frame.
|
// by its own reservation on the next frame.
|
||||||
|
//
|
||||||
|
// 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_;
|
f32 gutter = min_y_gutter_;
|
||||||
for (u32 row = 0; row < rows; ++row) {
|
for (Plot& p : plots_) {
|
||||||
plots_[row].set_min_y_gutter(0.0f);
|
p.set_min_y_gutter(0.0f);
|
||||||
const PlotLayout& row_layout = plots_[row].layout();
|
const PlotLayout& row_layout = p.layout();
|
||||||
gutter = std::max(gutter, row_layout.y_axis_width + row_layout.y_title_width);
|
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
|
// 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
|
||||||
// also what each would have done alone.
|
// also what each would have done alone.
|
||||||
for (u32 row = 0; row < rows; ++row) {
|
for (Plot& p : plots_) {
|
||||||
plots_[row].set_min_y_gutter(gutter);
|
p.set_min_y_gutter(gutter);
|
||||||
plots_[row].layout();
|
p.layout();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The figure's one x axis is the bottom row's, so the strip it is drawn in is that
|
// The figure's one x axis is the bottom row's, so the strip it is drawn in is that
|
||||||
|
|||||||
@@ -471,7 +471,7 @@ void Renderer::draw(Figure& figure) {
|
|||||||
// Every row is prepared before the figure lays itself out, because a row's strips
|
// Every row is prepared before the figure lays itself out, because a row's strips
|
||||||
// are measured from the metrics this installs: a row measuring with one backend and
|
// are measured from the metrics this installs: a row measuring with one backend and
|
||||||
// drawing with another is the defect the propagation point exists to prevent.
|
// drawing with another is the defect the propagation point exists to prevent.
|
||||||
const u32 rows = figure.row_count();
|
const u32 rows = figure.plot_count();
|
||||||
for (u32 row = 0; row < rows; ++row) prepare_plot(figure.plot(row));
|
for (u32 row = 0; row < rows; ++row) prepare_plot(figure.plot(row));
|
||||||
|
|
||||||
// The figure's one layout for the frame. It hands every row its rect and its
|
// The figure's one layout for the frame. It hands every row its rect and its
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include <plot/render/embedded_mono_font.hpp>
|
#include <plot/render/embedded_mono_font.hpp>
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <span>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -1141,3 +1142,136 @@ PLOT_TEST(figure_crosshair_draws_a_horizontal_line_only_in_the_hovered_row) {
|
|||||||
PLOT_CHECK(figure.plot(1u).layout().crosshair.vertical.w > 0.0f);
|
PLOT_CHECK(figure.plot(1u).layout().crosshair.vertical.w > 0.0f);
|
||||||
PLOT_CHECK(figure.plot(1u).layout().crosshair.horizontal.h == 0.0f);
|
PLOT_CHECK(figure.plot(1u).layout().crosshair.horizontal.h == 0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ cell grid
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
/// A figure whose rows hold `cells[r]` plots each (the flattened plots in row-major
|
||||||
|
/// order), with an x and a y axis on every plot unless `axes` is off.
|
||||||
|
plot::Figure split_figure(const std::vector<plot::u32>& cells, bool axes = true) {
|
||||||
|
plot::Figure figure{std::span<const plot::u32>(cells)}; // braces: parentheses would be a vexing parse
|
||||||
|
figure.set_viewport(whole_figure());
|
||||||
|
if (axes) {
|
||||||
|
for (plot::u32 i = 0; i < figure.plot_count(); ++i) {
|
||||||
|
figure.plot(i).add_x_axis({"t", {0.0, 100.0}, false, plot::AxisPlace::Bottom, false});
|
||||||
|
figure.plot(i).add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return figure;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
PLOT_TEST(figure_cells_all_ones_equals_rows) {
|
||||||
|
plot::Figure split = split_figure({1, 1, 1});
|
||||||
|
plot::Figure plain = figure_with_axes(3u);
|
||||||
|
const plot::FigureLayout& sl = split.layout();
|
||||||
|
const plot::FigureLayout& pl = plain.layout();
|
||||||
|
PLOT_CHECK(split.row_count() == 3u && split.plot_count() == 3u);
|
||||||
|
PLOT_CHECK(sl.cells.size() == 3u);
|
||||||
|
for (plot::u32 i = 0; i < 3u; ++i) {
|
||||||
|
PLOT_CHECK_NEAR(sl.cells[i].y, pl.rows[i].y, 0.01);
|
||||||
|
PLOT_CHECK_NEAR(sl.cells[i].h, pl.rows[i].h, 0.01);
|
||||||
|
PLOT_CHECK_NEAR(sl.cells[i].w, pl.rows[i].w, 0.01);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(figure_cells_tile_exactly) {
|
||||||
|
plot::Figure figure = split_figure({1, 2, 1});
|
||||||
|
figure.set_col_gap(10.0f);
|
||||||
|
const plot::FigureLayout& fl = figure.layout();
|
||||||
|
PLOT_CHECK(figure.row_count() == 3u && figure.plot_count() == 4u);
|
||||||
|
PLOT_CHECK(figure.cells_in_row(1u) == 2u);
|
||||||
|
PLOT_CHECK(figure.first_plot_of_row(1u) == 1u && figure.first_plot_of_row(3u) == 4u);
|
||||||
|
PLOT_CHECK(figure.row_of_plot(2u) == 1u);
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[2].x - fl.cells[1].right(), 10.0, 0.01);
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[2].right(), fl.rows[1].right(), 0.001);
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[1].x, fl.rows[1].x, 0.001);
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[1].y, fl.cells[2].y, 0.001);
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[1].h, fl.cells[2].h, 0.001);
|
||||||
|
PLOT_CHECK_NEAR(fl.cells[1].w, fl.cells[2].w, 0.01);
|
||||||
|
PLOT_CHECK_NEAR(fl.col_gap, 10.0, 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(figure_cell_weights_and_floor) {
|
||||||
|
plot::Figure figure = split_figure({2});
|
||||||
|
figure.set_col_gap(0.0f);
|
||||||
|
const std::vector<plot::f32> w{3.0f, 1.0f};
|
||||||
|
PLOT_CHECK(figure.set_cell_weights(0u, w));
|
||||||
|
PLOT_CHECK(figure.has_cell_weights(0u));
|
||||||
|
{
|
||||||
|
const auto& c = figure.layout().cells;
|
||||||
|
PLOT_CHECK_NEAR((c[0].w - 64.0), (c[1].w - 64.0) * 3.0, 0.01);
|
||||||
|
}
|
||||||
|
const std::vector<plot::f32> tiny{1000.0f, 0.001f};
|
||||||
|
PLOT_CHECK(figure.set_cell_weights(0u, tiny));
|
||||||
|
PLOT_CHECK(figure.layout().cells[1].w >= 64.0f - 0.001f);
|
||||||
|
const std::vector<plot::f32> three{1.0f, 1.0f, 1.0f};
|
||||||
|
const std::vector<plot::f32> zero{1.0f, 0.0f};
|
||||||
|
PLOT_CHECK(!figure.set_cell_weights(0u, three));
|
||||||
|
PLOT_CHECK(!figure.set_cell_weights(0u, zero));
|
||||||
|
PLOT_CHECK(!figure.set_cell_weights(5u, w));
|
||||||
|
figure.clear_cell_weights(0u);
|
||||||
|
PLOT_CHECK(!figure.has_cell_weights(0u));
|
||||||
|
const auto& c = figure.layout().cells;
|
||||||
|
PLOT_CHECK_NEAR(c[0].w, c[1].w, 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(figure_cells_narrow_degrades) {
|
||||||
|
plot::Figure figure = split_figure({4}, false);
|
||||||
|
figure.set_viewport(plot::Rect{0.0f, 0.0f, 20.0f, 100.0f});
|
||||||
|
for (plot::u32 i = 0; i < 4u; ++i) {
|
||||||
|
figure.plot(i).add_x_axis({"t", {0.0, 100.0}, false, plot::AxisPlace::Bottom, false});
|
||||||
|
figure.plot(i).add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
||||||
|
}
|
||||||
|
figure.set_col_gap(10.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);
|
||||||
|
PLOT_CHECK_NEAR(c.h, 100.0, 0.01);
|
||||||
|
sum += c.w;
|
||||||
|
}
|
||||||
|
PLOT_CHECK_NEAR(sum, 20.0, 0.01);
|
||||||
|
PLOT_CHECK_NEAR(fl.col_gap, 0.0, 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(figure_cells_share_x_link_and_crosshair) {
|
||||||
|
plot::Figure figure = split_figure({2, 1});
|
||||||
|
const plot::u32 group = figure.add_x_link();
|
||||||
|
for (plot::u32 i = 0; i < 3u; ++i) PLOT_CHECK(figure.link_x(i, group));
|
||||||
|
figure.plot(1u).set_x_range(0u, {3.0, 9.0});
|
||||||
|
figure.layout();
|
||||||
|
for (plot::u32 i = 0; i < 3u; ++i) {
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(i).x_range(0u).min, 3.0, 1e-9);
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(i).x_range(0u).max, 9.0, 1e-9);
|
||||||
|
}
|
||||||
|
figure.set_crosshair(4.0, true);
|
||||||
|
for (plot::u32 i = 0; i < 3u; ++i) {
|
||||||
|
PLOT_CHECK(figure.plot(i).crosshair().active);
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(i).crosshair().x, 4.0, 1e-9);
|
||||||
|
}
|
||||||
|
figure.clear_crosshair();
|
||||||
|
for (plot::u32 i = 0; i < 3u; ++i) PLOT_CHECK(!figure.plot(i).crosshair().active);
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(figure_cells_x_labels_bottom_row_only) {
|
||||||
|
plot::Figure figure = split_figure({1, 2});
|
||||||
|
const plot::FigureLayout& fl = figure.layout();
|
||||||
|
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(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_TEST(figure_cells_common_y_gutter) {
|
||||||
|
plot::Figure figure = split_figure({2}, false);
|
||||||
|
give_magnitude_row(figure.plot(0u), 0u);
|
||||||
|
give_magnitude_row(figure.plot(1u), 1u);
|
||||||
|
const plot::FigureLayout& fl = figure.layout();
|
||||||
|
const double g0 = figure.plot(0u).layout().axes.x - fl.cells[0].x;
|
||||||
|
const double g1 = figure.plot(1u).layout().axes.x - fl.cells[1].x;
|
||||||
|
PLOT_CHECK(g0 > 0.0);
|
||||||
|
PLOT_CHECK_NEAR(g0, g1, 0.01);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user