embedded_charts/axes/
ticks.rs

1//! Tick generation algorithms for axes.
2
3use crate::axes::traits::{AxisValue, Tick, TickGenerator};
4use crate::math::{Math, NumericConversion};
5use heapless::Vec;
6
7/// Linear tick generator that creates evenly spaced ticks
8#[derive(Debug, Clone)]
9pub struct LinearTickGenerator {
10    /// Preferred number of ticks
11    preferred_count: usize,
12    /// Whether to include minor ticks
13    include_minor_ticks: bool,
14    /// Ratio of minor ticks to major ticks
15    minor_tick_ratio: usize,
16}
17
18impl LinearTickGenerator {
19    /// Create a new linear tick generator
20    pub fn new(preferred_count: usize) -> Self {
21        Self {
22            preferred_count: preferred_count.clamp(2, 20),
23            include_minor_ticks: false,
24            minor_tick_ratio: 4,
25        }
26    }
27
28    /// Enable minor ticks with the specified ratio
29    ///
30    /// # Arguments
31    /// * `ratio` - Number of minor ticks between major ticks
32    pub fn with_minor_ticks(mut self, ratio: usize) -> Self {
33        self.include_minor_ticks = true;
34        self.minor_tick_ratio = ratio.clamp(1, 10);
35        self
36    }
37
38    /// Disable minor ticks
39    pub fn without_minor_ticks(mut self) -> Self {
40        self.include_minor_ticks = false;
41        self
42    }
43
44    /// Calculate nice tick spacing for the given range
45    fn calculate_nice_step<T: AxisValue>(min: T, max: T, target_count: usize) -> T {
46        let min_f32 = min.to_f32();
47        let max_f32 = max.to_f32();
48        let range = max_f32 - min_f32;
49
50        // Safety checks for edge cases
51        if target_count <= 1 {
52            return T::from_f32(range.max(1.0)); // Fallback to range or 1.0
53        }
54
55        if range <= 0.0 || !range.is_finite() {
56            return T::from_f32(1.0); // Fallback to 1.0
57        }
58
59        let rough_step = range / (target_count - 1) as f32;
60
61        // Minimum step threshold to prevent infinite loops
62        if rough_step < 1e-10 {
63            return T::from_f32(1e-6); // Safe minimum step
64        }
65
66        // Find the magnitude of the step using Math abstraction
67        let rough_step_num = rough_step.to_number();
68        let magnitude = Math::floor(Math::log10(rough_step_num));
69        let ten = 10.0f32.to_number();
70        let normalized_step = rough_step_num / Math::pow(ten, magnitude);
71
72        // Choose a nice normalized step
73        let one = 1.0f32.to_number();
74        let two = 2.0f32.to_number();
75        let five = 5.0f32.to_number();
76        let ten_norm = 10.0f32.to_number();
77
78        let nice_normalized = if normalized_step <= one {
79            one
80        } else if normalized_step <= two {
81            two
82        } else if normalized_step <= five {
83            five
84        } else {
85            ten_norm
86        };
87
88        let result = if magnitude >= 0.0.to_number() && magnitude <= 10.0.to_number() {
89            nice_normalized * Math::pow(ten, magnitude)
90        } else {
91            // Fallback for extreme magnitudes to prevent overflow
92            nice_normalized
93        };
94        let step_f32 = f32::from_number(result);
95
96        // Final safety check
97        if step_f32 <= 0.0 || !step_f32.is_finite() {
98            return T::from_f32(range / target_count as f32); // Simple fallback
99        }
100
101        T::from_f32(step_f32)
102    }
103
104    /// Generate major ticks for the range
105    fn generate_major_ticks<T: AxisValue>(&self, min: T, max: T) -> Vec<Tick<T>, 32> {
106        let mut ticks = Vec::new();
107
108        let step = Self::calculate_nice_step(min, max, self.preferred_count);
109        let step_f32 = step.to_f32();
110
111        // Safety check: prevent infinite loops from zero or very small steps
112        if step_f32 <= 0.0 || step_f32 < 1e-10 || !step_f32.is_finite() {
113            // Fallback: create simple ticks at min and max
114            let label_min = min.format();
115            let label_max = max.format();
116            let _ = ticks.push(Tick::major(min, label_min.as_str()));
117            if min.to_f32() != max.to_f32() {
118                let _ = ticks.push(Tick::major(max, label_max.as_str()));
119            }
120            return ticks;
121        }
122
123        // Find the first tick position (rounded down to nearest step)
124        let first_tick_value = {
125            let min_f32 = min.to_f32();
126            let min_num = min_f32.to_number();
127            let step_num = step_f32.to_number();
128            let first_tick_num = Math::floor(min_num / step_num) * step_num;
129            let first_tick_f32 = f32::from_number(first_tick_num);
130            T::from_f32(first_tick_f32)
131        };
132
133        // Generate ticks with additional safety checks
134        let mut current = first_tick_value;
135        let mut iteration_count = 0;
136        let max_iterations = 100; // Safety limit
137
138        while current.to_f32() <= max.to_f32()
139            && ticks.len() < 32
140            && iteration_count < max_iterations
141        {
142            if current.to_f32() >= min.to_f32() {
143                let label = current.format();
144
145                let _ = ticks.push(Tick::major(current, label.as_str()));
146            }
147
148            let prev_value = current.to_f32();
149            current = T::from_f32(current.to_f32() + step_f32);
150            iteration_count += 1;
151
152            // Safety check: ensure we're actually making progress
153            if current.to_f32() <= prev_value {
154                break; // Step is too small, causing no progress
155            }
156        }
157
158        ticks
159    }
160
161    /// Generate minor ticks for the given range
162    fn generate_minor_ticks_for_range<T: AxisValue>(
163        &self,
164        min: T,
165        max: T,
166        major_ticks: &[Tick<T>],
167    ) -> Vec<Tick<T>, 32> {
168        let mut minor_ticks = Vec::new();
169
170        if major_ticks.len() < 2 {
171            return minor_ticks;
172        }
173
174        // Calculate the step size between major ticks
175        let major_step = major_ticks[1].value.to_f32() - major_ticks[0].value.to_f32();
176        let minor_step = major_step / (self.minor_tick_ratio + 1) as f32;
177
178        // Generate minor ticks between ALL consecutive pairs of major ticks
179        for window in major_ticks.windows(2) {
180            if let [tick1, tick2] = window {
181                for i in 1..=self.minor_tick_ratio {
182                    let minor_value_f32 = tick1.value.to_f32() + minor_step * i as f32;
183
184                    // Only add if within range and not equal to the next major tick
185                    if minor_value_f32 >= min.to_f32() && minor_value_f32 <= max.to_f32() {
186                        // Make sure we don't add a minor tick exactly at a major tick position
187                        let distance_to_next_major = (tick2.value.to_f32() - minor_value_f32).abs();
188                        if distance_to_next_major > 0.001 {
189                            // Small tolerance for floating point comparison
190                            let minor_value = T::from_f32(minor_value_f32);
191                            if minor_ticks.len() < 32 {
192                                let _ = minor_ticks.push(Tick::minor(minor_value));
193                            }
194                        }
195                    }
196                }
197            }
198        }
199
200        minor_ticks
201    }
202
203    /// Generate minor ticks between major ticks (legacy method for compatibility)
204    #[allow(dead_code)]
205    fn generate_minor_ticks<T: AxisValue>(&self, major_ticks: &[Tick<T>]) -> Vec<Tick<T>, 32> {
206        let mut minor_ticks = Vec::new();
207
208        if major_ticks.len() < 2 {
209            return minor_ticks;
210        }
211
212        // Calculate the step size between major ticks
213        let major_step = major_ticks[1].value.to_f32() - major_ticks[0].value.to_f32();
214        let minor_step = major_step / (self.minor_tick_ratio + 1) as f32;
215
216        // Generate minor ticks between each pair of major ticks
217        for window in major_ticks.windows(2) {
218            if let [tick1, _tick2] = window {
219                for i in 1..=self.minor_tick_ratio {
220                    let minor_value = T::from_f32(tick1.value.to_f32() + minor_step * i as f32);
221                    if minor_ticks.len() < 32 {
222                        let _ = minor_ticks.push(Tick::minor(minor_value));
223                    }
224                }
225            }
226        }
227
228        minor_ticks
229    }
230}
231
232impl<T: AxisValue> TickGenerator<T> for LinearTickGenerator {
233    fn generate_ticks(&self, min: T, max: T, max_ticks: usize) -> Vec<Tick<T>, 32> {
234        let mut all_ticks = Vec::new();
235
236        // Generate major ticks
237        let major_ticks = self.generate_major_ticks(min, max);
238
239        // Add major ticks to the result
240        for tick in &major_ticks {
241            if all_ticks.len() < max_ticks.min(32) {
242                let _ = all_ticks.push(tick.clone());
243            }
244        }
245
246        // Generate and add minor ticks if enabled
247        if self.include_minor_ticks {
248            let minor_ticks = self.generate_minor_ticks_for_range(min, max, &major_ticks);
249
250            for tick in minor_ticks {
251                if all_ticks.len() < max_ticks.min(32) {
252                    let _ = all_ticks.push(tick);
253                }
254            }
255
256            // Sort ticks by value (manual implementation for heapless::Vec)
257            let len = all_ticks.len();
258            for i in 0..len {
259                for j in 0..len - 1 - i {
260                    let a_val = all_ticks[j].value.to_f32();
261                    let b_val = all_ticks[j + 1].value.to_f32();
262                    if a_val > b_val {
263                        all_ticks.swap(j, j + 1);
264                    }
265                }
266            }
267        }
268
269        all_ticks
270    }
271
272    fn preferred_tick_count(&self) -> usize {
273        self.preferred_count
274    }
275
276    fn set_preferred_tick_count(&mut self, count: usize) {
277        self.preferred_count = count.clamp(2, 20);
278    }
279}
280
281/// Custom tick generator that allows manual specification of tick positions
282#[derive(Debug, Clone)]
283pub struct CustomTickGenerator<T> {
284    /// Manually specified tick positions
285    ticks: Vec<Tick<T>, 32>,
286}
287
288impl<T: Copy> CustomTickGenerator<T> {
289    /// Create a new custom tick generator
290    pub fn new() -> Self {
291        Self { ticks: Vec::new() }
292    }
293
294    /// Add a major tick at the specified value with a label
295    pub fn add_major_tick(mut self, value: T, label: &str) -> Self {
296        if self.ticks.len() < 32 {
297            let _ = self.ticks.push(Tick::major(value, label));
298        }
299        self
300    }
301
302    /// Add a minor tick at the specified value
303    pub fn add_minor_tick(mut self, value: T) -> Self {
304        if self.ticks.len() < 32 {
305            let _ = self.ticks.push(Tick::minor(value));
306        }
307        self
308    }
309
310    /// Clear all ticks
311    pub fn clear(&mut self) {
312        self.ticks.clear();
313    }
314}
315
316impl<T: Copy + PartialOrd> TickGenerator<T> for CustomTickGenerator<T> {
317    fn generate_ticks(&self, min: T, max: T, max_ticks: usize) -> Vec<Tick<T>, 32> {
318        let mut result = Vec::new();
319
320        for tick in &self.ticks {
321            if tick.value >= min && tick.value <= max && result.len() < max_ticks.min(32) {
322                let _ = result.push(tick.clone());
323            }
324        }
325
326        result
327    }
328
329    fn preferred_tick_count(&self) -> usize {
330        self.ticks.len()
331    }
332
333    fn set_preferred_tick_count(&mut self, _count: usize) {
334        // Custom tick generator ignores preferred count
335    }
336}
337
338impl<T: Copy> Default for CustomTickGenerator<T> {
339    fn default() -> Self {
340        Self::new()
341    }
342}
343
344/// Logarithmic tick generator for logarithmic scales
345#[derive(Debug, Clone)]
346pub struct LogTickGenerator {
347    /// Base of the logarithm (typically 10)
348    base: f32,
349    /// Whether to include minor ticks
350    include_minor_ticks: bool,
351}
352
353impl LogTickGenerator {
354    /// Create a new logarithmic tick generator with base 10
355    pub fn new() -> Self {
356        Self {
357            base: 10.0,
358            include_minor_ticks: false,
359        }
360    }
361
362    /// Create a logarithmic tick generator with a custom base
363    pub fn with_base(base: f32) -> Self {
364        Self {
365            base: base.max(2.0),
366            include_minor_ticks: false,
367        }
368    }
369
370    /// Enable minor ticks
371    pub fn with_minor_ticks(mut self) -> Self {
372        self.include_minor_ticks = true;
373        self
374    }
375}
376
377impl TickGenerator<f32> for LogTickGenerator {
378    fn generate_ticks(&self, min: f32, max: f32, max_ticks: usize) -> Vec<Tick<f32>, 32> {
379        let mut ticks = Vec::new();
380
381        if min <= 0.0 || max <= 0.0 {
382            return ticks; // Logarithmic scale requires positive values
383        }
384
385        let min_num = min.to_number();
386        let max_num = max.to_number();
387        let base_num = self.base.to_number();
388
389        let log_min = Math::ln(min_num) / Math::ln(base_num);
390        let log_max = Math::ln(max_num) / Math::ln(base_num);
391
392        let start_power = f32::from_number(Math::floor(log_min)) as i32;
393        let end_power = f32::from_number(Math::ceil(log_max)) as i32;
394
395        for power in start_power..=end_power {
396            if ticks.len() >= max_ticks.min(32) {
397                break;
398            }
399
400            let power_num = (power as f32).to_number();
401            let value_num = Math::pow(base_num, power_num);
402            let value = f32::from_number(value_num);
403            if value >= min && value <= max {
404                // Simple no_std formatting
405                let mut label = heapless::String::new();
406                if value >= 1000.0 {
407                    let k_val = (value / 1000.0) as i32;
408                    // Simple integer to string conversion
409                    let mut val = k_val;
410                    let mut digits = heapless::Vec::<u8, 8>::new();
411                    if val == 0 {
412                        let _ = digits.push(b'0');
413                    } else {
414                        while val > 0 {
415                            let _ = digits.push((val % 10) as u8 + b'0');
416                            val /= 10;
417                        }
418                    }
419                    for &digit in digits.iter().rev() {
420                        let _ = label.push(digit as char);
421                    }
422                    let _ = label.push('k');
423                } else if value >= 1.0 {
424                    let int_val = value as i32;
425                    let mut val = int_val;
426                    let mut digits = heapless::Vec::<u8, 8>::new();
427                    if val == 0 {
428                        let _ = digits.push(b'0');
429                    } else {
430                        while val > 0 {
431                            let _ = digits.push((val % 10) as u8 + b'0');
432                            val /= 10;
433                        }
434                    }
435                    for &digit in digits.iter().rev() {
436                        let _ = label.push(digit as char);
437                    }
438                } else {
439                    // For small values, just show "0.x"
440                    let _ = label.push_str("0.1");
441                }
442
443                let _ = ticks.push(Tick {
444                    value,
445                    is_major: true,
446                    label: Some(label),
447                });
448            }
449        }
450
451        ticks
452    }
453
454    fn preferred_tick_count(&self) -> usize {
455        5
456    }
457
458    fn set_preferred_tick_count(&mut self, _count: usize) {
459        // Log tick generator doesn't use preferred count in the same way
460    }
461}
462
463impl Default for LogTickGenerator {
464    fn default() -> Self {
465        Self::new()
466    }
467}
468
469#[cfg(test)]
470mod tests {
471    use super::*;
472
473    #[test]
474    #[cfg(not(feature = "integer-math"))] // Skip for integer-math to avoid overflow
475    fn test_linear_tick_generator() {
476        let generator = LinearTickGenerator::new(5);
477        let ticks = generator.generate_ticks(0.0f32, 10.0f32, 10);
478
479        assert!(!ticks.is_empty());
480        assert!(ticks.len() <= 10);
481
482        // Check that ticks are in ascending order
483        for window in ticks.windows(2) {
484            if let [tick1, tick2] = window {
485                assert!(tick1.value <= tick2.value);
486            }
487        }
488    }
489
490    #[test]
491    #[cfg(not(any(feature = "fixed-point", feature = "integer-math")))] // Skip for fixed-point and integer-math to avoid overflow
492    fn test_linear_tick_generator_with_minor_ticks() {
493        let generator = LinearTickGenerator::new(3).with_minor_ticks(2);
494        let ticks = generator.generate_ticks(0.0f32, 10.0f32, 20);
495
496        assert!(!ticks.is_empty());
497
498        let major_count = ticks.iter().filter(|t| t.is_major).count();
499        let minor_count = ticks.iter().filter(|t| !t.is_major).count();
500
501        assert!(major_count > 0);
502        assert!(minor_count > 0);
503    }
504
505    #[test]
506    fn test_custom_tick_generator() {
507        let generator = CustomTickGenerator::new()
508            .add_major_tick(0.0, "Start")
509            .add_major_tick(5.0, "Middle")
510            .add_major_tick(10.0, "End")
511            .add_minor_tick(2.5);
512
513        let ticks = generator.generate_ticks(0.0f32, 10.0f32, 10);
514        assert_eq!(ticks.len(), 4);
515
516        let major_count = ticks.iter().filter(|t| t.is_major).count();
517        assert_eq!(major_count, 3);
518    }
519
520    #[test]
521    #[cfg(not(any(feature = "fixed-point", feature = "integer-math")))] // Skip for fixed-point and integer-math to avoid overflow
522    fn test_log_tick_generator() {
523        let generator = LogTickGenerator::new();
524        let ticks = generator.generate_ticks(1.0f32, 1000.0f32, 10);
525
526        assert!(!ticks.is_empty());
527
528        // All ticks should be major for log scale
529        assert!(ticks.iter().all(|t| t.is_major));
530
531        // Values should be powers of 10
532        for tick in &ticks {
533            let value_num = tick.value.to_number();
534            let log_value = f32::from_number(Math::log10(value_num));
535            assert!((log_value.round() - log_value).abs() < 0.001);
536        }
537    }
538}