Skip to main content

entrenar/monitor/wasm/
utils.rs

1//! Utility functions for WASM dashboard rendering.
2
3/// Normalize values to 0.0-1.0 range.
4pub fn normalize_values(values: &[f64]) -> Vec<f64> {
5    if values.is_empty() {
6        return Vec::new();
7    }
8
9    let min = values.iter().copied().fold(f64::INFINITY, f64::min);
10    let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
11
12    if (max - min).abs() < 1e-10 {
13        // All values are the same
14        return vec![0.5; values.len()];
15    }
16
17    values.iter().map(|v| (v - min) / (max - min)).collect()
18}
19
20/// Generate a sparkline string from values.
21pub fn generate_sparkline(values: &[f64], max_len: usize) -> String {
22    const CHARS: &[char] = &['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
23
24    if values.is_empty() {
25        return String::new();
26    }
27
28    // Subsample if needed
29    let subsampled: Vec<f64> = if values.len() > max_len {
30        let step = values.len() as f64 / max_len as f64;
31        (0..max_len).map(|i| values[(i as f64 * step) as usize]).collect()
32    } else {
33        values.to_vec()
34    };
35
36    let normalized = normalize_values(&subsampled);
37    normalized
38        .iter()
39        .map(|&v| {
40            let idx = ((v * 7.0).round() as usize).min(7);
41            CHARS[idx]
42        })
43        .collect()
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49
50    #[test]
51    fn test_normalize_values_empty() {
52        let result = normalize_values(&[]);
53        assert!(result.is_empty());
54    }
55
56    #[test]
57    fn test_normalize_values_single() {
58        let result = normalize_values(&[5.0]);
59        assert_eq!(result.len(), 1);
60        assert!((result[0] - 0.5).abs() < 1e-6);
61    }
62
63    #[test]
64    fn test_normalize_values_range() {
65        let result = normalize_values(&[0.0, 5.0, 10.0]);
66        assert_eq!(result.len(), 3);
67        assert!((result[0] - 0.0).abs() < 1e-6);
68        assert!((result[1] - 0.5).abs() < 1e-6);
69        assert!((result[2] - 1.0).abs() < 1e-6);
70    }
71
72    #[test]
73    fn test_normalize_values_constant() {
74        let result = normalize_values(&[5.0, 5.0, 5.0]);
75        assert_eq!(result.len(), 3);
76        // All same value should return 0.5
77        for v in &result {
78            assert!((v - 0.5).abs() < 1e-6);
79        }
80    }
81
82    #[test]
83    fn test_generate_sparkline_empty() {
84        let result = generate_sparkline(&[], 20);
85        assert!(result.is_empty());
86    }
87
88    #[test]
89    fn test_generate_sparkline_basic() {
90        let result = generate_sparkline(&[0.0, 0.5, 1.0], 20);
91        assert_eq!(result.chars().count(), 3);
92    }
93
94    #[test]
95    fn test_generate_sparkline_subsample() {
96        let values: Vec<f64> = (0..100).map(f64::from).collect();
97        let result = generate_sparkline(&values, 20);
98        assert_eq!(result.chars().count(), 20);
99    }
100}
101
102#[cfg(test)]
103mod proptests {
104    use super::*;
105    use proptest::prelude::*;
106
107    proptest! {
108        /// Property: Normalization always produces values in [0, 1]
109        #[test]
110        fn prop_normalize_bounded(values in prop::collection::vec(-1000.0f64..1000.0, 2..100)) {
111            let normalized = normalize_values(&values);
112
113            for v in &normalized {
114                prop_assert!(*v >= 0.0 - 1e-10);
115                prop_assert!(*v <= 1.0 + 1e-10);
116            }
117        }
118
119        /// Property: Normalization preserves length
120        #[test]
121        fn prop_normalize_preserves_length(values in prop::collection::vec(-1000.0f64..1000.0, 1..100)) {
122            let normalized = normalize_values(&values);
123            prop_assert_eq!(normalized.len(), values.len());
124        }
125
126        /// Property: Sparkline length is bounded
127        #[test]
128        fn prop_sparkline_bounded(values in prop::collection::vec(0.0f64..100.0, 1..200)) {
129            let sparkline = generate_sparkline(&values, 20);
130            let char_count = sparkline.chars().count();
131            prop_assert!(char_count <= 20);
132        }
133
134        /// Property: Sparkline chars are valid Unicode blocks
135        #[test]
136        fn prop_sparkline_valid_chars(values in prop::collection::vec(0.0f64..100.0, 1..50)) {
137            let sparkline = generate_sparkline(&values, 20);
138            let valid_chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
139
140            for c in sparkline.chars() {
141                prop_assert!(valid_chars.contains(&c));
142            }
143        }
144    }
145}