Skip to main content

rich_ext/chart/
scale.rs

1//! A linear numeric scale with "nice" ticks, and how values are written.
2
3use crate::format;
4
5/// A linear scale from [`min`](Self::min) to [`max`](Self::max).
6///
7/// Every chart maps its values through one. [`Scale::from_values`] skips NaN
8/// and infinite values and never returns an empty range, so the charts never
9/// divide by zero:
10///
11/// - no finite values: `0..1`;
12/// - every value equal to `v`: `v - |v| .. v + |v|`, so `v` sits in the
13///   middle (`0..1` when `v` is 0);
14/// - otherwise the smallest and largest value.
15///
16/// ```
17/// use rich_ext::chart::Scale;
18///
19/// let scale = Scale::from_values([3.0, f64::NAN, 17.0, 9.0]);
20/// assert_eq!((scale.min(), scale.max()), (3.0, 17.0));
21/// assert_eq!(scale.nice(5).ticks(5), vec![0.0, 5.0, 10.0, 15.0, 20.0]);
22/// assert_eq!(scale.normalize(10.0), Some(0.5));
23///
24/// // Explicit bounds win, in either order.
25/// assert_eq!(Scale::new(10.0, -10.0).normalize(0.0), Some(0.5));
26/// ```
27#[derive(Clone, Copy, Debug, PartialEq)]
28#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
29pub struct Scale {
30    min: f64,
31    max: f64,
32}
33
34impl Default for Scale {
35    fn default() -> Self {
36        Scale { min: 0.0, max: 1.0 }
37    }
38}
39
40impl Scale {
41    /// A scale between two bounds, in either order. Non-finite bounds fall
42    /// back to `0..1`; equal bounds are widened as in
43    /// [`from_values`](Self::from_values).
44    pub fn new(a: f64, b: f64) -> Self {
45        if !a.is_finite() || !b.is_finite() {
46            return Scale::default();
47        }
48        let (min, max) = if a <= b { (a, b) } else { (b, a) };
49        if min == max {
50            return Self::degenerate(min);
51        }
52        Scale { min, max }
53    }
54
55    fn degenerate(v: f64) -> Self {
56        if v == 0.0 {
57            Scale { min: 0.0, max: 1.0 }
58        } else {
59            Scale {
60                min: v - v.abs(),
61                max: v + v.abs(),
62            }
63        }
64    }
65
66    /// The range of the finite values in `values`.
67    pub fn from_values(values: impl IntoIterator<Item = f64>) -> Self {
68        let mut range: Option<(f64, f64)> = None;
69        for v in values.into_iter().filter(|v| v.is_finite()) {
70            range = Some(match range {
71                None => (v, v),
72                Some((lo, hi)) => (lo.min(v), hi.max(v)),
73            });
74        }
75        match range {
76            None => Scale::default(),
77            Some((lo, hi)) => Scale::new(lo, hi),
78        }
79    }
80
81    /// Override either bound. A bound left `None` keeps its value; if the
82    /// result is empty or reversed it is fixed up as [`new`](Self::new) does.
83    pub fn bounds(self, min: Option<f64>, max: Option<f64>) -> Self {
84        let min = min.filter(|v| v.is_finite());
85        let max = max.filter(|v| v.is_finite());
86        let width = self.span().abs().max(1.0);
87        match (min, max) {
88            // Only one bound was given and it passed the other: keep the
89            // given one and widen away from it.
90            (Some(lo), None) if lo > self.max => Scale::new(lo, lo + width),
91            (None, Some(hi)) if hi < self.min => Scale::new(hi - width, hi),
92            (lo, hi) => Scale::new(lo.unwrap_or(self.min), hi.unwrap_or(self.max)),
93        }
94    }
95
96    /// Stretch the scale so it contains 0, as bars need.
97    pub fn include_zero(self) -> Self {
98        Scale::new(self.min.min(0.0), self.max.max(0.0))
99    }
100
101    /// Widen the bounds outward to multiples of the tick step that
102    /// [`ticks`](Self::ticks) would use for `count` ticks.
103    pub fn nice(self, count: usize) -> Self {
104        let step = self.step(count);
105        let min = (self.min / step).floor() * step;
106        let max = (self.max / step).ceil() * step;
107        Scale::new(clean(min), clean(max))
108    }
109
110    /// The lower bound.
111    pub fn min(&self) -> f64 {
112        self.min
113    }
114
115    /// The upper bound.
116    pub fn max(&self) -> f64 {
117        self.max
118    }
119
120    /// `max - min`, always above zero. Bounds near `±f64::MAX` would
121    /// overflow it; it is then `f64::MAX`.
122    pub fn span(&self) -> f64 {
123        let span = self.max - self.min;
124        if span.is_finite() { span } else { f64::MAX }
125    }
126
127    /// Where `value` falls, from 0 (at `min`) to 1 (at `max`), clamped.
128    /// `None` for NaN and infinities.
129    pub fn normalize(&self, value: f64) -> Option<f64> {
130        if !value.is_finite() {
131            return None;
132        }
133        let span = self.max - self.min;
134        let at = if span.is_finite() {
135            (value - self.min) / span
136        } else {
137            // Halved, so neither difference overflows.
138            (value * 0.5 - self.min * 0.5) / (self.max * 0.5 - self.min * 0.5)
139        };
140        Some(at.clamp(0.0, 1.0))
141    }
142
143    /// The value a fraction `t` of the way from `min` to `max` (0 gives
144    /// `min`, 1 gives `max`), without overflowing.
145    pub(crate) fn lerp(&self, t: f64) -> f64 {
146        let span = self.max - self.min;
147        if span.is_finite() {
148            self.min + t * span
149        } else {
150            self.min * (1.0 - t) + self.max * t
151        }
152    }
153
154    /// The tick step for about `count` ticks: 1, 2 or 5 times a power of ten.
155    pub fn step(&self, count: usize) -> f64 {
156        let intervals = count.max(2) - 1;
157        nice_number(self.span() / intervals as f64, true)
158    }
159
160    /// Round values inside the scale, about `count` of them (at least 2),
161    /// spaced by [`step`](Self::step). They include the bounds only when the
162    /// bounds are themselves multiples of the step; use [`nice`](Self::nice)
163    /// first for ticks that span the whole scale.
164    pub fn ticks(&self, count: usize) -> Vec<f64> {
165        self.ticks_every(self.step(count))
166    }
167
168    /// The multiples of `step` inside the scale. After [`nice`](Self::nice)
169    /// the range is wider, so its own [`step`](Self::step) may differ; pass
170    /// the step of the range before widening to keep the ticks the bounds
171    /// were rounded to.
172    pub fn ticks_every(&self, step: f64) -> Vec<f64> {
173        if !(step.is_finite() && step > 0.0) || self.span() / step > 10_000.0 {
174            return vec![self.min, self.max];
175        }
176        let first = (self.min / step - 1e-9).ceil() as i64;
177        let last = (self.max / step + 1e-9).floor() as i64;
178        (first..=last).map(|k| clean(k as f64 * step)).collect()
179    }
180}
181
182/// Heckbert's "nice number": 1, 2, 5 or 10 times a power of ten near `x`.
183fn nice_number(x: f64, round: bool) -> f64 {
184    if !(x.is_finite() && x > 0.0) {
185        return 1.0;
186    }
187    let exponent = x.log10().floor();
188    let power = 10f64.powf(exponent);
189    let fraction = x / power;
190    let nice = if round {
191        match fraction {
192            f if f < 1.5 => 1.0,
193            f if f < 3.0 => 2.0,
194            f if f < 7.0 => 5.0,
195            _ => 10.0,
196        }
197    } else {
198        match fraction {
199            f if f <= 1.0 => 1.0,
200            f if f <= 2.0 => 2.0,
201            f if f <= 5.0 => 5.0,
202            _ => 10.0,
203        }
204    };
205    nice * power
206}
207
208/// `lo + k * unit`, without overflowing on the way when the result itself
209/// is finite.
210pub(crate) fn along(lo: f64, k: f64, unit: f64) -> f64 {
211    let offset = k * unit;
212    let value = lo + offset;
213    if value.is_finite() || !lo.is_finite() || !unit.is_finite() {
214        value
215    } else {
216        (lo * 0.5 + k * (unit * 0.5)) * 2.0
217    }
218}
219
220/// `(value - lo) / unit`: how many `unit`s `value` is past `lo`, without
221/// overflowing on the way.
222pub(crate) fn units_past(value: f64, lo: f64, unit: f64) -> f64 {
223    let diff = value - lo;
224    if diff.is_finite() {
225        diff / unit
226    } else {
227        (value * 0.5 - lo * 0.5) / unit * 2.0
228    }
229}
230
231/// Drop floating-point noise (`0.30000000000000004`) and negative zero,
232/// keeping twelve significant digits whatever the magnitude.
233pub(crate) fn clean(v: f64) -> f64 {
234    if !v.is_finite() {
235        return v;
236    }
237    let rounded: f64 = format!("{v:.11e}").parse().unwrap_or(v);
238    if rounded == 0.0 { 0.0 } else { rounded }
239}
240
241/// How a chart writes values on axes, bars and summaries.
242///
243/// ```
244/// use rich_ext::chart::ValueFormat;
245///
246/// assert_eq!(ValueFormat::Compact.format(1_234.0), "1.2k");
247/// assert_eq!(ValueFormat::Compact.format(3_400_000.0), "3.4M");
248/// assert_eq!(ValueFormat::Compact.format(0.25), "0.25");
249/// assert_eq!(ValueFormat::Fixed(1).format(2.0), "2.0");
250/// ```
251#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
252pub enum ValueFormat {
253    /// Short forms: `950`, `0.25`, `12.5`, then `1.2k`, `3.4M`, `5.0B`
254    /// ([`format::compact`] from a thousand up, at most two decimals
255    /// below), and `1.0e15` from a thousand trillion. A chart axis or
256    /// histogram whose round values this cannot write exactly (`2015`
257    /// would read `2.0k`) writes them in full instead.
258    #[default]
259    Compact,
260    /// A fixed number of decimals: `Fixed(2)` writes `3.14`. At most
261    /// [`MAX_DECIMALS`](Self::MAX_DECIMALS) are written; more are capped.
262    Fixed(usize),
263}
264
265impl ValueFormat {
266    /// The most decimals [`Fixed`](Self::Fixed) writes: an `f64` holds
267    /// at most 17 significant digits.
268    pub const MAX_DECIMALS: usize = 17;
269
270    /// `value` in this format. NaN and infinities are written `-`.
271    pub fn format(self, value: f64) -> String {
272        if !value.is_finite() {
273            return "-".to_string();
274        }
275        let value = if value == 0.0 { 0.0 } else { value };
276        match self {
277            ValueFormat::Fixed(decimals) => {
278                let decimals = decimals.min(Self::MAX_DECIMALS);
279                let text = format!("{value:.decimals$}");
280                // `-0.0` rounds to "-0.0"; write it as zero.
281                if text
282                    .trim_start_matches('-')
283                    .chars()
284                    .all(|c| c == '0' || c == '.')
285                {
286                    text.trim_start_matches('-').to_string()
287                } else {
288                    text
289                }
290            }
291            ValueFormat::Compact => {
292                if value.abs() >= 1e15 {
293                    // Past `T`, compact would write every digit.
294                    return format!("{value:.1e}");
295                }
296                if value.abs() >= 1000.0 {
297                    return format::compact(value);
298                }
299                let text = format!("{value:.2}");
300                let text = text.trim_end_matches('0').trim_end_matches('.');
301                if text == "-0" {
302                    "0".to_string()
303                } else {
304                    text.to_string()
305                }
306            }
307        }
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    #[test]
316    fn ticks_are_round_numbers_inside_the_scale() {
317        let s = Scale::new(0.0, 100.0);
318        // 100 / 4 = 25 rounds to a step of 20.
319        assert_eq!(s.ticks(5), vec![0.0, 20.0, 40.0, 60.0, 80.0, 100.0]);
320        assert_eq!(s.ticks(6), vec![0.0, 20.0, 40.0, 60.0, 80.0, 100.0]);
321        assert_eq!(s.ticks(3), vec![0.0, 50.0, 100.0]);
322        assert_eq!(
323            Scale::new(0.0, 1.0).ticks(5),
324            vec![0.0, 0.2, 0.4, 0.6, 0.8, 1.0]
325        );
326        assert_eq!(Scale::new(3.0, 17.0).ticks(4), vec![5.0, 10.0, 15.0]);
327        assert_eq!(Scale::new(0.1, 0.35).ticks(3), vec![0.1, 0.2, 0.3]);
328    }
329
330    #[test]
331    fn nice_widens_to_whole_steps() {
332        let s = Scale::new(3.0, 17.0).nice(5);
333        assert_eq!((s.min(), s.max()), (0.0, 20.0));
334        assert_eq!(s.ticks(5), vec![0.0, 5.0, 10.0, 15.0, 20.0]);
335        // The ticks of the widened range at the step it was rounded to.
336        let data = Scale::new(1.0, 5.0);
337        let wide = data.nice(3);
338        assert_eq!((wide.min(), wide.max()), (0.0, 6.0));
339        assert_eq!(wide.ticks_every(data.step(3)), vec![0.0, 2.0, 4.0, 6.0]);
340        assert_eq!(wide.ticks(3), vec![0.0, 5.0]);
341        assert_eq!(wide.ticks_every(0.0), vec![0.0, 6.0]);
342        let s = Scale::new(-7.0, 42.0).nice(5);
343        assert_eq!((s.min(), s.max()), (-10.0, 50.0));
344        // Already nice: unchanged.
345        let s = Scale::new(0.0, 100.0).nice(5);
346        assert_eq!((s.min(), s.max()), (0.0, 100.0));
347    }
348
349    #[test]
350    fn a_single_bound_past_the_data_is_kept() {
351        let data = Scale::from_values([150.0, 200.0]);
352        let s = data.bounds(None, Some(100.0));
353        assert_eq!((s.min(), s.max()), (50.0, 100.0));
354        let s = data.bounds(Some(300.0), None);
355        assert_eq!((s.min(), s.max()), (300.0, 350.0));
356        // Both given, in either order.
357        let s = data.bounds(Some(10.0), Some(0.0));
358        assert_eq!((s.min(), s.max()), (0.0, 10.0));
359    }
360
361    #[test]
362    fn tiny_scales_keep_their_magnitude() {
363        let s = Scale::new(1e-12, 5e-12).nice(5);
364        assert_eq!((s.min(), s.max()), (1e-12, 5e-12));
365        assert_eq!(s.ticks(5), vec![1e-12, 2e-12, 3e-12, 4e-12, 5e-12]);
366        assert_eq!(clean(0.1 + 0.2), 0.3);
367        assert_eq!(clean(-0.0).to_bits(), 0.0f64.to_bits());
368    }
369
370    #[test]
371    fn degenerate_ranges_never_collapse() {
372        let zero = Scale::from_values([0.0, 0.0, 0.0]);
373        assert_eq!((zero.min(), zero.max()), (0.0, 1.0));
374        let five = Scale::from_values([5.0; 4]);
375        assert_eq!((five.min(), five.max()), (0.0, 10.0));
376        assert_eq!(five.normalize(5.0), Some(0.5));
377        let neg = Scale::from_values([-4.0]);
378        assert_eq!((neg.min(), neg.max()), (-8.0, 0.0));
379        let empty = Scale::from_values(std::iter::empty());
380        assert_eq!((empty.min(), empty.max()), (0.0, 1.0));
381        assert_eq!(Scale::new(2.0, 2.0).span(), 4.0);
382        assert!(Scale::new(f64::NAN, 3.0) == Scale::default());
383        for s in [zero, five, neg, empty] {
384            assert!(s.span() > 0.0);
385            assert!(!s.ticks(5).is_empty());
386        }
387    }
388
389    #[test]
390    fn negative_values_and_zero() {
391        let s = Scale::from_values([-30.0, -5.0, 20.0]);
392        assert_eq!((s.min(), s.max()), (-30.0, 20.0));
393        assert_eq!(s.ticks(6), vec![-30.0, -20.0, -10.0, 0.0, 10.0, 20.0]);
394        assert_eq!(s.normalize(-30.0), Some(0.0));
395        assert_eq!(s.normalize(-5.0), Some(0.5));
396        let pos = Scale::from_values([4.0, 9.0]).include_zero();
397        assert_eq!((pos.min(), pos.max()), (0.0, 9.0));
398        let neg = Scale::from_values([-4.0, -9.0]).include_zero();
399        assert_eq!((neg.min(), neg.max()), (-9.0, 0.0));
400        // No "-0" ticks.
401        assert!(
402            Scale::new(-1.0, 1.0)
403                .ticks(5)
404                .iter()
405                .all(|t| t.to_string() != "-0")
406        );
407    }
408
409    #[test]
410    fn nan_and_infinities_are_skipped() {
411        let s = Scale::from_values([f64::NAN, 1.0, f64::INFINITY, 3.0, f64::NEG_INFINITY]);
412        assert_eq!((s.min(), s.max()), (1.0, 3.0));
413        assert_eq!(s.normalize(f64::NAN), None);
414        assert_eq!(s.normalize(f64::INFINITY), None);
415        assert_eq!(Scale::from_values([f64::NAN]), Scale::default());
416    }
417
418    #[test]
419    fn explicit_bounds_override_and_clamp() {
420        let s = Scale::from_values([3.0, 7.0]).bounds(Some(0.0), Some(10.0));
421        assert_eq!((s.min(), s.max()), (0.0, 10.0));
422        assert_eq!(s.normalize(15.0), Some(1.0));
423        assert_eq!(s.normalize(-5.0), Some(0.0));
424        let only_max = Scale::from_values([3.0, 7.0]).bounds(None, Some(100.0));
425        assert_eq!((only_max.min(), only_max.max()), (3.0, 100.0));
426        // A min above the data's max still gives a usable scale.
427        let past = Scale::from_values([3.0, 7.0]).bounds(Some(50.0), None);
428        assert_eq!(past.min(), 50.0);
429        assert!(past.span() > 0.0);
430    }
431
432    #[test]
433    fn value_formats() {
434        let c = ValueFormat::Compact;
435        assert_eq!(c.format(0.0), "0");
436        assert_eq!(c.format(-0.0), "0");
437        assert_eq!(c.format(42.0), "42");
438        assert_eq!(c.format(12.5), "12.5");
439        assert_eq!(c.format(0.126), "0.13");
440        assert_eq!(c.format(-3.0), "-3");
441        assert_eq!(c.format(1_250.0), "1.2k");
442        assert_eq!(c.format(-3_400_000.0), "-3.4M");
443        assert_eq!(c.format(f64::NAN), "-");
444        assert_eq!(c.format(-0.001), "0");
445        let f = ValueFormat::Fixed(2);
446        assert_eq!(f.format(1.23456), "1.23");
447        assert_eq!(f.format(-0.001), "0.00");
448        assert_eq!(ValueFormat::Fixed(0).format(2.6), "3");
449    }
450}