fix(ticks): label the tick at the origin 0, not from the atto end of the table
CI / debug (tests + all adapters) (push) Canceled after 0s
CI / coverage (floor + measured table) (push) Canceled after 0s
CI / asan (AddressSanitizer) (push) Canceled after 0s

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:
Martino FerrariandClaude Opus 4.6 committed 2026-10-02 00:52:20 +02:00
1 parent 525759f9a9
commit e337c6a60c
4 files changed
+104 -11

No files matched your search

+21 -2
View File
@@ -189,6 +189,23 @@ void generate_ticks(Range range, f32 px, f32 min_px, std::vector<Tick>& out, boo
const f64 first = include_minors ? std::ceil(lo / minor_step) * minor_step : first_major;
if (!std::isfinite(first)) return;
// The walk starts at `ceil(lo / stride) * stride`, which is not representable
// whenever the stride is not a power of two -- 0.1 and 0.2 are the common ones --
// so a range that straddles the origin never lands *on* it: [-1, 1] stepped by 0.1
// reaches -1.39e-16 where it means zero, and that renders as `-139a`, a label from
// the far end of the SI table sitting between `-500m` and `500m`. Deriving `v` by
// multiplication instead of accumulation does not help, because the error is
// already in `first`.
//
// 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 -- a relative gap of some ten orders of magnitude. Anything inside a
// millionth of the stride is therefore the zero tick and nothing else, and it is
// stored as exactly `0.0` rather than merely labelled as such: `Axis::refresh()`
// re-formats a time axis from `tick.value`, where the same residue reads
// `3.19e-13ms`.
const f64 zero_tol = (include_minors ? minor_step : step) * 1e-6;
char label[kLabelBuf];
usize minors = 0;
for (f64 v = first; v <= hi + stride * 1e-9;) {
@@ -213,12 +230,14 @@ void generate_ticks(Range range, f32 px, f32 min_px, std::vector<Tick>& out, boo
}
if (!major) ++minors;
Tick tick;
tick.value = v;
// The walk variable itself is left alone: `minor_index_is_major` counts indices
// from `first_major` and both operands have to stay on the walk's own grid.
tick.value = std::fabs(v) < zero_tol ? 0.0 : v;
tick.major = major;
if (major) {
// format_si owns both label guards: it never writes past the buffer, and it
// falls back to %.3g for a non-finite value or a label over 12 characters.
format_si(v, label, sizeof label);
format_si(tick.value, label, sizeof label);
tick.label = label;
}
out.push_back(std::move(tick));