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>
93 lines
4.5 KiB
C++
93 lines
4.5 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 `500ms`; 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);
|
|
|
|
} // namespace plot
|