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:
1 parent
211c5b97bc
commit
2dcd1f1864
7 files changed
+197
-14
No files matched your search
+33
-9
@@ -99,14 +99,34 @@ void format_time(f64 seconds, char* buf, usize n) {
|
||||
const f64 t = std::fabs(seconds);
|
||||
|
||||
if (t < 1.0) {
|
||||
// Sub-second: milliseconds. The 999.5 cut keeps %.3g out of scientific
|
||||
// notation (it would print "1e+03ms") right below the one-second boundary.
|
||||
const f64 ms = t * 1000.0;
|
||||
if (ms < 999.5) {
|
||||
std::snprintf(buf, n, "%s%.3gms", sign, ms);
|
||||
} else {
|
||||
std::snprintf(buf, n, "%s%.3gs", sign, t);
|
||||
// Sub-second: the prefix that keeps the mantissa in [1, 1000) -- the same rule the SI
|
||||
// formatter uses on a y axis, and the reading a zoomed-in window needs. A fixed
|
||||
// millisecond unit printed `0.005ms` where `5us` is what the axis is showing, and
|
||||
// `1e-05ms` where `10ns` is. The micro sign is the UTF-8 sequence the embedded font has a
|
||||
// glyph for (see `format_si`), not a Latin-1 byte.
|
||||
// Exactly zero carries no unit, the same convention `format_si` states: every prefixed
|
||||
// form of it is "0" with a unit the value does not have.
|
||||
if (t == 0.0) {
|
||||
std::snprintf(buf, n, "0");
|
||||
return;
|
||||
}
|
||||
static const struct {
|
||||
f64 scale;
|
||||
const char* unit;
|
||||
} kUnits[] = {
|
||||
{1.0e-3, "ms"},
|
||||
{1.0e-6, "\u00b5s"},
|
||||
{1.0e-9, "ns"},
|
||||
{1.0e-12, "ps"},
|
||||
};
|
||||
for (const auto& u : kUnits) {
|
||||
if (t < u.scale) continue;
|
||||
std::snprintf(buf, n, "%s%.3g%s", sign, t / u.scale, u.unit);
|
||||
return;
|
||||
}
|
||||
// Below a picosecond nothing on a time axis reads in prefixed units; plain seconds keep
|
||||
// the fallback's own precision rather than inventing one.
|
||||
std::snprintf(buf, n, "%s%.3gs", sign, t);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -131,7 +151,8 @@ void format_time(f64 seconds, char* buf, usize n) {
|
||||
}
|
||||
}
|
||||
|
||||
void generate_ticks(Range range, f32 px, f32 min_px, std::vector<Tick>& out, bool include_minors) {
|
||||
void generate_ticks(Range range, f32 px, f32 min_px, std::vector<Tick>& out, bool include_minors,
|
||||
f64 origin) {
|
||||
out.clear();
|
||||
|
||||
const f64 lo = range.min;
|
||||
@@ -157,7 +178,10 @@ void generate_ticks(Range range, f32 px, f32 min_px, std::vector<Tick>& out, boo
|
||||
: 10.0);
|
||||
if (!std::isfinite(step) || !(step > 0.0)) return; // span so large the step overflowed
|
||||
|
||||
const f64 first_major = std::ceil(lo / step) * step;
|
||||
// The major walk starts on the *origin's* grid: with an origin the ticks a host reads are
|
||||
// round numbers relative to it (0, 0.5ms, 1ms of the window it zoomed into), which is the
|
||||
// whole point of having one. `origin = 0` is the plain absolute grid.
|
||||
const f64 first_major = origin + std::ceil((lo - origin) / step) * step;
|
||||
if (!std::isfinite(first_major)) return;
|
||||
|
||||
// Minor ticks subdivide the major step: by 4 when the step's leading digit is
|
||||
|
||||
Reference in new issue
Block a user