fix(plot): single-rounding clock labels, log-axis fits that refuse non-positive data

This commit is contained in:
Martino Ferrari committed 2026-09-27 22:28:56 +02:00
1 parent 93fc06f050
commit 606e40a9f1
7 files changed
+258 -73

No files matched your search

+85
View File
@@ -24,6 +24,13 @@ f32 widest_major_label(const plot::Axis& a, const plot::render::TextBackend& t)
return w;
}
/// A log axis must never be left inverted or non-positive by an auto-fit.
void check_log_axis_range(const plot::Range& r) {
PLOT_CHECK(std::isfinite(r.min) && std::isfinite(r.max));
PLOT_CHECK(r.min > 0.0);
PLOT_CHECK(r.min < r.max);
}
bool has_vertical(const plot::Grid& g, f64 x, bool major) {
for (const plot::GridLine& l : g.lines()) {
if (l.x0 == l.x1 && l.x0 == x && l.major == major) return true;
@@ -259,6 +266,84 @@ PLOT_TEST(inverted_and_out_of_range_measurements_are_invalid) {
PLOT_CHECK(!p.measurement(99u).valid); // unknown measure id
}
PLOT_TEST(auto_fit_log_axis_stays_positive_and_ordered) {
// All-negative trace bounds: nothing to show on a log axis, so the fit is
// refused and the caller's (positive) range survives untouched.
{
std::vector<f32> y = {-5.0f, -3.0f, -1.0f};
plot::data::ArraySeries s(std::move(y), 0.0, 1.0);
plot::Plot p;
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
const u32 ax = p.add_x_axis({"t", {1.0, 10.0}, false, plot::AxisPlace::Bottom, false});
const u32 ay = p.add_y_axis({"v", {1.0, 100.0}, true, plot::AxisPlace::Left, true});
p.add_trace({&s, plot::Color::hex(0x40C0FF), 1.5f, ax, ay});
p.auto_fit_y(ay);
check_log_axis_range(p.y_range(ay));
PLOT_CHECK_NEAR(p.y_range(ay).min, 1.0, 1e-12);
PLOT_CHECK_NEAR(p.y_range(ay).max, 100.0, 1e-12);
(void)p.layout(); // the per-layout refit refuses too
check_log_axis_range(p.y_range(ay));
PLOT_CHECK_NEAR(p.y_range(ay).max, 100.0, 1e-12);
}
// All-zero bounds: same refusal, no "0" or negative bound on the axis. One
// sample at x = 0 makes both bounds exactly {0, 0}.
{
std::vector<f32> y = {0.0f};
plot::data::ArraySeries s(std::move(y), 0.0, 1.0);
plot::Plot p;
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
const u32 ax = p.add_x_axis({"t", {1.0, 10.0}, true, plot::AxisPlace::Bottom, true});
const u32 ay = p.add_y_axis({"v", {1.0, 10.0}, true, plot::AxisPlace::Left, true});
p.add_trace({&s, plot::Color::hex(0x40C0FF), 1.5f, ax, ay});
p.auto_fit_x();
p.auto_fit_y(ay);
check_log_axis_range(p.x_range(ax));
check_log_axis_range(p.y_range(ay));
PLOT_CHECK_NEAR(p.x_range(ax).min, 1.0, 1e-12); // refused: range untouched
PLOT_CHECK_NEAR(p.x_range(ax).max, 10.0, 1e-12);
PLOT_CHECK_NEAR(p.y_range(ay).min, 1.0, 1e-12);
PLOT_CHECK_NEAR(p.y_range(ay).max, 10.0, 1e-12);
}
// Spanning zero with some positive samples: the fit uses the positive part, and
// the positive data stays inside the range.
{
std::vector<f32> y = {-1.0f, 0.5f, 3.0f};
plot::data::ArraySeries s(std::move(y), -1.0, 1.0); // x bounds {-1, 1}
plot::Plot p;
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
const u32 ax = p.add_x_axis({"t", {1.0, 10.0}, true, plot::AxisPlace::Bottom, true});
const u32 ay = p.add_y_axis({"v", {1.0, 100.0}, true, plot::AxisPlace::Left, true});
p.add_trace({&s, plot::Color::hex(0x40C0FF), 1.5f, ax, ay});
p.auto_fit_x();
p.auto_fit_y(ay);
check_log_axis_range(p.x_range(ax));
check_log_axis_range(p.y_range(ay));
PLOT_CHECK(p.x_range(ax).min <= 1.0); // the positive x data
PLOT_CHECK(p.x_range(ax).max >= 1.0);
PLOT_CHECK(p.y_range(ay).min <= 3.0); // the positive y data
PLOT_CHECK(p.y_range(ay).max >= 3.0);
}
// Purely positive stays a plain log fit, still ordered and positive.
{
std::vector<f32> y = {1.0f, 2.0f, 3.0f};
plot::data::ArraySeries s(std::move(y), 1.0, 1.0); // x bounds {1, 3}
plot::Plot p;
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
const u32 ax = p.add_x_axis({"t", {1.0, 10.0}, true, plot::AxisPlace::Bottom, true});
const u32 ay = p.add_y_axis({"v", {1.0, 100.0}, true, plot::AxisPlace::Left, true});
p.add_trace({&s, plot::Color::hex(0x40C0FF), 1.5f, ax, ay});
p.auto_fit_x();
p.auto_fit_y(ay);
check_log_axis_range(p.x_range(ax));
check_log_axis_range(p.y_range(ay));
PLOT_CHECK(p.x_range(ax).min < 1.0 && p.x_range(ax).max > 3.0);
PLOT_CHECK(p.y_range(ay).min < 1.0 && p.y_range(ay).max > 3.0);
}
}
PLOT_TEST(grid_lines_are_tick_aligned_data_space_segments) {
plot::Plot p;
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});