d3rs/array/
ticks.rs

1//! Tick generation utilities
2//!
3//! Provides functions for generating nice tick values for axes and scales.
4//! Consolidates tick generation from scale/ticks.rs and adds additional utilities.
5
6/// Generate nice linear ticks for a given domain.
7///
8/// Uses Wilkinson's algorithm to generate approximately `count` tick values
9/// that are "nice" (round numbers).
10///
11/// # Example
12///
13/// ```
14/// use d3rs::array::ticks;
15///
16/// let t = ticks(0.0, 100.0, 5);
17/// assert!(t.len() >= 5);
18/// assert!(t[0] <= 0.0);
19/// assert!(*t.last().unwrap() >= 100.0);
20/// ```
21pub fn ticks(start: f64, stop: f64, count: usize) -> Vec<f64> {
22    if count == 0 || start == stop {
23        return vec![start];
24    }
25
26    let (start, stop, reverse) = if start > stop {
27        (stop, start, true)
28    } else {
29        (start, stop, false)
30    };
31
32    let step = tick_step(start, stop, count);
33    if step == 0.0 || !step.is_finite() {
34        return vec![start];
35    }
36
37    let tick_start = (start / step).ceil() * step;
38    let tick_stop = (stop / step).floor() * step;
39
40    let n = ((tick_stop - tick_start) / step).round() as usize + 1;
41    let mut result: Vec<f64> = (0..n).map(|i| tick_start + step * i as f64).collect();
42
43    if reverse {
44        result.reverse();
45    }
46
47    result
48}
49
50/// Compute the step size for generating approximately `count` ticks.
51///
52/// # Example
53///
54/// ```
55/// use d3rs::array::tick_step;
56///
57/// let step = tick_step(0.0, 100.0, 10);
58/// assert_eq!(step, 10.0);
59/// ```
60pub fn tick_step(start: f64, stop: f64, count: usize) -> f64 {
61    if count == 0 {
62        return 0.0;
63    }
64
65    let step0 = (stop - start).abs() / count as f64;
66    let step1 = 10_f64.powf(step0.log10().floor());
67
68    let error = step0 / step1;
69
70    if error >= 10.0_f64.sqrt() * 5.0 {
71        step1 * 10.0
72    } else if error >= 10.0_f64.sqrt() * 2.0 {
73        step1 * 5.0
74    } else if error >= 10.0_f64.sqrt() {
75        step1 * 2.0
76    } else {
77        step1
78    }
79}
80
81/// Increment a nice step size to the next larger nice value.
82///
83/// Nice values are powers of 10, times 2 or 5.
84pub fn tick_increment(start: f64, stop: f64, count: usize) -> f64 {
85    let step = (stop - start) / count.max(1) as f64;
86    let power = step.log10().floor();
87    let error = step / 10_f64.powf(power);
88
89    if error >= 10.0_f64.sqrt() * 5.0 {
90        10_f64.powf(power + 1.0)
91    } else if error >= 10.0_f64.sqrt() * 2.0 {
92        5.0 * 10_f64.powf(power)
93    } else if error >= 10.0_f64.sqrt() {
94        2.0 * 10_f64.powf(power)
95    } else {
96        10_f64.powf(power)
97    }
98}
99
100/// Extend the domain to nice round values.
101///
102/// Returns (nice_start, nice_stop) that encompass the original domain
103/// and align with the tick step.
104///
105/// # Example
106///
107/// ```
108/// use d3rs::array::nice;
109///
110/// let (start, stop) = nice(0.134, 0.867, 5);
111/// assert!(start <= 0.134);
112/// assert!(stop >= 0.867);
113/// ```
114pub fn nice(start: f64, stop: f64, count: usize) -> (f64, f64) {
115    if start == stop {
116        return (start, stop);
117    }
118
119    let (start, stop, reverse) = if start > stop {
120        (stop, start, true)
121    } else {
122        (start, stop, false)
123    };
124
125    let step = tick_increment(start, stop, count);
126    if step == 0.0 || !step.is_finite() {
127        return if reverse {
128            (stop, start)
129        } else {
130            (start, stop)
131        };
132    }
133
134    let nice_start = (start / step).floor() * step;
135    let nice_stop = (stop / step).ceil() * step;
136
137    if reverse {
138        (nice_stop, nice_start)
139    } else {
140        (nice_start, nice_stop)
141    }
142}
143
144/// Find a "nice" number approximately equal to the range.
145///
146/// Nice numbers are 1, 2, or 5 times a power of 10.
147/// If `round` is true, rounds the number, otherwise takes the ceiling.
148///
149/// # Example
150///
151/// ```
152/// use d3rs::array::nice_number;
153///
154/// assert_eq!(nice_number(10.0, false), 10.0);
155/// assert_eq!(nice_number(15.0, false), 20.0);
156/// assert_eq!(nice_number(25.0, false), 50.0);
157/// ```
158pub fn nice_number(range: f64, round: bool) -> f64 {
159    if range == 0.0 {
160        return 0.0;
161    }
162
163    let exponent = range.abs().log10().floor();
164    let fraction = range.abs() / 10_f64.powf(exponent);
165
166    let nice_fraction = if round {
167        if fraction < 1.5 {
168            1.0
169        } else if fraction < 3.0 {
170            2.0
171        } else if fraction < 7.0 {
172            5.0
173        } else {
174            10.0
175        }
176    } else if fraction <= 1.0 {
177        1.0
178    } else if fraction <= 2.0 {
179        2.0
180    } else if fraction <= 5.0 {
181        5.0
182    } else {
183        10.0
184    };
185
186    nice_fraction * 10_f64.powf(exponent) * range.signum()
187}
188
189/// Generate nice logarithmic ticks for a given domain.
190///
191/// Generates ticks at powers of the base, with optional subdivisions.
192///
193/// # Example
194///
195/// ```
196/// use d3rs::array::log_ticks;
197///
198/// let t = log_ticks(1.0, 1000.0, 10.0, false);
199/// assert_eq!(t, vec![1.0, 10.0, 100.0, 1000.0]);
200/// ```
201pub fn log_ticks(min: f64, max: f64, base: f64, subdivisions: bool) -> Vec<f64> {
202    if min <= 0.0 || max <= 0.0 || base <= 1.0 {
203        return vec![];
204    }
205
206    let log_min = min.log(base).floor();
207    let log_max = max.log(base).ceil();
208
209    let mut ticks = Vec::new();
210
211    let mut exp = log_min;
212    while exp <= log_max {
213        let tick = base.powf(exp);
214
215        if tick >= min && tick <= max {
216            ticks.push(tick);
217        }
218
219        // Add subdivisions (e.g., 20, 30, ..., 90 for base 10)
220        if subdivisions && exp < log_max {
221            for i in 2..base as i32 {
222                let sub_tick = tick * i as f64;
223                if sub_tick >= min && sub_tick <= max {
224                    ticks.push(sub_tick);
225                }
226            }
227        }
228
229        exp += 1.0;
230    }
231
232    ticks.sort_by(|a, b| a.partial_cmp(b).unwrap());
233    ticks
234}
235
236/// Generate ticks at specific intervals (e.g., every 5, 10, etc.).
237///
238/// # Example
239///
240/// ```
241/// use d3rs::array::ticks_interval;
242///
243/// let t = ticks_interval(0.0, 100.0, 20.0);
244/// assert_eq!(t, vec![0.0, 20.0, 40.0, 60.0, 80.0, 100.0]);
245/// ```
246pub fn ticks_interval(start: f64, stop: f64, interval: f64) -> Vec<f64> {
247    if interval <= 0.0 || start == stop {
248        return vec![start];
249    }
250
251    let tick_start = (start / interval).ceil() * interval;
252    let tick_stop = (stop / interval).floor() * interval;
253
254    let n = ((tick_stop - tick_start) / interval).round() as usize + 1;
255    (0..n).map(|i| tick_start + interval * i as f64).collect()
256}
257
258/// Generate date/time ticks (placeholder for future implementation).
259///
260/// For now, this just returns ticks for numeric timestamps.
261pub fn time_ticks(start: f64, stop: f64, count: usize) -> Vec<f64> {
262    ticks(start, stop, count)
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268
269    #[test]
270    fn test_ticks() {
271        let t = ticks(0.0, 100.0, 5);
272        assert!(t.len() >= 5);
273        assert!(t[0] <= 0.0);
274        assert!(*t.last().unwrap() >= 100.0);
275    }
276
277    #[test]
278    fn test_tick_step() {
279        assert_eq!(tick_step(0.0, 100.0, 10), 10.0);
280        assert_eq!(tick_step(0.0, 1.0, 10), 0.1);
281    }
282
283    #[test]
284    fn test_nice() {
285        let (start, stop) = nice(0.134, 0.867, 5);
286        assert!(start <= 0.134);
287        assert!(stop >= 0.867);
288    }
289
290    #[test]
291    fn test_nice_number() {
292        assert_eq!(nice_number(10.0, false), 10.0);
293        assert_eq!(nice_number(15.0, false), 20.0);
294        assert_eq!(nice_number(25.0, false), 50.0);
295        assert_eq!(nice_number(75.0, false), 100.0);
296    }
297
298    #[test]
299    fn test_log_ticks() {
300        let t = log_ticks(1.0, 1000.0, 10.0, false);
301        assert_eq!(t, vec![1.0, 10.0, 100.0, 1000.0]);
302    }
303
304    #[test]
305    fn test_log_ticks_subdivisions() {
306        let t = log_ticks(10.0, 100.0, 10.0, true);
307        assert!(t.contains(&10.0));
308        assert!(t.contains(&20.0));
309        assert!(t.contains(&50.0));
310        assert!(t.contains(&100.0));
311    }
312
313    #[test]
314    fn test_ticks_interval() {
315        let t = ticks_interval(0.0, 100.0, 20.0);
316        assert_eq!(t, vec![0.0, 20.0, 40.0, 60.0, 80.0, 100.0]);
317    }
318
319    #[test]
320    fn test_ticks_reverse() {
321        let t = ticks(100.0, 0.0, 5);
322        assert!(t[0] >= t[1]); // Descending order
323    }
324}