fix(plot): single-rounding clock labels, log-axis fits that refuse non-positive data
This commit is contained in:
1 parent
93fc06f050
commit
606e40a9f1
7 files changed
+258
-73
No files matched your search
@@ -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});
|
||||
|
||||
@@ -1,11 +1,42 @@
|
||||
#include "harness.hpp"
|
||||
#include <plot/plot/ticks.hpp>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
/// Fails if any colon-separated field of a clock label reaches its radix: minutes
|
||||
/// and seconds must stay below 60 and hours below 24 (a value that rounds up to
|
||||
/// 24 h takes the day shape instead), so "1:60" or "24:00:00" cannot slip through.
|
||||
void check_no_field_reaches_its_radix(const std::string& label) {
|
||||
const std::size_t colons = static_cast<std::size_t>(std::count(label.begin(), label.end(), ':'));
|
||||
if (colons == 0) return; // decimal shapes have no fields
|
||||
// Two colons mean h:mm:ss, one means m:ss: only the former starts with hours.
|
||||
const int first_radix = colons >= 2 ? 24 : 60;
|
||||
std::string clock = label;
|
||||
if (!clock.empty() && (clock[0] == '-' || clock[0] == '+')) clock.erase(0, 1);
|
||||
std::size_t start = 0;
|
||||
std::size_t field = 0;
|
||||
while (start <= clock.size()) {
|
||||
const std::size_t colon = clock.find(':', start);
|
||||
const std::string part =
|
||||
clock.substr(start, colon == std::string::npos ? std::string::npos : colon - start);
|
||||
if (!part.empty()) {
|
||||
const int value = std::atoi(part.c_str());
|
||||
PLOT_CHECK(value >= 0 && value < (field == 0 ? first_radix : 60));
|
||||
}
|
||||
if (colon == std::string::npos) break;
|
||||
start = colon + 1;
|
||||
++field;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PLOT_TEST(nice_ticks_are_powers_of_ten_times_125) {
|
||||
std::vector<plot::Tick> t;
|
||||
plot::generate_ticks({0.0, 1.0}, /*px=*/500.0f, /*min_px=*/60.0f, t);
|
||||
@@ -107,3 +138,29 @@ PLOT_TEST(format_time_shapes) {
|
||||
plot::format_time(86400.0, b, sizeof b); PLOT_CHECK(std::string(b) == "1d");
|
||||
plot::format_time(std::nan(""), b, sizeof b); PLOT_CHECK(std::string(b) == "nan");
|
||||
}
|
||||
|
||||
PLOT_TEST(format_time_never_lets_a_field_reach_its_radix) {
|
||||
char b[32];
|
||||
// Rounding the seconds leftover on its own used to print 119.5 s as "1:60";
|
||||
// every field now comes from one rounded whole-second count.
|
||||
plot::format_time(119.5, b, sizeof b); PLOT_CHECK(std::string(b) == "2:00");
|
||||
plot::format_time(3599.5, b, sizeof b); PLOT_CHECK(std::string(b) == "1:00:00");
|
||||
plot::format_time(59.4999, b, sizeof b); PLOT_CHECK(std::string(b) == "59.5s");
|
||||
plot::format_time(59.5, b, sizeof b); PLOT_CHECK(std::string(b) == "1:00");
|
||||
plot::format_time(60.0, b, sizeof b); PLOT_CHECK(std::string(b) == "1:00");
|
||||
// At 24 h the day field appears, so the hour field never reaches 24.
|
||||
plot::format_time(86399.4, b, sizeof b); PLOT_CHECK(std::string(b) == "23:59:59");
|
||||
plot::format_time(86399.5, b, sizeof b); PLOT_CHECK(std::string(b) == "1d");
|
||||
plot::format_time(86400.0, b, sizeof b); PLOT_CHECK(std::string(b) == "1d");
|
||||
|
||||
// Sweep the boundary neighbourhoods and check every emitted field.
|
||||
const double values[] = {0.4, 0.9995, 1.0, 59.4, 59.4999, 59.5, 59.9, 60.0, 90.0, 119.5, 3599.4,
|
||||
3599.5, 3599.9, 86399.4, 86399.5, 86399.9, 86400.0, 172800.0, -119.5,
|
||||
-3599.5, -86399.5};
|
||||
for (const double v : values) {
|
||||
plot::format_time(v, b, sizeof b);
|
||||
const std::string label(b);
|
||||
PLOT_CHECK(!label.empty());
|
||||
check_no_field_reaches_its_radix(label);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user