feat(plot): an axis can write its labels relative to an origin, and time scales by prefix

A host zooming into a sub-second window cannot label it absolutely: Time
formatting uses three significant digits, so 12.345671s and 12.345672s are the
same string and a 1 ms step around a multi-second time is unlabelled. The axis
gains an `origin`: every value it writes is `v - origin`, and the ticks are
generated on the shifted grid, so the window reads 0, 500us, 1ms.
`origin_follows_range` re-takes the origin from the axis' own range on every
refresh -- the mode a *moving* window needs, since the edge is derived from the
newest sample at layout time and a value the host wrote a frame earlier is a
frame of scroll stale.

`format_time` now scales its sub-second values by prefix, the way `format_si`
does on a y axis: the unit that keeps the mantissa in [1, 1000). A fixed
millisecond unit printed 0.005ms where 5us is the reading, and 1e-05ms where
10ns is. An exact zero prints `0`, the same convention `format_si` states. The
origin subtraction's dust is snapped by a *relative* epsilon, so a tick on the
origin reads 0 rather than 1.78e-12ms.

Tested by `ticks_are_written_relative_to_an_axis_origin`,
`a_range_origin_puts_the_zero_tick_on_the_left_edge` and the extended
`format_time_shapes`; the suite is 359 cases, 0 failed.
This commit is contained in:
Martino Ferrari committed 2026-10-03 12:14:42 +02:00
1 parent 211c5b97bc
commit 2dcd1f1864
7 files changed
+197 -14

No files matched your search

+15
View File
@@ -488,6 +488,21 @@ class Plot {
/// grab it, which is what a host wants for a marker it draws as a *readout*. An id that
/// names no cursor is ignored, like every other setter here.
void set_cursor_selectable(u32 id, bool selectable);
/// Sets the value an axis' labels are written relative to (see `Axis::origin`). The ticks
/// are generated on that shifted grid too, so a host that zooms into a window can label it
/// from its own left edge. An id that names no axis of that orientation is ignored, like the
/// other setters here -- x and y ids are separate sequences, so the orientation is part of
/// the call.
void set_x_axis_origin(u32 axis, f64 origin);
void set_y_axis_origin(u32 axis, f64 origin);
/// The origin the matching setter stored (0 for an axis that never had one).
f64 x_axis_origin(u32 axis) const;
f64 y_axis_origin(u32 axis) const;
/// Makes an axis' label origin follow its own range (see `Axis::origin_follows_range`): the
/// labels then count from the left edge the frame actually has, which is the only way a
/// host can label a *moving* window from its start without being a frame behind.
void set_x_axis_origin_from_range(u32 axis, bool on);
void set_y_axis_origin_from_range(u32 axis, bool on);
/// The measurement for `id`. Re-evaluated only when a cursor moved, when the
/// measure was added, or when the series underneath it changed: a different