plotiron/
utils.rs

1//! Utility functions for the plotting library
2
3use num_traits::Float;
4
5/// Calculate the range of values in a slice
6pub fn calculate_range<T: Float + Copy>(data: &[T]) -> (T, T) {
7    if data.is_empty() {
8        return (T::zero(), T::one());
9    }
10    
11    let mut min_val = data[0];
12    let mut max_val = data[0];
13    
14    for &value in data.iter() {
15        if value < min_val {
16            min_val = value;
17        }
18        if value > max_val {
19            max_val = value;
20        }
21    }
22    
23    // Add some padding if min == max
24    if min_val == max_val {
25        let padding = if min_val == T::zero() { T::one() } else { min_val * T::from(0.1).unwrap() };
26        min_val = min_val - padding;
27        max_val = max_val + padding;
28    }
29    
30    (min_val, max_val)
31}
32
33/// Generate nice tick values for an axis
34pub fn generate_ticks(min: f64, max: f64, target_count: usize) -> Vec<f64> {
35    if min >= max || target_count == 0 {
36        return vec![min, max];
37    }
38    
39    let range = max - min;
40    let raw_step = range / (target_count - 1) as f64;
41    
42    // Find a "nice" step size
43    let magnitude = 10.0_f64.powf(raw_step.log10().floor());
44    let normalized_step = raw_step / magnitude;
45    
46    let nice_step = if normalized_step <= 1.0 {
47        1.0
48    } else if normalized_step <= 2.0 {
49        2.0
50    } else if normalized_step <= 5.0 {
51        5.0
52    } else {
53        10.0
54    } * magnitude;
55    
56    // Generate ticks
57    let start = (min / nice_step).floor() * nice_step;
58    let mut ticks = Vec::new();
59    let mut current = start;
60    
61    while current <= max + nice_step * 0.001 {
62        if current >= min - nice_step * 0.001 {
63            ticks.push(current);
64        }
65        current += nice_step;
66    }
67    
68    if ticks.is_empty() {
69        vec![min, max]
70    } else {
71        ticks
72    }
73}
74
75/// Linear interpolation between two values
76pub fn lerp(a: f64, b: f64, t: f64) -> f64 {
77    a + (b - a) * t
78}
79
80/// Map a value from one range to another
81pub fn map_range(value: f64, from_min: f64, from_max: f64, to_min: f64, to_max: f64) -> f64 {
82    if from_max == from_min {
83        return to_min;
84    }
85    let t = (value - from_min) / (from_max - from_min);
86    lerp(to_min, to_max, t)
87}
88
89/// Format a number for display on axes
90pub fn format_number(value: f64) -> String {
91    if value.abs() < 1e-10 {
92        "0".to_string()
93    } else if value.abs() >= 1e6 || value.abs() < 1e-3 {
94        format!("{:.2e}", value)
95    } else if value.fract() == 0.0 {
96        format!("{:.0}", value)
97    } else {
98        format!("{:.3}", value).trim_end_matches('0').trim_end_matches('.').to_string()
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn test_calculate_range() {
108        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
109        let (min, max) = calculate_range(&data);
110        assert_eq!(min, 1.0);
111        assert_eq!(max, 5.0);
112    }
113
114    #[test]
115    fn test_generate_ticks() {
116        let ticks = generate_ticks(0.0, 10.0, 6);
117        assert!(!ticks.is_empty());
118        assert!(ticks[0] <= 0.0);
119        assert!(ticks[ticks.len() - 1] >= 10.0);
120    }
121
122    #[test]
123    fn test_map_range() {
124        assert_eq!(map_range(5.0, 0.0, 10.0, 0.0, 100.0), 50.0);
125        assert_eq!(map_range(0.0, 0.0, 10.0, 0.0, 100.0), 0.0);
126        assert_eq!(map_range(10.0, 0.0, 10.0, 0.0, 100.0), 100.0);
127    }
128}