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.
96 lines
4.7 KiB
C++
96 lines
4.7 KiB
C++
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
|
|
|
#pragma once
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include <plot/core/types.hpp>
|
|
|
|
namespace plot {
|
|
|
|
/// One axis tick. `value` is in data coordinates; `major` marks the labelled
|
|
/// ticks (minor ticks carry an empty label); `label` is UTF-8.
|
|
struct Tick {
|
|
f64 value = 0.0;
|
|
bool major = true;
|
|
std::string label;
|
|
};
|
|
|
|
/// How an axis renders its tick labels.
|
|
enum class TickFormat {
|
|
Si, ///< SI-prefixed numbers, e.g. `1.5u`, `2.5G`, `1.23k`.
|
|
Time, ///< clock strings, e.g. `1:30`, `1:00:00`.
|
|
};
|
|
|
|
/// Writes an SI-prefixed rendering of `v` to `buf`, always NUL-terminated and
|
|
/// never writing past `n` bytes (a too-small buffer truncates, it does not
|
|
/// overflow). ASCII plus the UTF-8 micro sign U+00B5 (0xC2 0xB5) for 1e-6, so
|
|
/// `plot::render::EmbeddedMonoFont` renders it instead of mojibake.
|
|
///
|
|
/// `format_si(0.0)` is exactly `"0"`. Values use three significant digits, so
|
|
/// `1234.0` renders as `1.23k`. The sign of a negative value is kept. A value
|
|
/// whose engineering exponent falls outside the prefix table (or a label that
|
|
/// would exceed 12 characters) falls back to `"%.3g"`, and a non-finite value
|
|
/// always does -- `nan`/`inf` are never silently dropped, they are printed by the
|
|
/// fallback exactly as `%.3g` prints them.
|
|
///
|
|
/// `unit` is appended after the prefix when non-null, which is how a caller
|
|
/// spells a unit the library cannot know: `format_si(v, buf, n, "\xC2\xB0")` for
|
|
/// degrees, `"\xCE\xA9"` for ohms.
|
|
void format_si(f64 v, char* buf, usize n, const char* unit = nullptr);
|
|
|
|
/// Writes `seconds` as a compact clock string to `buf`, NUL-terminated and never
|
|
/// past `n` bytes. Negatives keep their sign; a non-finite value falls back to
|
|
/// `"%.3g"`.
|
|
///
|
|
/// The whole value is rounded to a single whole-second count first and every
|
|
/// field is derived from that one number, so no field can ever reach its radix:
|
|
/// 119.5 s is `2:00`, not `1:60`, and 3599.5 s is `1:00:00`.
|
|
///
|
|
/// Shapes, by magnitude: below 1 s the prefixed unit that keeps the mantissa in
|
|
/// `[1, 1000)` -- `500ms`, `5ms`, `500us`, `5us`, `5ns`, `5ps` (the micro sign is
|
|
/// UTF-8 U+00B5, as in `format_si`) -- with an exact zero printed as `0` rather
|
|
/// than as a zero of some unit; below 60 s `1.5s`; below 1 h `1:30`;
|
|
/// at or above 24 h, as a decimal day count `2.5d`. Between 1 h and 24 h the
|
|
/// shape is `1:01:01` -- and because a value that rounds up to 24 h takes the day
|
|
/// shape, the hour field of that shape is always below 24. The 60 s and 1 h
|
|
/// boundaries are inclusive of the rounded value: 59.5 s is `1:00`.
|
|
void format_time(f64 seconds, char* buf, usize n);
|
|
|
|
/// The hard cap on `generate_ticks` output, minor ticks included. A range dense
|
|
/// enough to need more is truncated rather than allowed to grow without bound.
|
|
inline constexpr usize kMaxTicks = 256;
|
|
|
|
/// Nice-number ticks for the closed interval `range` on an axis `px` pixels long,
|
|
/// with major ticks at least `min_px` pixels apart. Steps are 1/2/5 x 10^n, chosen
|
|
/// by rounding the ideal step *up* to the next nice value so major spacing really
|
|
/// does meet `min_px`. Ticks are appended to `out`, which is cleared first, in
|
|
/// ascending value order and labelled by `format_si`.
|
|
///
|
|
/// `include_minors` adds, for grid geometry, the minor ticks subdividing each
|
|
/// major step by 5 (by 4 when the step's leading digit is 2). Without it `out`
|
|
/// holds exactly the labelled major ticks -- the labelled set a caller draws
|
|
/// numbers for, with no unlabelled entries to filter. The cap counts **minor**
|
|
/// ticks only: a range dense enough to exceed it loses its subdivisions, never
|
|
/// its labelled majors -- every major at or below `hi` is emitted, so the far
|
|
/// end of the axis keeps its numbers.
|
|
///
|
|
/// A range that straddles the origin gets a tick of **exactly** `0.0` there, labelled
|
|
/// `0`. The walk cannot land on the origin by itself -- it starts at
|
|
/// `ceil(min / stride) * stride`, which is not representable whenever the stride is
|
|
/// not a power of two -- so the tick that means zero arrives a few ULP off and would
|
|
/// otherwise be labelled from the far end of the SI table (`-139a` between `-500m` and
|
|
/// `500m`). Every tick is an integer multiple of the stride, so anything within a
|
|
/// millionth of one *is* the origin and nothing else; a range whose ticks are
|
|
/// legitimately that small keeps them.
|
|
///
|
|
/// Hostile input clears `out` and returns: a non-finite endpoint, `max <= min`
|
|
/// (a zero or negative span, which includes the collapsed range of a
|
|
/// single-sample series), a non-positive `px` or `min_px`, and a span whose step
|
|
/// overflows to infinity.
|
|
void generate_ticks(Range range, f32 px, f32 min_px, std::vector<Tick>& out,
|
|
bool include_minors = false, f64 origin = 0.0);
|
|
|
|
} // namespace plot
|