axis_ticks/
lib.rs

1/*!
2A port of [`d3-ticks`](https://observablehq.com/@d3/d3-ticks), part of the JavaScript D3 plotting library.
3
4Generates an array of nicely rounded values between two numbers for which are ideal for positioning axis labels and grid-lines.
5```
6let ticks = axis_ticks::ticks(-0.125, 0.25, 10);
7
8assert_eq!(
9    ticks,
10    [-0.15, -0.1, -0.05, 0.0, 0.05, 0.1, 0.15, 0.2, 0.25]
11);
12```
13*/
14
15use num_traits::{
16    cast::FromPrimitive,
17    float::{Float, FloatConst},
18};
19
20pub fn ticks<T: Float + FloatConst + FromPrimitive>(start: T, stop: T, count: usize) -> Vec<T> {
21    if start == stop && count > 0 {
22        return vec![start];
23    }
24
25    let reverse = stop < start;
26    let (start, stop) = if reverse {
27        (stop, start)
28    } else {
29        (start, stop)
30    };
31
32    let step = tick_increment(start, stop, count);
33    if step.is_zero() || !step.is_finite() {
34        return vec![];
35    }
36
37    let mut ticks = if step.is_sign_positive() {
38        let start: T = (start / step).ceil();
39        let stop: T = (stop / step).floor();
40        let n = (stop - start + T::from_f64(1.0).unwrap())
41            .ceil()
42            .to_usize()
43            .unwrap();
44        let mut ticks = vec![T::from_f64(0.0).unwrap(); n];
45        for i in 0..n {
46            ticks[i] = (start + T::from_usize(i).unwrap()) * step;
47        }
48        ticks
49    } else {
50        let step = step * T::from_f64(-1.0).unwrap();
51        let start = (start * step).floor();
52        let stop = (stop * step).ceil();
53        let n = (stop - start + T::from_f64(1.0).unwrap())
54            .ceil()
55            .to_usize()
56            .unwrap();
57        let mut ticks = vec![T::from_f64(0.0).unwrap(); n];
58        for i in 0..n {
59            ticks[i] = (start + T::from_usize(i).unwrap()) / step;
60        }
61        ticks
62    };
63
64    if reverse {
65        ticks.reverse()
66    }
67
68    ticks
69}
70
71fn tick_increment<T: Float + FloatConst + FromPrimitive>(start: T, stop: T, count: usize) -> T {
72    let step = (stop - start) / T::from_usize(count).unwrap();
73    let power = (step.ln() / T::LN_10()).floor();
74    let error = step / T::from_f64(10.0).unwrap().powf(power);
75
76    let v = if error >= T::from_f64(50.0).unwrap().sqrt() {
77        T::from_f64(10.0).unwrap()
78    } else if error >= T::from_f64(10.0).unwrap().sqrt() {
79        T::from_f64(5.0).unwrap()
80    } else if error >= T::from_f64(2.0).unwrap().sqrt() {
81        T::from_f64(2.0).unwrap()
82    } else {
83        T::from_f64(1.0).unwrap()
84    };
85
86    if power >= T::from_f64(0.0).unwrap() {
87        v * T::from_f64(10.0).unwrap().powf(power)
88    } else {
89        (T::from_f64(-1.0).unwrap()
90            * T::from_f64(10.0)
91                .unwrap()
92                .powf(power * T::from_f64(-1.0).unwrap()))
93            / v
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn returns_empty_vec_if_any_argument_is_nan() {
103        assert_eq!(ticks(f32::NAN, 1.0, 1), []);
104        assert_eq!(ticks(0.0, f32::NAN, 1), []);
105        assert_eq!(ticks(f32::NAN, f32::NAN, 1), []);
106    }
107
108    #[test]
109    fn returns_the_empty_vec_if_start_equal_stop() {
110        assert_eq!(ticks(1.0, 1.0, 0), []);
111    }
112
113    #[test]
114    fn returns_the_empty_vec_if_count_is_not_positive() {
115        assert_eq!(ticks(0.0, 1.0, 0), []);
116    }
117
118    #[test]
119    fn returns_approximately_count_plus_1_ticks_when_start_less_than_stop() {
120        assert_eq!(
121            ticks(0.0f32, 1.0f32, 10),
122            [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
123        );
124        assert_eq!(
125            ticks(0.0f64, 1.0f64, 10),
126            [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
127        );
128        assert_eq!(
129            ticks(0.0, 1.0, 8),
130            [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
131        );
132        assert_eq!(ticks(0.0, 1.0, 7), [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]);
133        assert_eq!(ticks(0.0, 1.0, 6), [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]);
134        assert_eq!(ticks(0.0, 1.0, 5), [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]);
135        assert_eq!(ticks(0.0, 1.0, 4), [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]);
136        assert_eq!(ticks(0.0, 1.0, 3), [0.0, 0.5, 1.0]);
137        assert_eq!(ticks(0.0, 1.0, 2), [0.0, 0.5, 1.0]);
138        assert_eq!(ticks(0.0, 1.0, 1), [0.0, 1.0]);
139        assert_eq!(
140            ticks(0.0, 10.0, 10),
141            [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
142        );
143        assert_eq!(
144            ticks(0.0, 10.0, 9),
145            [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
146        );
147        assert_eq!(
148            ticks(0.0, 10.0, 8),
149            [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
150        );
151        assert_eq!(ticks(0.0, 10.0, 7), [0.0, 2.0, 4.0, 6.0, 8.0, 10.0]);
152        assert_eq!(ticks(0.0, 10.0, 6), [0.0, 2.0, 4.0, 6.0, 8.0, 10.0]);
153        assert_eq!(ticks(0.0, 10.0, 5), [0.0, 2.0, 4.0, 6.0, 8.0, 10.0]);
154        assert_eq!(ticks(0.0, 10.0, 4), [0.0, 2.0, 4.0, 6.0, 8.0, 10.0]);
155        assert_eq!(ticks(0.0, 10.0, 3), [0.0, 5.0, 10.0]);
156        assert_eq!(ticks(0.0, 10.0, 2), [0.0, 5.0, 10.0]);
157        assert_eq!(ticks(0.0, 10.0, 1), [0.0, 10.0]);
158        assert_eq!(
159            ticks(-10.0, 10.0, 10),
160            [-10.0, -8.0, -6.0, -4.0, -2.0, 0.0, 2.0, 4.0, 6.0, 8.0, 10.0]
161        );
162        assert_eq!(
163            ticks(-10.0, 10.0, 9),
164            [-10.0, -8.0, -6.0, -4.0, -2.0, 0.0, 2.0, 4.0, 6.0, 8.0, 10.0]
165        );
166        assert_eq!(
167            ticks(-10.0, 10.0, 8),
168            [-10.0, -8.0, -6.0, -4.0, -2.0, 0.0, 2.0, 4.0, 6.0, 8.0, 10.0]
169        );
170        assert_eq!(
171            ticks(-10.0, 10.0, 7),
172            [-10.0, -8.0, -6.0, -4.0, -2.0, 0.0, 2.0, 4.0, 6.0, 8.0, 10.0]
173        );
174        assert_eq!(ticks(-10.0, 10.0, 6), [-10.0, -5.0, 0.0, 5.0, 10.0]);
175        assert_eq!(ticks(-10.0, 10.0, 5), [-10.0, -5.0, 0.0, 5.0, 10.0]);
176        assert_eq!(ticks(-10.0, 10.0, 4), [-10.0, -5.0, 0.0, 5.0, 10.0]);
177        assert_eq!(ticks(-10.0, 10.0, 3), [-10.0, -5.0, 0.0, 5.0, 10.0]);
178        assert_eq!(ticks(-10.0, 10.0, 2), [-10.0, 0.0, 10.0]);
179        assert_eq!(ticks(-10.0, 10.0, 1), [0.0]);
180    }
181
182    #[test]
183    fn some_more_complex_tests() {
184        assert_eq!(
185            ticks(0.0, 1.0, 20),
186            [
187                0.0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7,
188                0.75, 0.8, 0.85, 0.9, 0.95, 1.0
189            ]
190        );
191
192        assert_eq!(
193            ticks(0.125, 0.25, 5),
194            [0.12, 0.14, 0.16, 0.18, 0.2, 0.22, 0.24, 0.26]
195        );
196
197        assert_eq!(
198            ticks(0.125, 0.25, 10),
199            [0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18, 0.19, 0.2, 0.21, 0.22, 0.23, 0.24, 0.25]
200        );
201
202        assert_eq!(
203            ticks(-0.125, 0.25, 10),
204            [-0.15, -0.1, -0.05, 0.0, 0.05, 0.1, 0.15, 0.2, 0.25]
205        );
206    }
207}