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
+148
-24
@@ -23,6 +23,56 @@ namespace {
|
||||
/// wants a taller floor gives the figure a taller viewport, or gives the rows weights.
|
||||
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
|
||||
/// 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
|
||||
@@ -41,18 +91,72 @@ f64 from_link_space(f64 v, bool log) noexcept {
|
||||
|
||||
} // namespace
|
||||
|
||||
Figure::Figure(u32 rows) : plots_(rows) {}
|
||||
|
||||
Plot& Figure::plot(u32 row) noexcept {
|
||||
return row < plots_.size() ? plots_[row] : fallback_;
|
||||
Figure::Figure(u32 rows) : plots_(rows) {
|
||||
const std::vector<u32> ones(rows, 1u);
|
||||
build_cells(std::span<const u32>(ones));
|
||||
}
|
||||
|
||||
const Plot& Figure::plot(u32 row) const noexcept {
|
||||
return row < plots_.size() ? plots_[row] : fallback_;
|
||||
Figure::Figure(std::span<const u32> cells_per_row) : plots_(total_cells(cells_per_row)) {
|
||||
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) {
|
||||
if (weights.size() != plots_.size()) {
|
||||
if (weights.size() != cells_per_row_.size()) {
|
||||
return Status{ErrorCode::InvalidArgument, "row weights must name every row"};
|
||||
}
|
||||
f32 total = 0.0f;
|
||||
@@ -369,6 +473,8 @@ void Figure::sync_x_links() {
|
||||
const FigureLayout& Figure::layout() {
|
||||
const u32 rows = row_count();
|
||||
layout_.rows.assign(rows, Rect{});
|
||||
layout_.cells.clear();
|
||||
layout_.col_gap = 0.0f;
|
||||
layout_.row_gap = row_gap_;
|
||||
layout_.x_axis_strip = Rect{};
|
||||
if (rows == 0u) {
|
||||
@@ -441,20 +547,35 @@ const FigureLayout& Figure::layout() {
|
||||
// short to hold the one asked for.
|
||||
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;
|
||||
for (u32 row = 0; row < rows; ++row) {
|
||||
Plot& p = plots_[row];
|
||||
p.set_viewport(layout_.rows[row]);
|
||||
// One x axis for the whole figure, and it is the bottom row's: every row keeps its
|
||||
// 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 other row gives all of it to that one, 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 row has its own.
|
||||
p.set_x_strip_mode(labels_bottom_only_ && row != bottom_row ? XStrip::None : XStrip::Both);
|
||||
// Before either pass below, and the same value for every row: nothing is measured on
|
||||
// the right, so unlike the left gutter this needs no measurement pass -- the rows end
|
||||
// on a common column because they are all told the one width.
|
||||
p.set_right_gutter(right_gutter_);
|
||||
// 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;
|
||||
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);
|
||||
// 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
|
||||
// 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
|
||||
// measurement, so a reserved floor cannot widen what a row reports and then be widened
|
||||
// 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_;
|
||||
for (u32 row = 0; row < rows; ++row) {
|
||||
plots_[row].set_min_y_gutter(0.0f);
|
||||
const PlotLayout& row_layout = plots_[row].layout();
|
||||
for (Plot& p : plots_) {
|
||||
p.set_min_y_gutter(0.0f);
|
||||
const PlotLayout& row_layout = p.layout();
|
||||
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
|
||||
// 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 (u32 row = 0; row < rows; ++row) {
|
||||
plots_[row].set_min_y_gutter(gutter);
|
||||
plots_[row].layout();
|
||||
for (Plot& p : plots_) {
|
||||
p.set_min_y_gutter(gutter);
|
||||
p.layout();
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
const u32 rows = figure.row_count();
|
||||
const u32 rows = figure.plot_count();
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user