//! Ticks as data: nice numbers (1, 2 and 5 × 10ᵏ; minutes and hours on long //! time axes), one SI prefix per axis, and time written as seconds from an //! origin or as a clock (m:ss.fff). Everything writes into inline //! [`Label`]s, so an axis is relabelled every frame without allocating. //! //! A tick's value is computed as an integer times the step's mantissa, scaled //! by an exact power of ten, so 0.3 is 3 / 10 and never 0.30000000000000004. use crate::label::Label; use core::fmt::Write; use libm::{ceil, fabs, floor, log10, round}; /// Evenly spaced ticks: tick `i` is `(k0 + i) · mantissa · 10^exp`. #[derive(Clone, Copy, Debug, PartialEq)] pub struct TickSet { /// The first tick's multiple of the step. pub k0: i64, /// How many ticks. pub count: u32, /// The step's mantissa: 1, 2 or 5, or a whole number of seconds on a /// minute or hour time axis (exp 0). pub mantissa: u32, /// The step's power of ten. pub exp: i32, } impl TickSet { /// No ticks. pub const EMPTY: TickSet = TickSet { k0: 0, count: 0, mantissa: 1, exp: 0, }; /// The step between ticks. pub fn step(&self) -> f64 { scale10(f64::from(self.mantissa), self.exp) } /// Tick `i`'s value (exact to the last bit for decimal steps). pub fn value(&self, i: u32) -> f64 { let k = self.k0 + i64::from(i); scale10(k as f64 * f64::from(self.mantissa), self.exp) } /// The first and last tick values, or None when there are none. pub fn bounds(&self) -> Option<(f64, f64)> { (self.count > 0).then(|| (self.value(0), self.value(self.count - 1))) } } const POW10: [f64; 23] = [ 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22, ]; /// 10^|e| exactly up to 10^22 (every power of ten up to there is a double). fn pow10_abs(e: i32) -> f64 { let a = e.unsigned_abs() as usize; if a < POW10.len() { POW10[a] } else { libm::pow(10.0, f64::from(e.unsigned_abs())) } } /// `x · 10^e`, dividing for negative `e` so the result is correctly rounded. pub fn scale10(x: f64, e: i32) -> f64 { if e >= 0 { x * pow10_abs(e) } else { x / pow10_abs(e) } } /// The nice step for `span` split into about `target` intervals: a mantissa /// of 1, 2 or 5 and a power of ten. pub fn nice_step(span: f64, target: u32) -> (u32, i32) { let raw = fabs(span) / f64::from(target.max(1)); if raw <= 0.0 || !raw.is_finite() { return (1, 0); } let mut e = floor(log10(raw)) as i32; let mut f = scale10(raw, -e); // log10 can land a hair off at exact powers of ten. if f >= 10.0 { e += 1; f = scale10(raw, -e); } else if f < 1.0 { e -= 1; f = scale10(raw, -e); } if f < 1.5 { (1, e) } else if f < 3.5 { (2, e) } else if f < 7.5 { (5, e) } else { (1, e + 1) } } fn span_ticks(lo: f64, hi: f64, mantissa: u32, exp: i32) -> TickSet { let step = scale10(f64::from(mantissa), exp); if !(lo.is_finite() && hi.is_finite() && step > 0.0) || hi < lo { return TickSet::EMPTY; } // A bound within a millionth of a step of a tick counts as on it. let k0 = ceil(lo / step - 1e-6) as i64; let k1 = floor(hi / step + 1e-6) as i64; let count = (k1 - k0 + 1).clamp(0, 10_000) as u32; TickSet { k0, count, mantissa, exp, } } /// About `target` nice ticks inside `[lo, hi]`. pub fn ticks(lo: f64, hi: f64, target: u32) -> TickSet { let (m, e) = nice_step(hi - lo, target); span_ticks(lo, hi, m, e) } /// `[lo, hi]` widened outward to whole multiples of its nice step, so an /// axis begins and ends on a tick. pub fn nice_bounds(lo: f64, hi: f64, target: u32) -> (f64, f64) { let (m, e) = nice_step(hi - lo, target); let step = scale10(f64::from(m), e); if !(step > 0.0 && lo.is_finite() && hi.is_finite()) { return (lo, hi); } let a = floor(lo / step + 1e-9); let b = ceil(hi / step - 1e-9); (scale10(a * f64::from(m), e), scale10(b * f64::from(m), e)) } /// The SI prefix symbol for a power of ten that is a multiple of 3. pub fn si_prefix(exp3: i32) -> &'static str { match exp3 { -15 => "f", -12 => "p", -9 => "n", -6 => "µ", -3 => "m", 3 => "k", 6 => "M", 9 => "G", 12 => "T", 15 => "P", _ => "", } } /// The prefix for a magnitude: below 1 the prefix that puts it in 1 … 999 /// (0.5 → 500 m); from 1 to 9 999 none; from 10 000 up, k, M, G, … pub fn si_exp3(maxabs: f64) -> i32 { if maxabs <= 0.0 || !maxabs.is_finite() { return 0; } let l = floor(log10(maxabs) + 1e-12) as i32; if (0..4).contains(&l) { return 0; } (3 * l.div_euclid(3)).clamp(-15, 15) } /// Writes `x` with `decimals` decimals, a true minus sign (U+2212) and no /// negative zero. fn write_fixed(out: &mut Label, x: f64, decimals: u8) { let half = 0.5 * scale10(1.0, -i32::from(decimals)); let mut x = x; if fabs(x) < half { x = 0.0; } if x < 0.0 { out.push('−'); x = -x; } let _ = write!(out, "{:.*}", usize::from(decimals), x); } /// How the numbers on one value axis are written: one SI prefix for the whole /// axis (it joins the unit: "mV") and as many decimals as the step needs. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct NumberFormat { /// The prefix's power of ten (a multiple of 3; 0 for none). pub exp3: i32, /// Decimals after the prefix is applied. pub decimals: u8, } impl NumberFormat { /// The format for a tick set: with `si`, a prefix chosen by the largest /// magnitude on the axis; decimals from the step. pub fn for_ticks(ts: &TickSet, si: bool) -> NumberFormat { let exp3 = if si { match ts.bounds() { Some((a, b)) => si_exp3(fabs(a).max(fabs(b))), None => 0, } } else { 0 }; NumberFormat { exp3, decimals: (exp3 - ts.exp).clamp(0, 12) as u8, } } /// The prefix symbol ("m", "k", "" …). pub fn prefix(&self) -> &'static str { si_prefix(self.exp3) } /// Writes `v` (in base units) into `out` after clearing it. pub fn write(&self, v: f64, out: &mut Label) { out.clear(); self.append(v, out); } /// Appends `v` (in base units) to `out`. pub fn append(&self, v: f64, out: &mut Label) { if !v.is_finite() { out.push('—'); return; } write_fixed(out, scale10(v, -self.exp3), self.decimals); } /// The same prefix with `extra` more decimals (a readout reads finer than /// the axis's ticks). pub fn finer(&self, extra: u8) -> NumberFormat { NumberFormat { exp3: self.exp3, decimals: (self.decimals + extra).min(12), } } /// `prefix + unit` into `out` ("mV"); a unit marked not SI ("dB", "%") /// gets no prefix, so the format must then have been made without one. pub fn unit_label(&self, unit: &str, out: &mut Label) { out.clear(); out.push_str(self.prefix()); out.push_str(unit); } } /// Writes `v` with `sig` significant digits and its own SI prefix (when /// `si`) into `out`, returning the prefix's power of ten. pub fn write_value(v: f64, sig: u8, si: bool, out: &mut Label) -> i32 { out.clear(); if !v.is_finite() { out.push('—'); return 0; } let exp3 = if si { si_exp3(fabs(v)) } else { 0 }; let s = scale10(v, -exp3); let l = if s == 0.0 { 0 } else { floor(log10(fabs(s)) + 1e-12) as i32 }; let decimals = (i32::from(sig.max(1)) - 1 - l).clamp(0, 12) as u8; write_fixed(out, s, decimals); exp3 } /// How a time axis reads. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum TimeStyle { /// Seconds from an origin, signed: a live scope's "−2.5 … 0" before now, /// or a triggered sweep's time from its trigger. In s, ms or µs; past two /// minutes, −m:ss. Relative, /// A clock from an origin (a recording's start): m:ss, m:ss.fff, h:mm:ss. Clock, } /// Steps for long time axes, in seconds. const TIME_STEPS: [u32; 15] = [ 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600, 7200, 10_800, 21_600, 43_200, 86_400, ]; /// About `target` ticks on a time axis from `lo` to `hi` seconds (both /// measured from the axis's origin): decimal steps below ten seconds, then /// 10 s, 15 s, 30 s, minutes and hours. pub fn time_ticks(lo: f64, hi: f64, target: u32) -> TickSet { let raw = (hi - lo) / f64::from(target.max(1)); if !(raw.is_finite() && raw > 0.0) { return TickSet::EMPTY; } if raw < 7.5 { return ticks(lo, hi, target); } let mut best = TIME_STEPS[0]; let mut err = f64::INFINITY; for &s in &TIME_STEPS { let d = fabs(libm::log(f64::from(s) / raw)); if d < err { err = d; best = s; } } span_ticks(lo, hi, best, 0) } /// How the labels of one time axis are written. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct TimeFormat { /// The style. pub style: TimeStyle, /// The seconds' prefix for decimal labels (0, −3 or −6). pub exp3: i32, /// Decimals (of seconds after the prefix, or of the clock's seconds). pub decimals: u8, /// Written as a clock: 0 no, 1 m:ss, 2 h:mm:ss. pub clock: u8, } impl TimeFormat { /// The format for a tick set from [`time_ticks`] (values from the origin). pub fn for_ticks(ts: &TickSet, style: TimeStyle) -> TimeFormat { let (a, b) = ts.bounds().unwrap_or((0.0, 0.0)); let maxabs = fabs(a).max(fabs(b)); let clock_decimals = (-ts.exp).clamp(0, 6) as u8; let clock_kind = if maxabs >= 3600.0 { 2 } else { 1 }; match style { TimeStyle::Relative if maxabs < 120.0 => { let exp3 = si_exp3(maxabs).clamp(-6, 0); TimeFormat { style, exp3, decimals: (exp3 - ts.exp).clamp(0, 9) as u8, clock: 0, } } _ => TimeFormat { style, exp3: 0, decimals: clock_decimals, clock: clock_kind, }, } } /// The unit for the axis title: "s", "ms", "µs", "m:ss" or "h:mm:ss". pub fn unit(&self) -> &'static str { match (self.clock, self.exp3) { (1, _) => "m:ss", (2, _) => "h:mm:ss", (_, -3) => "ms", (_, -6) => "µs", _ => "s", } } /// The same format with `extra` more decimals (the cursor's time). pub fn finer(&self, extra: u8) -> TimeFormat { let cap = if self.clock > 0 { 6 } else { 9 }; TimeFormat { decimals: (self.decimals + extra).min(cap), ..*self } } /// Writes `t` seconds from the origin into `out` after clearing it. pub fn write(&self, t: f64, out: &mut Label) { out.clear(); if !t.is_finite() { out.push('—'); return; } if self.clock == 0 { write_fixed(out, scale10(t, -self.exp3), self.decimals); return; } // Whole units of 10^-decimals seconds, split without float error. let unit = scale10(1.0, -i32::from(self.decimals)); let mut n = round(t / unit) as i64; if n < 0 { out.push('−'); n = -n; } let per_s = scale10(1.0, i32::from(self.decimals)) as i64; let frac = n % per_s; let secs = n / per_s; let (h, m, s) = (secs / 3600, (secs / 60) % 60, secs % 60); if self.clock == 2 { let _ = write!(out, "{h}:{m:02}:{s:02}"); } else { let _ = write!(out, "{}:{s:02}", secs / 60); } if self.decimals > 0 { let _ = write!(out, ".{:0width$}", frac, width = usize::from(self.decimals)); } } } #[cfg(test)] mod tests { use super::*; extern crate std; use std::vec::Vec; fn labels_num(ts: &TickSet, f: &NumberFormat) -> Vec { (0..ts.count) .map(|i| { let mut l = Label::new(); f.write(ts.value(i), &mut l); std::string::String::from(l.as_str()) }) .collect() } fn labels_time(ts: &TickSet, f: &TimeFormat) -> Vec { (0..ts.count) .map(|i| { let mut l = Label::new(); f.write(ts.value(i), &mut l); std::string::String::from(l.as_str()) }) .collect() } #[test] fn nice_steps_are_1_2_5() { assert_eq!(nice_step(10.0, 5), (2, 0)); assert_eq!(nice_step(1.0, 5), (2, -1)); assert_eq!(nice_step(7.0, 5), (1, 0)); assert_eq!(nice_step(0.003, 4), (1, -3)); assert_eq!(nice_step(100.0, 10), (1, 1)); assert_eq!(nice_step(1000.0, 10), (1, 2)); assert_eq!(nice_step(0.0, 5), (1, 0)); assert_eq!(nice_step(f64::NAN, 5), (1, 0)); // Every power of ten, both ways, lands on mantissa 1. for e in -12..=12 { assert_eq!(nice_step(scale10(5.0, e), 5), (1, e), "10^{e}"); } } #[test] fn tick_values_are_exact() { let ts = ticks(0.1, 0.35, 5); assert_eq!(ts.step(), 0.05); let v: Vec = (0..ts.count).map(|i| ts.value(i)).collect(); assert_eq!(v, [0.1, 0.15, 0.2, 0.25, 0.3, 0.35]); let ts = ticks(-1.0, 1.0, 5); let v: Vec = (0..ts.count).map(|i| ts.value(i)).collect(); assert_eq!(v, [-1.0, -0.5, 0.0, 0.5, 1.0]); // Bounds a hair inside a tick still include it. let ts = ticks(0.30000000000000004, 0.9, 6); assert_eq!(ts.value(0), 0.3); } #[test] fn bounds_widen_to_ticks() { assert_eq!(nice_bounds(0.13, 0.87, 5), (0.1, 0.9)); assert_eq!(nice_bounds(-3.2, 7.9, 5), (-4.0, 8.0)); assert_eq!(nice_bounds(2.0, 4.0, 4), (2.0, 4.0)); } #[test] fn si_prefixes_per_axis() { let ts = ticks(0.0, 0.002, 4); let f = NumberFormat::for_ticks(&ts, true); assert_eq!(f.prefix(), "m"); assert_eq!(labels_num(&ts, &f), ["0.0", "0.5", "1.0", "1.5", "2.0"]); let ts = ticks(0.0, 50_000.0, 5); let f = NumberFormat::for_ticks(&ts, true); assert_eq!(f.prefix(), "k"); assert_eq!(labels_num(&ts, &f), ["0", "10", "20", "30", "40", "50"]); let ts = ticks(0.0, 2000.0, 4); let f = NumberFormat::for_ticks(&ts, true); assert_eq!(f.prefix(), ""); assert_eq!(labels_num(&ts, &f), ["0", "500", "1000", "1500", "2000"]); let ts = ticks(-0.5, 0.5, 4); let f = NumberFormat::for_ticks(&ts, true); assert_eq!(f.prefix(), "m"); assert_eq!(labels_num(&ts, &f), ["−400", "−200", "0", "200", "400"]); let ts = ticks(-1.0, 1.0, 4); let f = NumberFormat::for_ticks(&ts, false); assert_eq!(labels_num(&ts, &f), ["−1.0", "−0.5", "0.0", "0.5", "1.0"]); let ts = ticks(0.0, 4e-7, 4); let f = NumberFormat::for_ticks(&ts, true); assert_eq!(f.prefix(), "n"); let mut u = Label::new(); f.unit_label("V", &mut u); assert_eq!(u, "nV"); assert_eq!(si_exp3(0.5), -3); assert_eq!(si_exp3(0.0005), -6); assert_eq!(si_exp3(9999.0), 0); assert_eq!(si_exp3(1e7), 6); assert_eq!(si_exp3(1e30), 15); } #[test] fn values_with_significant_digits() { let mut l = Label::new(); assert_eq!(write_value(0.0123456, 4, true, &mut l), -3); assert_eq!(l, "12.35"); assert_eq!(write_value(-1234.6, 4, true, &mut l), 0); assert_eq!(l, "−1235"); assert_eq!(write_value(98_765.0, 3, true, &mut l), 3); assert_eq!(l, "98.8"); write_value(f64::NAN, 3, true, &mut l); assert_eq!(l, "—"); write_value(-0.0, 3, false, &mut l); assert_eq!(l, "0.00"); } #[test] fn relative_time_axes() { let ts = time_ticks(-30.0, 0.0, 6); let f = TimeFormat::for_ticks(&ts, TimeStyle::Relative); assert_eq!(f.unit(), "s"); assert_eq!( labels_time(&ts, &f), ["−30", "−25", "−20", "−15", "−10", "−5", "0"] ); let ts = time_ticks(-0.1, 0.0, 5); let f = TimeFormat::for_ticks(&ts, TimeStyle::Relative); assert_eq!(f.unit(), "ms"); assert_eq!( labels_time(&ts, &f), ["−100", "−80", "−60", "−40", "−20", "0"] ); let ts = time_ticks(-0.025, 0.075, 4); let f = TimeFormat::for_ticks(&ts, TimeStyle::Relative); assert_eq!(labels_time(&ts, &f), ["−20", "0", "20", "40", "60"]); let ts = time_ticks(-2.5, 0.0, 5); let f = TimeFormat::for_ticks(&ts, TimeStyle::Relative); assert_eq!( labels_time(&ts, &f), ["−2.5", "−2.0", "−1.5", "−1.0", "−0.5", "0.0"] ); let ts = time_ticks(-300.0, 0.0, 5); let f = TimeFormat::for_ticks(&ts, TimeStyle::Relative); assert_eq!(f.unit(), "m:ss"); assert_eq!( labels_time(&ts, &f), ["−5:00", "−4:00", "−3:00", "−2:00", "−1:00", "0:00"] ); } #[test] fn clock_time_axes() { let ts = time_ticks(0.0, 300.0, 5); let f = TimeFormat::for_ticks(&ts, TimeStyle::Clock); assert_eq!( labels_time(&ts, &f), ["0:00", "1:00", "2:00", "3:00", "4:00", "5:00"] ); let ts = time_ticks(61.25, 61.75, 5); let f = TimeFormat::for_ticks(&ts, TimeStyle::Clock); assert_eq!( labels_time(&ts, &f), ["1:01.3", "1:01.4", "1:01.5", "1:01.6", "1:01.7"] ); let ts = time_ticks(3600.0, 7200.0, 6); let f = TimeFormat::for_ticks(&ts, TimeStyle::Clock); assert_eq!(f.unit(), "h:mm:ss"); assert_eq!(labels_time(&ts, &f)[..3], ["1:00:00", "1:10:00", "1:20:00"]); let ts = time_ticks(0.0, 0.01, 5); let f = TimeFormat::for_ticks(&ts, TimeStyle::Clock).finer(1); let mut l = Label::new(); f.write(0.0123456, &mut l); assert_eq!(l, "0:00.0123"); // Rounding never shows 60 seconds. let f = TimeFormat { style: TimeStyle::Clock, exp3: 0, decimals: 1, clock: 1, }; f.write(59.96, &mut l); assert_eq!(l, "1:00.0"); } }