fix(plot): a figure gives every row the same y gutter so the axes line up
This commit is contained in:
1 parent
663b57c679
commit
cae672bc99
6 files changed
+348
-5
No files matched your search
@@ -43,6 +43,42 @@ std::vector<plot::Rect> rows_of(plot::Figure& figure) {
|
||||
return figure.layout().rows;
|
||||
}
|
||||
|
||||
/// Gives one plot the axes of the misalignment cases: a fixed x range and a fixed y
|
||||
/// range from `give_magnitude_row()`'s table. Fixed (`auto_scale` off), so the labels a row
|
||||
/// measures are the ones its magnitude produces whatever order the rows are laid out
|
||||
/// in -- which is what makes two rows' strips comparable.
|
||||
void give_magnitude_row(plot::Plot& p, plot::u32 which) {
|
||||
static plot::render::EmbeddedMonoFont font;
|
||||
const plot::Range y_ranges[3] = {{0.0, 671.0e6}, {0.0, 90.0}, {0.0, 2.51}};
|
||||
p.set_text_backend(&font);
|
||||
p.add_x_axis({"t", {0.0, 100.0}, false, plot::AxisPlace::Bottom, false});
|
||||
p.add_y_axis({"v", y_ranges[which % 3u], false, plot::AxisPlace::Left, false});
|
||||
}
|
||||
|
||||
/// A figure whose rows carry deliberately different magnitudes: the "671m"-scale
|
||||
/// row's tick labels are four characters wide, the "90" row's two and the "2.51"
|
||||
/// row's three. That is the shape the reported misalignment had -- each row sized its
|
||||
/// own y strip from its own widest label, so the rows' axes started at three different
|
||||
/// x.
|
||||
plot::Figure different_magnitude_figure(plot::u32 rows = 3u) {
|
||||
plot::Figure figure(rows);
|
||||
figure.set_viewport(whole_figure());
|
||||
for (plot::u32 row = 0; row < rows; ++row) give_magnitude_row(figure.plot(row), row);
|
||||
return figure;
|
||||
}
|
||||
|
||||
/// The left gutter `which`'s row would reserve for itself if it were a plot *standing
|
||||
/// alone* in `viewport` -- the label strip plus the title column, which is the width a
|
||||
/// figure's common gutter is a maximum over. The same axes, text backend and metrics,
|
||||
/// laid out with no figure floor, so reading it leaves the figure's rows untouched.
|
||||
plot::f32 natural_gutter_width(plot::u32 which, plot::Rect viewport) {
|
||||
plot::Plot solo;
|
||||
give_magnitude_row(solo, which);
|
||||
solo.set_viewport(viewport);
|
||||
solo.layout();
|
||||
return solo.layout().y_axis_width + solo.layout().y_title_width;
|
||||
}
|
||||
|
||||
bool rows_overlap(const plot::Rect& a, const plot::Rect& b) {
|
||||
return a.x < b.right() && b.x < a.right() && a.y < b.bottom() && b.y < a.bottom();
|
||||
}
|
||||
@@ -255,6 +291,135 @@ PLOT_TEST(every_row_keeps_a_positive_height) {
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------- y gutter alignment
|
||||
|
||||
/// Spec §16, the invariant: a figure gives its rows one common left gutter, so every
|
||||
/// row's `axes` starts and ends on the same pixel columns. This is the mechanism; the
|
||||
/// case below is the drawn consequence a user sees as "the y axes line up".
|
||||
PLOT_TEST(figure_rows_share_one_y_gutter) {
|
||||
plot::Figure figure = different_magnitude_figure();
|
||||
const plot::FigureLayout layout = figure.layout();
|
||||
const plot::Rect first = figure.plot(0u).layout().axes;
|
||||
for (plot::u32 row = 0; row < 3u; ++row) {
|
||||
const plot::Rect& axes = figure.plot(row).layout().axes;
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(axes.x), static_cast<plot::f64>(first.x), 1e-4);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(axes.w), static_cast<plot::f64>(first.w), 1e-4);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(axes.right()), static_cast<plot::f64>(first.right()),
|
||||
1e-4);
|
||||
// The rows are still their own plots vertically: the equality is the *left gutter*
|
||||
// and not three rows accidentally given one rect.
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(axes.y), static_cast<plot::f64>(layout.rows[row].y),
|
||||
1e-4);
|
||||
}
|
||||
// The rows really do disagree about the strip they need, which is what makes the
|
||||
// gutter a decision the figure has to make: each row's own labels, measured with no
|
||||
// floor in the row's own viewport, ask for a different width -- and every row's
|
||||
// applied width is at least what it asked for, so the figure widened the narrow ones
|
||||
// rather than inventing a width they did not need.
|
||||
plot::f32 natural[3] = {};
|
||||
for (plot::u32 row = 0; row < 3u; ++row) {
|
||||
natural[row] = natural_gutter_width(row, layout.rows[row]);
|
||||
const plot::PlotLayout& rl = figure.plot(row).layout();
|
||||
PLOT_CHECK(rl.y_axis_width + rl.y_title_width >= natural[row] - 1e-4f);
|
||||
}
|
||||
PLOT_CHECK(!(natural[0] == natural[1] && natural[1] == natural[2]));
|
||||
// The shared x axis is still the *bottom row's own* label area -- aligning the rows'
|
||||
// left chrome must not move where a press on the shared axis lands.
|
||||
const plot::PlotLayout& bottom = figure.plot(2u).layout();
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(layout.x_axis_strip.x),
|
||||
static_cast<plot::f64>(bottom.x_label_area.x), 1e-4);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(layout.x_axis_strip.w),
|
||||
static_cast<plot::f64>(bottom.x_label_area.w), 1e-4);
|
||||
PLOT_CHECK(layout.x_axis_strip.h > 0.0f);
|
||||
}
|
||||
|
||||
/// Spec §16, the drawn consequence: with one gutter, a given data x maps to **one**
|
||||
/// pixel column of the figure, because every row's x transform is built from the same
|
||||
/// axes rect. That is the property a vertical feature -- a cursor, a stacked sample --
|
||||
/// relies on, and it is what "the y axis are not vertically aligned" complained about.
|
||||
PLOT_TEST(figure_rows_map_a_data_x_to_one_column) {
|
||||
plot::Figure figure = different_magnitude_figure();
|
||||
figure.layout();
|
||||
const plot::f32 column = static_cast<plot::f32>(figure.plot(0u).x_transform(0u).to_screen_x(37.5));
|
||||
for (plot::u32 row = 0; row < 3u; ++row) {
|
||||
const plot::Plot& p = figure.plot(row);
|
||||
const plot::f32 x = static_cast<plot::f32>(p.x_transform(0u).to_screen_x(37.5));
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(x), static_cast<plot::f64>(column), 1e-3);
|
||||
// ... and the leftmost data column lands on the axes' left edge in every row, so
|
||||
// the alignment is the full width and not a common offset into three different ones.
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(p.x_transform(0u).to_screen_x(0.0)),
|
||||
static_cast<plot::f64>(p.layout().axes.x), 1e-3);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(p.x_transform(0u).to_screen_x(100.0)),
|
||||
static_cast<plot::f64>(p.layout().axes.right()), 1e-2);
|
||||
}
|
||||
}
|
||||
|
||||
/// The neighbour: a `Plot` standing alone is not a figure and keeps sizing its strip
|
||||
/// from its own data. The floor is the figure's imposition, unset by default, and the
|
||||
/// only thing that widens a lone plot's *gutter* is an explicit one.
|
||||
PLOT_TEST(a_plot_on_its_own_keeps_its_own_y_gutter) {
|
||||
plot::Plot p;
|
||||
give_magnitude_row(p, 1u); // the "90" row: the narrowest labels
|
||||
p.set_viewport(whole_figure());
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(p.min_y_gutter()), 0.0, 1e-12);
|
||||
const plot::PlotLayout& natural_layout = p.layout();
|
||||
const plot::f32 natural = natural_layout.y_axis_width + natural_layout.y_title_width;
|
||||
PLOT_CHECK(natural > 0.0f);
|
||||
|
||||
// An explicit floor widens the gutter, which is the same lever `Figure` pulls -- and
|
||||
// it is a floor on the *gutter*, so the label strip and the title column together
|
||||
// reach it rather than the label strip alone.
|
||||
p.set_min_y_gutter(natural + 20.0f);
|
||||
const plot::PlotLayout& wide = p.layout();
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(wide.y_axis_width + wide.y_title_width),
|
||||
static_cast<plot::f64>(natural + 20.0f), 1e-3);
|
||||
// Negative values mean "no floor", like `set_row_gap()`, and the gutter goes back to
|
||||
// what the labels and titles ask for.
|
||||
p.set_min_y_gutter(-5.0f);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(p.min_y_gutter()), 0.0, 1e-12);
|
||||
const plot::PlotLayout& back = p.layout();
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(back.y_axis_width + back.y_title_width),
|
||||
static_cast<plot::f64>(natural), 1e-4);
|
||||
}
|
||||
|
||||
/// The figure's gutter is clamped by the same cap as the measured strip, so a figure
|
||||
/// whose rows are much wider than its viewport still cannot let the gutter squeeze the
|
||||
/// axes below the half-viewport floor `Plot::layout()` guarantees.
|
||||
PLOT_TEST(figure_gutter_never_squeezes_the_axes_below_the_floor) {
|
||||
plot::Figure figure = different_magnitude_figure();
|
||||
const plot::f32 w = 40.0f;
|
||||
figure.set_viewport({0.0f, 0.0f, w, 60.0f});
|
||||
figure.layout();
|
||||
const plot::Rect first = figure.plot(0u).layout().axes;
|
||||
for (plot::u32 row = 0; row < 3u; ++row) {
|
||||
const plot::Rect& axes = figure.plot(row).layout().axes;
|
||||
PLOT_CHECK(axes.w >= 0.5f * w - 1e-3f);
|
||||
PLOT_CHECK(axes.w > 0.0f);
|
||||
// ... and the rows still agree, which is the point: the cap is the figure's width,
|
||||
// and every row shares it.
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(axes.x), static_cast<plot::f64>(first.x), 1e-4);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(axes.w), static_cast<plot::f64>(first.w), 1e-4);
|
||||
}
|
||||
}
|
||||
|
||||
/// The degenerate neighbour: a figure of one row imposes exactly what that row
|
||||
/// measured, so it lays out as the plot would on its own -- the gutter cannot move a
|
||||
/// row that has no sibling to line up with.
|
||||
PLOT_TEST(a_one_row_figure_lays_out_as_the_plot_alone_does) {
|
||||
plot::Figure figure = different_magnitude_figure(1u);
|
||||
const plot::FigureLayout layout = figure.layout();
|
||||
plot::Plot solo;
|
||||
give_magnitude_row(solo, 0u);
|
||||
solo.set_viewport(layout.rows[0]);
|
||||
solo.layout();
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(figure.plot(0u).layout().axes.x),
|
||||
static_cast<plot::f64>(solo.layout().axes.x), 1e-4);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(figure.plot(0u).layout().axes.w),
|
||||
static_cast<plot::f64>(solo.layout().axes.w), 1e-4);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(figure.plot(0u).layout().y_axis_width),
|
||||
static_cast<plot::f64>(solo.layout().y_axis_width), 1e-4);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- the x link
|
||||
|
||||
/// Linking a row to a group that already has a range puts it on that range at once,
|
||||
|
||||
Reference in new issue
Block a user