entrenar/train/tui/
sparkline.rs1pub const SPARK_CHARS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
8
9pub fn sparkline(values: &[f32], width: usize) -> String {
22 if values.is_empty() || width == 0 {
23 return String::new();
24 }
25
26 let values: Vec<f32> = if values.len() > width {
28 let step = values.len() as f32 / width as f32;
29 (0..width)
30 .map(|i| {
31 let idx = (i as f32 * step) as usize;
32 values[idx.min(values.len() - 1)]
33 })
34 .collect()
35 } else {
36 values.to_vec()
37 };
38
39 let min = values.iter().copied().fold(f32::INFINITY, f32::min);
41 let max = values.iter().copied().fold(f32::NEG_INFINITY, f32::max);
42 let range = max - min;
43
44 if range < f32::EPSILON {
46 return SPARK_CHARS[4].to_string().repeat(values.len());
47 }
48
49 values
51 .iter()
52 .map(|v| {
53 let normalized = (v - min) / range;
54 let idx = (normalized * 7.0).round() as usize;
55 SPARK_CHARS[idx.min(7)]
56 })
57 .collect()
58}
59
60pub fn sparkline_range(values: &[f32], width: usize, min: f32, max: f32) -> String {
62 if values.is_empty() || width == 0 {
63 return String::new();
64 }
65
66 let range = max - min;
67 if range < f32::EPSILON {
68 return SPARK_CHARS[4].to_string().repeat(values.len().min(width));
69 }
70
71 let values: Vec<f32> = if values.len() > width {
72 let step = values.len() as f32 / width as f32;
73 (0..width).map(|i| values[(i as f32 * step) as usize]).collect()
74 } else {
75 values.to_vec()
76 };
77
78 values
79 .iter()
80 .map(|v| {
81 let clamped = v.clamp(min, max);
82 let normalized = (clamped - min) / range;
83 let idx = (normalized * 7.0).round() as usize;
84 SPARK_CHARS[idx.min(7)]
85 })
86 .collect()
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn test_sparkline_empty() {
95 assert_eq!(sparkline(&[], 10), "");
96 }
97
98 #[test]
99 fn test_sparkline_zero_width() {
100 assert_eq!(sparkline(&[1.0, 2.0, 3.0], 0), "");
101 }
102
103 #[test]
104 fn test_sparkline_constant() {
105 let result = sparkline(&[5.0, 5.0, 5.0, 5.0], 10);
106 assert!(result.chars().all(|c| c == SPARK_CHARS[4]));
107 }
108
109 #[test]
110 fn test_sparkline_ascending() {
111 let values: Vec<f32> = (0..8).map(|i| i as f32).collect();
112 let result = sparkline(&values, 8);
113 let chars: Vec<char> = result.chars().collect();
115 assert_eq!(chars[0], SPARK_CHARS[0]);
116 assert_eq!(chars[7], SPARK_CHARS[7]);
117 }
118
119 #[test]
120 fn test_sparkline_descending() {
121 let values: Vec<f32> = (0..8).rev().map(|i| i as f32).collect();
122 let result = sparkline(&values, 8);
123 let chars: Vec<char> = result.chars().collect();
124 assert_eq!(chars[0], SPARK_CHARS[7]);
125 assert_eq!(chars[7], SPARK_CHARS[0]);
126 }
127
128 #[test]
129 fn test_sparkline_subsampling() {
130 let values: Vec<f32> = (0..100).map(|i| i as f32).collect();
131 let result = sparkline(&values, 10);
132 assert_eq!(result.chars().count(), 10);
133 }
134
135 #[test]
136 fn test_sparkline_range_empty() {
137 assert_eq!(sparkline_range(&[], 10, 0.0, 1.0), "");
138 }
139
140 #[test]
141 fn test_sparkline_range_clamping() {
142 let values = vec![-1.0, 0.0, 0.5, 1.0, 2.0];
143 let result = sparkline_range(&values, 5, 0.0, 1.0);
144 let chars: Vec<char> = result.chars().collect();
145 assert_eq!(chars[0], SPARK_CHARS[0]);
147 assert_eq!(chars[4], SPARK_CHARS[7]);
149 }
150
151 #[test]
152 fn test_sparkline_range_zero_width() {
153 assert_eq!(sparkline_range(&[1.0, 2.0], 0, 0.0, 1.0), "");
154 }
155
156 #[test]
157 fn test_sparkline_range_constant_range() {
158 let values = vec![5.0, 5.0, 5.0];
159 let result = sparkline_range(&values, 5, 5.0, 5.0);
160 assert!(result.chars().all(|c| c == SPARK_CHARS[4]));
162 }
163
164 #[test]
165 fn test_sparkline_range_subsampling() {
166 let values: Vec<f32> = (0..100).map(|i| i as f32).collect();
167 let result = sparkline_range(&values, 10, 0.0, 99.0);
168 assert_eq!(result.chars().count(), 10);
169 }
170
171 #[test]
172 fn test_spark_chars_length() {
173 assert_eq!(SPARK_CHARS.len(), 8);
174 }
175
176 #[test]
177 fn test_sparkline_single_value() {
178 let result = sparkline(&[5.0], 10);
179 assert_eq!(result.chars().count(), 1);
181 }
182
183 #[test]
184 fn test_sparkline_range_middle_value() {
185 let result = sparkline_range(&[0.5], 1, 0.0, 1.0);
186 let chars: Vec<char> = result.chars().collect();
187 assert_eq!(chars[0], SPARK_CHARS[4]);
189 }
190}