fix(plot): a figure row's strip shows labels and title independently, and the shared axis is the bottom row's strip

This commit is contained in:
Martino Ferrari committed 2026-09-28 15:07:39 +02:00
1 parent 8f1e60a105
commit 770da4c2b2
9 files changed
+415 -175

No files matched your search

+82
View File
@@ -1,6 +1,7 @@
#include "harness.hpp"
#include <plot/input/controller.hpp>
#include <plot/plot/figure.hpp>
#include <plot/plot/plot.hpp>
#include <plot/data/array_series.hpp>
#include <plot/data/stream_series.hpp>
@@ -1306,3 +1307,84 @@ PLOT_TEST(hot_handles_follow_a_gesture_that_moves_the_geometry) {
PLOT_CHECK(f.plot.hot_handles().zone == f.controller.region_grab(rid));
PLOT_CHECK(f.plot.hot_handles().zone != plot::RegionGrab::Left);
}
/// A `Figure`'s shared x axis is the **bottom row's own**, so the axis zoom is
/// reachable there through the one hit test the controller has always used: the press
/// that lands in the bottom row's x label strip starts `AxisZoomX` exactly as it does on
/// a stand-alone plot, and because the rows share one x link the zoom moves all of them.
///
/// A figure that put its shared axis *outside* every row would leave this gesture
/// unreachable on every row at once: `x_label_area` would be zero-height, `contains()`
/// would never be true, and a middle drag would fall through to `DragMode::None`.
///
/// The other half is what makes that a statement about *which* row: the same drag on an
/// interior row -- at the point a label strip would land if that row had one -- is
/// deliberately not the gesture. The row reserved no x chrome, so there is no strip for
/// the press to be inside, and the zoom the bottom row's strip can start cannot be
/// started from a row that has none. If that were not so the shared axis would be one
/// strip among several, each of which could zoom the one range they all share.
PLOT_TEST(a_middle_drag_on_a_figures_bottom_row_zooms_the_shared_x_axis) {
plot::Figure figure(3u);
figure.set_viewport({0, 0, 800, 600});
for (u32 row = 0; row < 3u; ++row) {
plot::Plot& p = figure.plot(row);
p.add_x_axis({"t", {0.0, 100.0}, false, plot::AxisPlace::Bottom, false});
p.add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
}
const u32 group = figure.add_x_link();
for (u32 row = 0; row < 3u; ++row) PLOT_CHECK(figure.link_x(row, group));
const plot::Range before = figure.x_link_range(group);
figure.layout();
const plot::PlotLayout& bottom = figure.plot(2u).layout();
PLOT_CHECK(bottom.x_label_area.h > 0.0f);
const f32 strip_x = bottom.axes.x + 0.5f * bottom.axes.w;
const f32 strip_y = bottom.x_label_area.y + 0.5f * bottom.x_label_area.h;
PLOT_CHECK(bottom.x_label_area.contains(strip_x, strip_y));
plot::input::Controller c(figure.plot(2u));
c.set_viewport(figure.viewport());
c.on_pointer({strip_x, strip_y, 0, 0, plot::input::Button::Middle, 0, true, true, false});
PLOT_CHECK(c.drag_mode() == plot::input::DragMode::AxisZoomX);
// Same sign and magnitude as on a single plot: a positive dy zooms in, 20 px being a
// tenth of a decade.
c.on_pointer({strip_x, strip_y + 20, 0, 20, plot::input::Button::Middle, 0, true, false, false});
c.on_pointer({strip_x, strip_y + 20, 0, 0, plot::input::Button::Middle, 0, false, false, true});
PLOT_CHECK(c.drag_mode() == plot::input::DragMode::None);
// The gesture reached the row's own axis...
PLOT_CHECK_NEAR(figure.plot(2u).x_range(0u).span(), 100.0 * std::pow(10.0, -20.0 / 200.0), 1e-6);
// ... and, through the link, every other row with it: one x axis, zoomed once.
figure.layout();
for (u32 row = 0; row < 3u; ++row) {
PLOT_CHECK_NEAR(figure.plot(row).x_range(0u).span(), figure.x_link_range(group).span(), 1e-9);
PLOT_CHECK_NEAR(figure.plot(row).x_range(0u).span(), 100.0 * std::pow(10.0, -20.0 / 200.0),
1e-6);
}
PLOT_CHECK(figure.x_link_range(group).span() < before.span());
// An interior row is not the same gesture. A press at the point a label strip *would*
// land in -- the band this row's chrome would occupy if the figure gave it one -- is
// not inside anything: the row reserved no x chrome, so its `x_label_area` is
// zero-height and no press can be inside it. The middle drag therefore falls through
// to `DragMode::None`, and because `update_drag()` does nothing in that mode the
// group's window -- the one range every row shares -- is untouched by it.
const plot::PlotLayout& interior = figure.plot(0u).layout();
PLOT_CHECK(interior.x_label_area.h == 0.0f);
const f32 interior_x = interior.axes.x + 0.5f * interior.axes.w;
const f32 interior_y = figure.layout().rows[0].bottom() - 0.5f * bottom.x_label_area.h;
PLOT_CHECK(!interior.x_label_area.contains(interior_x, interior_y));
const plot::Range shared_before = figure.x_link_range(group);
plot::input::Controller interior_controller(figure.plot(0u));
interior_controller.on_pointer(
{interior_x, interior_y, 0, 0, plot::input::Button::Middle, 0, true, true, false});
PLOT_CHECK(interior_controller.drag_mode() == plot::input::DragMode::None);
interior_controller.on_pointer(
{interior_x, interior_y + 20, 0, 20, plot::input::Button::Middle, 0, true, false, false});
interior_controller.on_pointer(
{interior_x, interior_y + 20, 0, 0, plot::input::Button::Middle, 0, false, false, true});
PLOT_CHECK(interior_controller.drag_mode() == plot::input::DragMode::None);
PLOT_CHECK_NEAR(figure.plot(0u).x_range(0u).span(), shared_before.span(), 1e-12);
PLOT_CHECK_NEAR(figure.x_link_range(group).span(), shared_before.span(), 1e-12);
}