figure: cells of a row can share one y range
This commit is contained in:
1 parent
73f610b136
commit
25c3381551
6 files changed
+214
-7
No files matched your search
@@ -102,12 +102,16 @@ struct Axis {
|
|||||||
/// group stops following or the row leaves it.
|
/// group stops following or the row leaves it.
|
||||||
bool live_owner = false;
|
bool live_owner = false;
|
||||||
f64 live_owner_newest = 0.0;
|
f64 live_owner_newest = 0.0;
|
||||||
/// Set while this plot is a member of a `Figure` x link group (`Figure::link_x()`
|
/// Set while this axis is a member of a `Figure` link group. On an x axis that is
|
||||||
/// writes it, `unlink_row()` clears it). The group owns the shared x window *and*
|
/// an x link group (`Figure::link_x()` writes it, `unlink_row()` clears it): the
|
||||||
/// its follow state -- `Figure::link_x()` refuses a `Live` member joiner -- so
|
/// group owns the shared x window *and* its follow state -- `Figure::link_x()`
|
||||||
/// `Plot::set_x_follow(Live)` on this axis is refused after linking too, rather
|
/// refuses a `Live` member joiner -- so `Plot::set_x_follow(Live)` on this axis is
|
||||||
/// than letting a later call put the member's axis back into a state the group
|
/// refused after linking too, rather than letting a later call put the member's
|
||||||
/// cannot drive (see `Plot::set_x_follow()`).
|
/// axis back into a state the group cannot drive (see `Plot::set_x_follow()`).
|
||||||
|
/// On a y axis it is the row's shared-y group (`Figure::set_share_row_y()`): the
|
||||||
|
/// group, not the plot, owns the range *and* the auto scale state, so
|
||||||
|
/// `Plot::autoscale_axes()` skips the axis and the figure fits the union of the
|
||||||
|
/// members' traces instead.
|
||||||
bool link_member = false;
|
bool link_member = false;
|
||||||
/// Ticks for the last `refresh()`, ascending, majors and (when asked) minors.
|
/// Ticks for the last `refresh()`, ascending, majors and (when asked) minors.
|
||||||
std::vector<Tick> ticks;
|
std::vector<Tick> ticks;
|
||||||
|
|||||||
@@ -314,6 +314,22 @@ class Figure {
|
|||||||
/// A group's follow state. `Off` for group 0 and for a group that does not exist.
|
/// A group's follow state. `Off` for group 0 and for a group that does not exist.
|
||||||
XFollow x_link_follow(u32 group) const noexcept;
|
XFollow x_link_follow(u32 group) const noexcept;
|
||||||
|
|
||||||
|
/// Makes the cells of every split row (two or more) share their y axis 0: one range
|
||||||
|
/// and one auto scale state per row, default off. A range set on any member -- a
|
||||||
|
/// wheel, a drag, `Plot::set_y_range()` -- becomes the row's and is pushed to all of
|
||||||
|
/// its cells at the next `layout()`; the row is auto or manual as one, and while it is
|
||||||
|
/// auto it fits the union of every cell's visible traces (a cell with none is ignored;
|
||||||
|
/// padding and log handling are the plot's own, via `Plot::fit_y_bounds()`). The group,
|
||||||
|
/// not the plot, owns auto scale: members' own autoscale is suppressed, because each
|
||||||
|
/// would fit only its own traces and the cells would fight over the range. Turning it
|
||||||
|
/// on or off forgets the groups' state; a row of one cell is never a group.
|
||||||
|
void set_share_row_y(bool on);
|
||||||
|
bool share_row_y() const noexcept { return share_row_y_; }
|
||||||
|
/// Whether figure row `row` is auto scaled as a group. True for a row that is not a
|
||||||
|
/// group -- fewer than two cells, sharing off, or a row that does not exist -- so
|
||||||
|
/// "auto" is also the answer when there is no shared state to be manual.
|
||||||
|
bool row_y_auto(u32 row) const noexcept;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/// One link group: the range its members share, the scroll state behind that
|
/// One link group: the range its members share, the scroll state behind that
|
||||||
/// range while it follows, the rows in it (ascending) and, in step with them, the
|
/// range while it follows, the rows in it (ascending) and, in step with them, the
|
||||||
@@ -370,6 +386,21 @@ 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();
|
||||||
|
|
||||||
|
/// A split row's shared y axis: the range last pushed to its cells (the value the
|
||||||
|
/// next frame compares each cell against, so a gesture is told from the group's own
|
||||||
|
/// write -- the same device as `XLink::pushed`), whether the group has been
|
||||||
|
/// initialised from its first cell, and whether it is auto scaled.
|
||||||
|
struct RowY {
|
||||||
|
Range pushed{};
|
||||||
|
bool have = false;
|
||||||
|
bool auto_scale = true;
|
||||||
|
};
|
||||||
|
/// Advances every split row's y group for this frame: adopt what a cell was changed
|
||||||
|
/// to (a range or an auto flag), fit the union of the cells' traces while the group
|
||||||
|
/// is auto, push the result to all cells. Runs before any `Plot::layout()`, which is
|
||||||
|
/// where a plot would otherwise autoscale on its own.
|
||||||
|
void sync_row_y();
|
||||||
|
|
||||||
/// Fills the row bookkeeping (`cells_per_row_`, `row_first_`, `cell_weights_`) from a
|
/// 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
|
/// per-row cell count, clamping each to at least one. Does not touch `plots_`, which the
|
||||||
/// constructors size from `total_cells()`.
|
/// constructors size from `total_cells()`.
|
||||||
@@ -381,6 +412,8 @@ class Figure {
|
|||||||
/// index of row r's first cell and the last entry the plot count.
|
/// index of row r's first cell and the last entry the plot count.
|
||||||
std::vector<u32> row_first_;
|
std::vector<u32> row_first_;
|
||||||
std::vector<std::vector<f32>> cell_weights_;
|
std::vector<std::vector<f32>> cell_weights_;
|
||||||
|
bool share_row_y_ = false;
|
||||||
|
std::vector<RowY> row_y_;
|
||||||
f32 col_gap_ = 0.0f;
|
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
|
||||||
|
|||||||
@@ -435,6 +435,20 @@ class Plot {
|
|||||||
/// does not exist. Not for callers outside the figure: a `Plot` that is not in a
|
/// does not exist. Not for callers outside the figure: a `Plot` that is not in a
|
||||||
/// figure never has a linked axis.
|
/// figure never has a linked axis.
|
||||||
void set_x_link_member(u32 axis, bool linked) noexcept;
|
void set_x_link_member(u32 axis, bool linked) noexcept;
|
||||||
|
/// The y-side twin of `set_x_link_member()`: publishes that this y axis is a member
|
||||||
|
/// of its row's shared-y group (`Figure::set_share_row_y()`). A linked y axis is
|
||||||
|
/// never fitted by `layout()` -- the group owns its range and auto scale, and fits
|
||||||
|
/// the union of every member's traces -- but a gesture or `set_y_range()` still
|
||||||
|
/// writes it, which is how the group learns of a change. A no-op for an axis that
|
||||||
|
/// does not exist. Not for callers outside the figure.
|
||||||
|
void set_y_link_member(u32 axis, bool linked) noexcept;
|
||||||
|
/// Union of the bounds of the visible traces bound to y axis `axis`: what a shared-y
|
||||||
|
/// group collects from each member. False -- and `out` untouched -- when none.
|
||||||
|
bool y_data_bounds(u32 axis, Range& out) const;
|
||||||
|
/// Fits y axis `axis` to `bounds` the way `auto_fit_y()` fits it to its own traces
|
||||||
|
/// (same padding, same log handling), leaving `auto_scale` alone. False -- range
|
||||||
|
/// untouched -- when the axis does not exist or cannot be fitted to `bounds`.
|
||||||
|
bool fit_y_bounds(u32 axis, Range bounds);
|
||||||
|
|
||||||
/// Data -> pixels for one axis, built from the axis' range, log flag and the
|
/// Data -> pixels for one axis, built from the axis' range, log flag and the
|
||||||
/// current layout. Always invertible: a collapsed range or a collapsed axes
|
/// current layout. Always invertible: a collapsed range or a collapsed axes
|
||||||
|
|||||||
@@ -434,6 +434,80 @@ bool Figure::adopt_member_changes(XLink& group) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Figure::set_share_row_y(bool on) {
|
||||||
|
share_row_y_ = on;
|
||||||
|
row_y_.clear();
|
||||||
|
for (u32 row = 0; row < cells_per_row_.size(); ++row) {
|
||||||
|
if (cells_per_row_[row] < 2u) continue;
|
||||||
|
for (u32 i = row_first_[row]; i < row_first_[row] + cells_per_row_[row]; ++i) {
|
||||||
|
plots_[i].set_y_link_member(0u, on);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Figure::row_y_auto(u32 row) const noexcept {
|
||||||
|
return row < row_y_.size() && row_y_[row].have ? row_y_[row].auto_scale : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Figure::sync_row_y() {
|
||||||
|
if (!share_row_y_) return;
|
||||||
|
row_y_.resize(cells_per_row_.size());
|
||||||
|
for (u32 row = 0; row < cells_per_row_.size(); ++row) {
|
||||||
|
const u32 n = cells_per_row_[row];
|
||||||
|
if (n < 2u) continue;
|
||||||
|
RowY& g = row_y_[row];
|
||||||
|
const u32 first = row_first_[row];
|
||||||
|
// Axes added after set_share_row_y() are covered here.
|
||||||
|
for (u32 i = first; i < first + n; ++i) plots_[i].set_y_link_member(0u, true);
|
||||||
|
if (!g.have) {
|
||||||
|
g.pushed = plots_[first].y_axis(0u).range;
|
||||||
|
g.auto_scale = plots_[first].y_axis(0u).auto_scale;
|
||||||
|
}
|
||||||
|
// What happened since the last push. A member's auto flag that differs from the group's
|
||||||
|
// is a request (set_y_range clears it, auto_fit_y / set_y_auto_scale(true) sets it);
|
||||||
|
// otherwise a member whose range differs from the pushed one was moved by a gesture.
|
||||||
|
bool want_auto = g.auto_scale;
|
||||||
|
Range target = g.pushed;
|
||||||
|
bool flag_request = false;
|
||||||
|
for (u32 i = first; i < first + n && !flag_request; ++i) {
|
||||||
|
const Axis& a = plots_[i].y_axis(0u);
|
||||||
|
if (a.auto_scale != g.auto_scale) {
|
||||||
|
want_auto = a.auto_scale;
|
||||||
|
if (!want_auto) target = a.range; // auto -> manual: the gesture's range
|
||||||
|
flag_request = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!flag_request && !want_auto) {
|
||||||
|
for (u32 i = first; i < first + n; ++i) {
|
||||||
|
const Range r = plots_[i].y_axis(0u).range;
|
||||||
|
if (r.min != g.pushed.min || r.max != g.pushed.max) {
|
||||||
|
target = r;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (want_auto) {
|
||||||
|
Range bounds{};
|
||||||
|
bool any = false;
|
||||||
|
for (u32 i = first; i < first + n; ++i) {
|
||||||
|
Range b{};
|
||||||
|
if (!plots_[i].y_data_bounds(0u, b)) continue;
|
||||||
|
bounds = any ? Range{std::min(bounds.min, b.min), std::max(bounds.max, b.max)} : b;
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
if (any && plots_[first].fit_y_bounds(0u, bounds)) target = plots_[first].y_axis(0u).range;
|
||||||
|
// else: nothing to fit -- keep the range the group had
|
||||||
|
}
|
||||||
|
for (u32 i = first; i < first + n; ++i) {
|
||||||
|
plots_[i].set_y_range(0u, target); // clears the flag ...
|
||||||
|
plots_[i].set_y_auto_scale(0u, want_auto); // ... so restate it: every member mirrors the group
|
||||||
|
}
|
||||||
|
g.pushed = target;
|
||||||
|
g.auto_scale = want_auto;
|
||||||
|
g.have = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Figure::sync_x_links() {
|
void Figure::sync_x_links() {
|
||||||
for (XLink& group : links_) {
|
for (XLink& group : links_) {
|
||||||
if (group.rows.empty()) continue;
|
if (group.rows.empty()) continue;
|
||||||
@@ -581,6 +655,8 @@ const FigureLayout& Figure::layout() {
|
|||||||
// 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
|
||||||
// out below, so a following group's window is the range they lay out with.
|
// out below, so a following group's window is the range they lay out with.
|
||||||
sync_x_links();
|
sync_x_links();
|
||||||
|
// Before any `Plot::layout()`, where a plot would autoscale its own y axis.
|
||||||
|
sync_row_y();
|
||||||
// Every row is laid out against its own rect, so after this call each row's
|
// Every row is laid out against its own rect, so after this call each row's
|
||||||
// chrome is current: `plot(i).layout()` reads back what the figure computed rather
|
// chrome is current: `plot(i).layout()` reads back what the figure computed rather
|
||||||
// than what the row last held. That is the contract `Renderer::draw(Figure&)`
|
// than what the row last held. That is the contract `Renderer::draw(Figure&)`
|
||||||
|
|||||||
+15
-1
@@ -819,6 +819,19 @@ void Plot::set_x_link_member(u32 axis, bool linked) noexcept {
|
|||||||
x_axes_[axis].link_member = linked;
|
x_axes_[axis].link_member = linked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Plot::set_y_link_member(u32 axis, bool linked) noexcept {
|
||||||
|
if (axis >= y_axes_.size()) return;
|
||||||
|
y_axes_[axis].link_member = linked;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Plot::y_data_bounds(u32 axis, Range& out) const {
|
||||||
|
return trace_bounds_union(/*x_side=*/false, axis, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Plot::fit_y_bounds(u32 axis, Range bounds) {
|
||||||
|
return axis < y_axes_.size() && fit_axis_range(y_axes_[axis], bounds);
|
||||||
|
}
|
||||||
|
|
||||||
XFollow Plot::x_follow(u32 axis) const {
|
XFollow Plot::x_follow(u32 axis) const {
|
||||||
return axis < x_axes_.size() ? x_axes_[axis].follow : XFollow::Off;
|
return axis < x_axes_.size() ? x_axes_[axis].follow : XFollow::Off;
|
||||||
}
|
}
|
||||||
@@ -1001,7 +1014,8 @@ void Plot::autoscale_axes() {
|
|||||||
if (x_axes_[i].auto_scale && !follows_live(i)) fit_x_axis(i);
|
if (x_axes_[i].auto_scale && !follows_live(i)) fit_x_axis(i);
|
||||||
}
|
}
|
||||||
for (u32 i = 0; i < y_axes_.size(); ++i) {
|
for (u32 i = 0; i < y_axes_.size(); ++i) {
|
||||||
if (y_axes_[i].auto_scale) fit_y_axis(i);
|
// A shared-y member is fitted by its figure, against every cell's traces.
|
||||||
|
if (y_axes_[i].auto_scale && !y_axes_[i].link_member) fit_y_axis(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1275,3 +1275,69 @@ PLOT_TEST(figure_cells_common_y_gutter) {
|
|||||||
PLOT_CHECK(g0 > 0.0);
|
PLOT_CHECK(g0 > 0.0);
|
||||||
PLOT_CHECK_NEAR(g0, g1, 0.01);
|
PLOT_CHECK_NEAR(g0, g1, 0.01);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(figure_row_y_cells_share_a_range_after_a_gesture) {
|
||||||
|
plot::Figure figure = split_figure({2, 1});
|
||||||
|
figure.set_share_row_y(true);
|
||||||
|
figure.layout();
|
||||||
|
figure.plot(1u).set_y_range(0u, {-3.0, 7.0}); // what a wheel / drag / pan ends in
|
||||||
|
figure.layout();
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(0u).y_range(0u).min, -3.0, 1e-9);
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(0u).y_range(0u).max, 7.0, 1e-9);
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(1u).y_range(0u).min, -3.0, 1e-9);
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(1u).y_range(0u).max, 7.0, 1e-9);
|
||||||
|
// The unsplit row is its own thing.
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(2u).y_range(0u).min, -1.0, 1e-9);
|
||||||
|
PLOT_CHECK(!figure.row_y_auto(0u));
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(figure_row_y_auto_fits_the_union_and_ignores_empty_cells) {
|
||||||
|
plot::Figure figure = split_figure({3});
|
||||||
|
for (plot::u32 i = 0; i < 3u; ++i) figure.plot(i).set_y_auto_scale(0u, true);
|
||||||
|
plot::data::ArraySeries a(std::vector<plot::f32>{0.0f, 10.0f, 5.0f}, 0.0, 1.0);
|
||||||
|
plot::data::ArraySeries b(std::vector<plot::f32>{-20.0f, 5.0f, 0.0f}, 0.0, 1.0);
|
||||||
|
figure.plot(0u).add_trace({&a, plot::Color::hex(0x40C0FF), 1.5f, 0u, 0u});
|
||||||
|
figure.plot(1u).add_trace({&b, plot::Color::hex(0xFF8080), 1.5f, 0u, 0u});
|
||||||
|
figure.set_share_row_y(true); // cell 2 has no trace
|
||||||
|
figure.layout();
|
||||||
|
PLOT_CHECK(figure.row_y_auto(0u));
|
||||||
|
const plot::Range r0 = figure.plot(0u).y_range(0u);
|
||||||
|
PLOT_CHECK(r0.min <= -20.0 && r0.max >= 10.0);
|
||||||
|
PLOT_CHECK(r0.max - r0.min < 40.0); // padded, not blown up
|
||||||
|
for (plot::u32 i = 1; i < 3u; ++i) {
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(i).y_range(0u).min, r0.min, 1e-9);
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(i).y_range(0u).max, r0.max, 1e-9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(figure_row_y_manual_then_fit_returns_to_auto) {
|
||||||
|
plot::Figure figure = split_figure({2});
|
||||||
|
for (plot::u32 i = 0; i < 2u; ++i) figure.plot(i).set_y_auto_scale(0u, true);
|
||||||
|
plot::data::ArraySeries a(std::vector<plot::f32>{0.0f, 10.0f}, 0.0, 1.0);
|
||||||
|
plot::data::ArraySeries b(std::vector<plot::f32>{-4.0f, 4.0f}, 0.0, 1.0);
|
||||||
|
figure.plot(0u).add_trace({&a, plot::Color::hex(0x40C0FF), 1.5f, 0u, 0u});
|
||||||
|
figure.plot(1u).add_trace({&b, plot::Color::hex(0xFF8080), 1.5f, 0u, 0u});
|
||||||
|
figure.set_share_row_y(true);
|
||||||
|
figure.layout();
|
||||||
|
figure.plot(1u).set_y_range(0u, {1.0, 2.0});
|
||||||
|
figure.layout();
|
||||||
|
PLOT_CHECK(!figure.row_y_auto(0u));
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(0u).y_range(0u).min, 1.0, 1e-9);
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(0u).y_range(0u).max, 2.0, 1e-9);
|
||||||
|
figure.layout(); // manual holds: no fight with the plots' own autoscale
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(1u).y_range(0u).max, 2.0, 1e-9);
|
||||||
|
figure.plot(0u).auto_fit_y(0u); // what fit-all / double-click does to one cell
|
||||||
|
figure.layout();
|
||||||
|
PLOT_CHECK(figure.row_y_auto(0u));
|
||||||
|
PLOT_CHECK(figure.plot(1u).y_range(0u).min <= -4.0 && figure.plot(1u).y_range(0u).max >= 10.0);
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(0u).y_range(0u).min, figure.plot(1u).y_range(0u).min, 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
PLOT_TEST(figure_row_y_sharing_off_keeps_cells_independent) {
|
||||||
|
plot::Figure figure = split_figure({2});
|
||||||
|
figure.layout();
|
||||||
|
figure.plot(1u).set_y_range(0u, {-3.0, 7.0});
|
||||||
|
figure.layout();
|
||||||
|
PLOT_CHECK_NEAR(figure.plot(0u).y_range(0u).max, 1.0, 1e-9);
|
||||||
|
PLOT_CHECK(!figure.share_row_y());
|
||||||
|
}
|
||||||
Reference in new issue
Block a user