Files
plotter/include/plot/plot/ticks.hpp
T
Martino Ferrari 2dcd1f1864 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.
2026-10-03 12:14:42 +02:00

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