1use ratatui::{
4 buffer::Buffer,
5 layout::Rect,
6 style::Style,
7 widgets::{Block, Widget},
8};
9
10#[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 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 pub fn style(mut self, style: Style) -> Self {
34 self.style = style;
35 self
36 }
37
38 pub fn block(mut self, block: Block<'static>) -> Self {
40 self.block = Some(block);
41 self
42 }
43
44 pub fn add_data_point(&mut self, value: f64) {
46 self.data.push(value);
47
48 if self.data.len() > self.width {
50 self.data.remove(0);
51 }
52 }
53
54 pub fn set_data(&mut self, data: Vec<f64>) {
56 self.data = data;
57
58 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 pub fn clear(&mut self) {
67 self.data.clear();
68 }
69
70 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 let range = if (max_val - min_val).abs() < f64::EPSILON {
81 1.0
82 } else {
83 max_val - min_val
84 };
85
86 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 while result.chars().count() < self.width {
100 result.push(' ');
101 }
102
103 result
104 }
105
106 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 pub fn get_data(&self) -> &[f64] {
120 &self.data
121 }
122
123 pub fn len(&self) -> usize {
125 self.data.len()
126 }
127
128 pub fn is_empty(&self) -> bool {
130 self.data.is_empty()
131 }
132
133 pub fn width(&self) -> usize {
135 self.width
136 }
137
138 pub fn height(&self) -> usize {
140 self.height
141 }
142
143 pub fn set_width(&mut self, width: usize) {
145 self.width = width.max(1);
146
147 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 pub fn set_height(&mut self, height: usize) {
156 self.height = height.max(1);
157 }
158
159 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; 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#[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 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 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 pub fn get(&self, key: &str) -> Option<&Sparkline> {
239 self.sparklines.get(key)
240 }
241
242 pub fn get_mut(&mut self, key: &str) -> Option<&mut Sparkline> {
244 self.sparklines.get_mut(key)
245 }
246
247 pub fn remove(&mut self, key: &str) -> Option<Sparkline> {
249 self.sparklines.remove(key)
250 }
251
252 pub fn clear(&mut self) {
254 self.sparklines.clear();
255 }
256
257 pub fn keys(&self) -> impl Iterator<Item = &String> {
259 self.sparklines.keys()
260 }
261
262 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 pub fn len(&self) -> usize {
280 self.sparklines.len()
281 }
282
283 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(); assert_eq!(rendered.chars().count(), 5); }
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}