fix(ticks): label the tick at the origin 0, not from the atto end of the table
A range straddling zero never lands *on* zero: the walk starts at `ceil(min / stride) * stride`, which is not representable whenever the stride is not a power of two -- 0.1 and 0.2 are the everyday ones -- so the tick that means zero arrives a few ULP off and `format_si` labels it from six prefixes down. `[-1, 1]` read `-1 -500m -139a 500m 1`. Deriving the tick by multiplication rather than accumulation does not help, because the error is already in the first term. Every tick is an integer multiple of the stride, so the smallest magnitude a non-zero tick can have is the stride itself, while the residue is a few ULP of the range -- ten orders of magnitude apart. Anything inside a millionth of the stride is therefore the origin and nothing else. It is stored as exactly `0.0` rather than merely labelled so: `Axis::refresh()` re-formats a time axis from `tick.value`, where the same residue reads `3.19e-13ms`. The walk variable is left alone, since `minor_index_is_major` counts indices from `first_major` and both operands have to stay on the walk's own grid. The two narrow-y-title cases were calibrated on the defect -- the bogus `-55.5a` was a six-character label, and its width is what spent the chrome allowance and made the titled and untitled frames comparable. They are re-seated on a milli-scale range, whose `-200m` is wide for a real reason, and the zero-width strip the frame comparison depends on is now asserted rather than assumed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
525759f9a9
commit
e337c6a60c
4 files changed
+104
-11
No files matched your search
@@ -158,6 +158,52 @@ PLOT_TEST(epoch_window_yields_labelled_majors) {
|
||||
PLOT_CHECK(deep_majors >= 9);
|
||||
}
|
||||
|
||||
/// The tick that *means* zero reads `0`. The walk starts at `ceil(lo/stride)*stride`,
|
||||
/// inexact for every stride that is not a power of two, so a range straddling the
|
||||
/// origin used to reach a few e-16 there and label it from the far end of the SI
|
||||
/// table: `-1 -500m -139a 500m 1`. The tolerance is relative to the stride, which is
|
||||
/// also the smallest magnitude a non-zero tick can have, so a range whose ticks are
|
||||
/// *legitimately* that small keeps them.
|
||||
PLOT_TEST(the_tick_at_the_origin_reads_zero) {
|
||||
std::vector<plot::Tick> t;
|
||||
const float widths[] = {80.0f, 40.0f, 10.0f};
|
||||
for (const float min_px : widths) {
|
||||
for (const bool minors : {false, true}) {
|
||||
plot::generate_ticks({-1.0, 1.0}, 400.0f, min_px, t, minors);
|
||||
plot::usize zeros = 0;
|
||||
for (const plot::Tick& tick : t) {
|
||||
if (!tick.major) continue;
|
||||
PLOT_CHECK(!tick.label.empty());
|
||||
// Nothing between -500m and 500m but the origin itself, so no major may
|
||||
// carry a prefix from below `m`.
|
||||
PLOT_CHECK(tick.label.find('a') == std::string::npos);
|
||||
PLOT_CHECK(tick.label.find('f') == std::string::npos);
|
||||
if (tick.label == "0") {
|
||||
++zeros;
|
||||
PLOT_CHECK(tick.value == 0.0); // exact, so a Time axis relabels it as 0 too
|
||||
}
|
||||
}
|
||||
PLOT_CHECK(zeros == 1u);
|
||||
}
|
||||
}
|
||||
|
||||
// A range whose ticks really are atto-scale is untouched: only the origin folds.
|
||||
plot::generate_ticks({-2e-16, 2e-16}, 400.0f, 80.0f, t, /*include_minors=*/true);
|
||||
plot::usize tiny = 0;
|
||||
plot::usize tiny_zeros = 0;
|
||||
for (const plot::Tick& tick : t) {
|
||||
if (!tick.major) continue;
|
||||
if (tick.value == 0.0) {
|
||||
++tiny_zeros;
|
||||
} else {
|
||||
++tiny;
|
||||
PLOT_CHECK(tick.label.find('a') != std::string::npos);
|
||||
}
|
||||
}
|
||||
PLOT_CHECK(tiny_zeros == 1u);
|
||||
PLOT_CHECK(tiny >= 2u);
|
||||
}
|
||||
|
||||
PLOT_TEST(si_labels_are_utf8_and_never_overrun) {
|
||||
char b[32];
|
||||
plot::format_si(0.0000015, b, sizeof b);
|
||||
|
||||
Reference in new issue
Block a user