sorting_race/lib/
sparkline.rs

1//! Sparkline visualization for metrics
2
3use ratatui::{
4    buffer::Buffer,
5    layout::Rect,
6    style::Style,
7    widgets::{Block, Widget},
8};
9
10/// A simple sparkline chart renderer
11#[derive(Debug)]
12pub struct Sparkline {
13    width: usize,
14    height: usize,
15    data: Vec<f64>,
16    style: Style,
17    block: Option<Block<'static>>,
18}
19
20impl Sparkline {
21    /// Create a new sparkline with specified dimensions
22    pub fn new(width: usize, height: usize) -> Self {
23        Self {
24            width: width.max(1),
25            height: height.max(1),
26            data: Vec::new(),
27            style: Style::default(),
28            block: None,
29        }
30    }
31
32    /// Set the style for the sparkline
33    pub fn style(mut self, style: Style) -> Self {
34        self.style = style;
35        self
36    }
37
38    /// Set the block for the sparkline
39    pub fn block(mut self, block: Block<'static>) -> Self {
40        self.block = Some(block);
41        self
42    }
43
44    /// Add a data point to the sparkline
45    pub fn add_data_point(&mut self, value: f64) {
46        self.data.push(value);
47        
48        // Keep only the most recent data points that fit in the width
49        if self.data.len() > self.width {
50            self.data.remove(0);
51        }
52    }
53
54    /// Set all data points at once
55    pub fn set_data(&mut self, data: Vec<f64>) {
56        self.data = data;
57        
58        // Trim to fit width
59        if self.data.len() > self.width {
60            let start = self.data.len() - self.width;
61            self.data = self.data[start..].to_vec();
62        }
63    }
64
65    /// Clear all data
66    pub fn clear(&mut self) {
67        self.data.clear();
68    }
69
70    /// Render the sparkline as a string
71    pub fn render_string(&self) -> String {
72        if self.data.is_empty() {
73            return " ".repeat(self.width);
74        }
75
76        let min_val = self.data.iter().fold(f64::INFINITY, |a, &b| a.min(b));
77        let max_val = self.data.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
78        
79        // Avoid division by zero
80        let range = if (max_val - min_val).abs() < f64::EPSILON {
81            1.0
82        } else {
83            max_val - min_val
84        };
85
86        // Character levels for different heights
87        let chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
88        let levels = chars.len() as f64;
89
90        let mut result = String::new();
91        
92        for &value in &self.data {
93            let normalized = ((value - min_val) / range).clamp(0.0, 1.0);
94            let level = (normalized * (levels - 1.0)).round() as usize;
95            result.push(chars[level]);
96        }
97
98        // Pad with spaces if data is shorter than width
99        while result.chars().count() < self.width {
100            result.push(' ');
101        }
102
103        result
104    }
105
106    /// Render with labels
107    pub fn render_with_labels(&self, title: &str) -> String {
108        let sparkline = self.render_string();
109        let min_val = self.data.iter().fold(f64::INFINITY, |a, &b| a.min(b));
110        let max_val = self.data.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
111        
112        format!(
113            "{}: {} (min: {:.1}, max: {:.1})",
114            title, sparkline, min_val, max_val
115        )
116    }
117
118    /// Get current data
119    pub fn get_data(&self) -> &[f64] {
120        &self.data
121    }
122
123    /// Get data length
124    pub fn len(&self) -> usize {
125        self.data.len()
126    }
127
128    /// Check if empty
129    pub fn is_empty(&self) -> bool {
130        self.data.is_empty()
131    }
132
133    /// Get width
134    pub fn width(&self) -> usize {
135        self.width
136    }
137
138    /// Get height  
139    pub fn height(&self) -> usize {
140        self.height
141    }
142
143    /// Set width
144    pub fn set_width(&mut self, width: usize) {
145        self.width = width.max(1);
146        
147        // Trim data if necessary
148        if self.data.len() > self.width {
149            let start = self.data.len() - self.width;
150            self.data = self.data[start..].to_vec();
151        }
152    }
153
154    /// Set height
155    pub fn set_height(&mut self, height: usize) {
156        self.height = height.max(1);
157    }
158
159    /// Render the sparkline to a ratatui buffer
160    pub fn render_widget(&self, area: Rect, buf: &mut Buffer) {
161        let inner_area = if let Some(ref block) = self.block {
162            let inner = block.inner(area);
163            block.render(area, buf);
164            inner
165        } else {
166            area
167        };
168
169        if self.data.is_empty() || inner_area.width == 0 || inner_area.height == 0 {
170            return;
171        }
172
173        let sparkline_str = self.render_string();
174        let chars: Vec<char> = sparkline_str.chars().collect();
175        
176        let start_x = inner_area.left();
177        let y = inner_area.top() + inner_area.height / 2; // Center vertically
178        
179        for (i, ch) in chars.iter().enumerate() {
180            let x = start_x + i as u16;
181            if x >= inner_area.right() {
182                break;
183            }
184            
185            if y >= inner_area.top() && y < inner_area.bottom() {
186                buf[(x, y)]
187                    .set_symbol(&ch.to_string())
188                    .set_style(self.style);
189            }
190        }
191    }
192}
193
194impl Default for Sparkline {
195    fn default() -> Self {
196        Self::new(20, 1)
197    }
198}
199
200impl Widget for Sparkline {
201    fn render(self, area: Rect, buf: &mut Buffer) {
202        self.render_widget(area, buf);
203    }
204}
205
206impl Widget for &Sparkline {
207    fn render(self, area: Rect, buf: &mut Buffer) {
208        self.render_widget(area, buf);
209    }
210}
211
212/// Collection of sparklines for multiple metrics
213#[derive(Debug)]
214pub struct SparklineCollection {
215    sparklines: std::collections::HashMap<String, Sparkline>,
216    default_width: usize,
217    default_height: usize,
218}
219
220impl SparklineCollection {
221    /// Create a new sparkline collection
222    pub fn new(default_width: usize, default_height: usize) -> Self {
223        Self {
224            sparklines: std::collections::HashMap::new(),
225            default_width,
226            default_height,
227        }
228    }
229
230    /// Add or update a sparkline with data
231    pub fn update(&mut self, key: &str, value: f64) {
232        let sparkline = self.sparklines.entry(key.to_string())
233            .or_insert_with(|| Sparkline::new(self.default_width, self.default_height));
234        sparkline.add_data_point(value);
235    }
236
237    /// Get a sparkline by key
238    pub fn get(&self, key: &str) -> Option<&Sparkline> {
239        self.sparklines.get(key)
240    }
241
242    /// Get a mutable sparkline by key
243    pub fn get_mut(&mut self, key: &str) -> Option<&mut Sparkline> {
244        self.sparklines.get_mut(key)
245    }
246
247    /// Remove a sparkline
248    pub fn remove(&mut self, key: &str) -> Option<Sparkline> {
249        self.sparklines.remove(key)
250    }
251
252    /// Clear all sparklines
253    pub fn clear(&mut self) {
254        self.sparklines.clear();
255    }
256
257    /// Get all keys
258    pub fn keys(&self) -> impl Iterator<Item = &String> {
259        self.sparklines.keys()
260    }
261
262    /// Render all sparklines
263    pub fn render_all(&self) -> String {
264        let mut result = String::new();
265        let mut keys: Vec<_> = self.sparklines.keys().collect();
266        keys.sort();
267
268        for key in keys {
269            if let Some(sparkline) = self.sparklines.get(key) {
270                result.push_str(&sparkline.render_with_labels(key));
271                result.push('\n');
272            }
273        }
274
275        result
276    }
277
278    /// Get number of sparklines
279    pub fn len(&self) -> usize {
280        self.sparklines.len()
281    }
282
283    /// Check if empty
284    pub fn is_empty(&self) -> bool {
285        self.sparklines.is_empty()
286    }
287}
288
289impl Default for SparklineCollection {
290    fn default() -> Self {
291        Self::new(20, 1)
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    #[test]
300    fn test_sparkline_creation() {
301        let sparkline = Sparkline::new(10, 1);
302        assert_eq!(sparkline.width(), 10);
303        assert_eq!(sparkline.height(), 1);
304        assert!(sparkline.is_empty());
305    }
306
307    #[test]
308    fn test_sparkline_add_data() {
309        let mut sparkline = Sparkline::new(5, 1);
310        sparkline.add_data_point(1.0);
311        sparkline.add_data_point(2.0);
312        sparkline.add_data_point(3.0);
313        
314        assert_eq!(sparkline.len(), 3);
315        assert_eq!(sparkline.get_data(), &[1.0, 2.0, 3.0]);
316    }
317
318    #[test]
319    fn test_sparkline_width_limit() {
320        let mut sparkline = Sparkline::new(3, 1);
321        for i in 1..=5 {
322            sparkline.add_data_point(i as f64);
323        }
324        
325        assert_eq!(sparkline.len(), 3);
326        assert_eq!(sparkline.get_data(), &[3.0, 4.0, 5.0]);
327    }
328
329    #[test]
330    fn test_sparkline_render() {
331        let mut sparkline = Sparkline::new(5, 1);
332        sparkline.set_data(vec![1.0, 2.0, 3.0]);
333        
334        let rendered = sparkline.render_string(); // This is the string render method
335        assert_eq!(rendered.chars().count(), 5); // Should be padded to width, using char count for Unicode
336    }
337
338    #[test]
339    fn test_sparkline_collection() {
340        let mut collection = SparklineCollection::new(10, 1);
341        collection.update("test1", 5.0);
342        collection.update("test2", 10.0);
343        
344        assert_eq!(collection.len(), 2);
345        assert!(collection.get("test1").is_some());
346        assert!(collection.get("test2").is_some());
347    }
348}