1use crate::core::component::{VNode, BoxNode, BoxStyle, TextStyle, Color, NamedColor};
6
7#[derive(Debug, Clone)]
13pub struct Sparkline {
14 data: Vec<f64>,
15 width: Option<u16>,
16 height: u16,
17 color: Color,
18 show_min_max: bool,
19 label: Option<String>,
20}
21
22impl Default for Sparkline {
23 fn default() -> Self {
24 Self {
25 data: Vec::new(),
26 width: None,
27 height: 1,
28 color: Color::Named(NamedColor::Green),
29 show_min_max: false,
30 label: None,
31 }
32 }
33}
34
35impl Sparkline {
36 pub fn new() -> Self {
38 Self::default()
39 }
40
41 pub fn data<I>(mut self, data: I) -> Self
43 where
44 I: IntoIterator<Item = f64>,
45 {
46 self.data = data.into_iter().collect();
47 self
48 }
49
50 pub fn data_i32<I>(mut self, data: I) -> Self
52 where
53 I: IntoIterator<Item = i32>,
54 {
55 self.data = data.into_iter().map(|x| x as f64).collect();
56 self
57 }
58
59 pub fn width(mut self, width: u16) -> Self {
61 self.width = Some(width);
62 self
63 }
64
65 pub fn height(mut self, height: u16) -> Self {
67 self.height = height;
68 self
69 }
70
71 pub fn color(mut self, color: Color) -> Self {
73 self.color = color;
74 self
75 }
76
77 pub fn show_min_max(mut self) -> Self {
79 self.show_min_max = true;
80 self
81 }
82
83 pub fn label(mut self, label: impl Into<String>) -> Self {
85 self.label = Some(label.into());
86 self
87 }
88
89 pub fn build(self) -> VNode {
91 if self.data.is_empty() {
92 return VNode::styled_text("No data", TextStyle::color(Color::Named(NamedColor::Gray)));
93 }
94
95 let chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
97
98 let min = self.data.iter().copied().fold(f64::INFINITY, f64::min);
99 let max = self.data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
100 let range = max - min;
101
102 let width = self.width.unwrap_or(self.data.len() as u16);
103 let data_len = self.data.len();
104
105 let samples: Vec<f64> = if data_len == width as usize {
107 self.data.clone()
108 } else {
109 (0..width as usize)
110 .map(|i| {
111 let idx = (i * data_len) / width as usize;
112 self.data.get(idx).copied().unwrap_or(0.0)
113 })
114 .collect()
115 };
116
117 let sparkline: String = samples
119 .iter()
120 .map(|&v| {
121 if range == 0.0 {
122 chars[4]
123 } else {
124 let normalized = ((v - min) / range * 7.0) as usize;
125 chars[normalized.min(7)]
126 }
127 })
128 .collect();
129
130 let mut children = Vec::new();
131
132 if let Some(label) = &self.label {
133 children.push(VNode::styled_text(label.clone(), TextStyle::bold()));
134 }
135
136 children.push(VNode::styled_text(sparkline, TextStyle::color(self.color)));
137
138 if self.show_min_max {
139 children.push(VNode::styled_text(
140 format!("min: {:.1} max: {:.1}", min, max),
141 TextStyle { color: Some(Color::Named(NamedColor::Gray)), dim: true, ..Default::default() }
142 ));
143 }
144
145 VNode::Box(BoxNode {
146 children,
147 style: BoxStyle::default(),
148 ..Default::default()
149 })
150 }
151}
152
153#[derive(Debug, Clone, Copy, Default)]
159pub enum BarOrientation {
160 #[default]
161 Horizontal,
162 Vertical,
163}
164
165#[derive(Debug, Clone)]
167pub struct BarItem {
168 pub label: String,
170 pub value: f64,
172 pub color: Option<Color>,
174}
175
176impl BarItem {
177 pub fn new(label: impl Into<String>, value: f64) -> Self {
179 Self {
180 label: label.into(),
181 value,
182 color: None,
183 }
184 }
185
186 pub fn color(mut self, color: Color) -> Self {
188 self.color = Some(color);
189 self
190 }
191}
192
193#[derive(Debug, Clone)]
195pub struct BarChart {
196 items: Vec<BarItem>,
197 width: u16,
198 bar_width: u16,
199 orientation: BarOrientation,
200 color: Color,
201 show_values: bool,
202 max_value: Option<f64>,
203}
204
205impl Default for BarChart {
206 fn default() -> Self {
207 Self {
208 items: Vec::new(),
209 width: 40,
210 bar_width: 1,
211 orientation: BarOrientation::Horizontal,
212 color: Color::Named(NamedColor::Cyan),
213 show_values: true,
214 max_value: None,
215 }
216 }
217}
218
219impl BarChart {
220 pub fn new() -> Self {
222 Self::default()
223 }
224
225 pub fn items<I>(mut self, items: I) -> Self
227 where
228 I: IntoIterator<Item = BarItem>,
229 {
230 self.items = items.into_iter().collect();
231 self
232 }
233
234 pub fn data<I, S>(mut self, data: I) -> Self
236 where
237 I: IntoIterator<Item = (S, f64)>,
238 S: Into<String>,
239 {
240 self.items = data.into_iter().map(|(l, v)| BarItem::new(l, v)).collect();
241 self
242 }
243
244 pub fn width(mut self, width: u16) -> Self {
246 self.width = width;
247 self
248 }
249
250 pub fn bar_width(mut self, width: u16) -> Self {
252 self.bar_width = width;
253 self
254 }
255
256 pub fn orientation(mut self, orientation: BarOrientation) -> Self {
258 self.orientation = orientation;
259 self
260 }
261
262 pub fn horizontal(self) -> Self {
264 self.orientation(BarOrientation::Horizontal)
265 }
266
267 pub fn vertical(self) -> Self {
269 self.orientation(BarOrientation::Vertical)
270 }
271
272 pub fn color(mut self, color: Color) -> Self {
274 self.color = color;
275 self
276 }
277
278 pub fn show_values(mut self, show: bool) -> Self {
280 self.show_values = show;
281 self
282 }
283
284 pub fn max(mut self, max: f64) -> Self {
286 self.max_value = Some(max);
287 self
288 }
289
290 pub fn build(self) -> VNode {
292 if self.items.is_empty() {
293 return VNode::styled_text("No data", TextStyle::color(Color::Named(NamedColor::Gray)));
294 }
295
296 let max_value = self.max_value.unwrap_or_else(|| {
297 self.items.iter().map(|i| i.value).fold(0.0, f64::max)
298 });
299
300 let max_label_len = self.items.iter().map(|i| i.label.len()).max().unwrap_or(0);
301
302 let mut children = Vec::new();
303
304 match self.orientation {
305 BarOrientation::Horizontal => {
306 for item in &self.items {
307 let bar_len = if max_value > 0.0 {
308 ((item.value / max_value) * (self.width as f64 - max_label_len as f64 - 4.0)) as usize
309 } else {
310 0
311 };
312
313 let bar = "█".repeat(bar_len);
314 let color = item.color.unwrap_or(self.color);
315
316 let value_str = if self.show_values {
317 format!(" {:.1}", item.value)
318 } else {
319 String::new()
320 };
321
322 let line = format!(
323 "{:>width$} │{}{}",
324 item.label,
325 bar,
326 value_str,
327 width = max_label_len
328 );
329
330 children.push(VNode::styled_text(line, TextStyle::color(color)));
331 }
332 }
333 BarOrientation::Vertical => {
334 let height = 8;
336 let bar_chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
337
338 for row in (0..height).rev() {
339 let threshold = (row as f64 + 1.0) / height as f64;
340
341 let mut row_text = String::new();
342 for item in &self.items {
343 let normalized = if max_value > 0.0 { item.value / max_value } else { 0.0 };
344
345 let char = if normalized >= threshold {
346 '█'
347 } else if normalized > threshold - (1.0 / height as f64) {
348 let partial = ((normalized - (threshold - 1.0 / height as f64)) * height as f64 * 8.0) as usize;
349 bar_chars[partial.min(7)]
350 } else {
351 ' '
352 };
353
354 row_text.push(char);
355 row_text.push(' ');
356 }
357
358 children.push(VNode::styled_text(row_text, TextStyle::color(self.color)));
359 }
360
361 let labels: String = self.items.iter()
363 .map(|i| format!("{:.1}", i.label.chars().next().unwrap_or(' ')))
364 .collect::<Vec<_>>()
365 .join(" ");
366
367 children.push(VNode::styled_text(labels, TextStyle::color(Color::Named(NamedColor::Gray))));
368 }
369 }
370
371 VNode::Box(BoxNode {
372 children,
373 style: BoxStyle::default(),
374 ..Default::default()
375 })
376 }
377}
378
379#[derive(Debug, Clone, Copy, Default)]
385pub enum GaugeStyle {
386 #[default]
387 Bar,
388 Arc,
389 Circle,
390}
391
392#[derive(Debug, Clone)]
394pub struct Gauge {
395 value: f64,
396 max: f64,
397 min: f64,
398 width: u16,
399 style: GaugeStyle,
400 label: Option<String>,
401 show_percentage: bool,
402 color: Color,
403 #[allow(dead_code)]
404 background_color: Color,
405 thresholds: Vec<(f64, Color)>,
406}
407
408impl Default for Gauge {
409 fn default() -> Self {
410 Self {
411 value: 0.0,
412 max: 100.0,
413 min: 0.0,
414 width: 20,
415 style: GaugeStyle::Bar,
416 label: None,
417 show_percentage: true,
418 color: Color::Named(NamedColor::Green),
419 background_color: Color::Named(NamedColor::BrightBlack),
420 thresholds: Vec::new(),
421 }
422 }
423}
424
425impl Gauge {
426 pub fn new() -> Self {
428 Self::default()
429 }
430
431 pub fn value(mut self, value: f64) -> Self {
433 self.value = value;
434 self
435 }
436
437 pub fn max(mut self, max: f64) -> Self {
439 self.max = max;
440 self
441 }
442
443 pub fn min(mut self, min: f64) -> Self {
445 self.min = min;
446 self
447 }
448
449 pub fn width(mut self, width: u16) -> Self {
451 self.width = width;
452 self
453 }
454
455 pub fn style(mut self, style: GaugeStyle) -> Self {
457 self.style = style;
458 self
459 }
460
461 pub fn label(mut self, label: impl Into<String>) -> Self {
463 self.label = Some(label.into());
464 self
465 }
466
467 pub fn show_percentage(mut self, show: bool) -> Self {
469 self.show_percentage = show;
470 self
471 }
472
473 pub fn color(mut self, color: Color) -> Self {
475 self.color = color;
476 self
477 }
478
479 pub fn threshold(mut self, value: f64, color: Color) -> Self {
481 self.thresholds.push((value, color));
482 self.thresholds.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
483 self
484 }
485
486 pub fn traffic_light(self) -> Self {
488 self.threshold(50.0, Color::Named(NamedColor::Green))
489 .threshold(75.0, Color::Named(NamedColor::Yellow))
490 .threshold(90.0, Color::Named(NamedColor::Red))
491 }
492
493 fn get_color(&self) -> Color {
495 for (threshold, color) in self.thresholds.iter().rev() {
496 if self.value >= *threshold {
497 return *color;
498 }
499 }
500 self.color
501 }
502
503 pub fn build(self) -> VNode {
505 let range = self.max - self.min;
506 let percentage = if range > 0.0 {
507 ((self.value - self.min) / range * 100.0).clamp(0.0, 100.0)
508 } else {
509 0.0
510 };
511
512 let filled = ((percentage / 100.0) * self.width as f64) as usize;
513 let empty = self.width as usize - filled;
514
515 let color = self.get_color();
516
517 let bar = match self.style {
518 GaugeStyle::Bar => {
519 let filled_str = "█".repeat(filled);
520 let empty_str = "░".repeat(empty);
521 format!("{}{}", filled_str, empty_str)
522 }
523 GaugeStyle::Arc => {
524 let chars: Vec<char> = (0..self.width)
526 .map(|i| {
527 if (i as f64) < (self.width as f64 * percentage / 100.0) {
528 '◼'
529 } else {
530 '◻'
531 }
532 })
533 .collect();
534 chars.into_iter().collect()
535 }
536 GaugeStyle::Circle => {
537 let segments = 8;
539 let filled_segments = (percentage / 100.0 * segments as f64) as usize;
540 let pie_chars = ['○', '◔', '◑', '◕', '●'];
541 let idx = (filled_segments * pie_chars.len() / segments).min(pie_chars.len() - 1);
542 pie_chars[idx].to_string()
543 }
544 };
545
546 let mut content = String::new();
547
548 if let Some(label) = &self.label {
549 content.push_str(label);
550 content.push_str(": ");
551 }
552
553 content.push_str(&bar);
554
555 if self.show_percentage {
556 content.push_str(&format!(" {:.0}%", percentage));
557 }
558
559 VNode::styled_text(content, TextStyle::color(color))
560 }
561}
562
563#[derive(Debug, Clone)]
569pub struct LineChart {
570 data: Vec<Vec<f64>>,
571 labels: Vec<String>,
572 width: u16,
573 height: u16,
574 #[allow(dead_code)]
575 colors: Vec<Color>,
576 show_legend: bool,
577}
578
579impl Default for LineChart {
580 fn default() -> Self {
581 Self {
582 data: Vec::new(),
583 labels: Vec::new(),
584 width: 40,
585 height: 10,
586 colors: vec![
587 Color::Named(NamedColor::Cyan),
588 Color::Named(NamedColor::Green),
589 Color::Named(NamedColor::Yellow),
590 Color::Named(NamedColor::Magenta),
591 ],
592 show_legend: true,
593 }
594 }
595}
596
597impl LineChart {
598 pub fn new() -> Self {
600 Self::default()
601 }
602
603 pub fn series<I>(mut self, label: impl Into<String>, data: I) -> Self
605 where
606 I: IntoIterator<Item = f64>,
607 {
608 self.labels.push(label.into());
609 self.data.push(data.into_iter().collect());
610 self
611 }
612
613 pub fn size(mut self, width: u16, height: u16) -> Self {
615 self.width = width;
616 self.height = height;
617 self
618 }
619
620 pub fn legend(mut self, show: bool) -> Self {
622 self.show_legend = show;
623 self
624 }
625
626 pub fn build(self) -> VNode {
628 if self.data.is_empty() {
629 return VNode::styled_text("No data", TextStyle::color(Color::Named(NamedColor::Gray)));
630 }
631
632 let all_values: Vec<f64> = self.data.iter().flatten().copied().collect();
634 let min = all_values.iter().copied().fold(f64::INFINITY, f64::min);
635 let max = all_values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
636 let range = max - min;
637
638 let mut grid = vec![vec![' '; self.width as usize]; self.height as usize];
640
641 for (series_idx, series) in self.data.iter().enumerate() {
643 let char = ['●', '◆', '■', '▲'][series_idx % 4];
644
645 for (i, &value) in series.iter().enumerate() {
646 let x = (i * self.width as usize) / series.len().max(1);
647 let y = if range > 0.0 {
648 ((max - value) / range * (self.height - 1) as f64) as usize
649 } else {
650 self.height as usize / 2
651 };
652
653 if x < self.width as usize && y < self.height as usize {
654 grid[y][x] = char;
655 }
656 }
657 }
658
659 let mut children: Vec<VNode> = grid.into_iter().map(|row| {
660 VNode::styled_text(
661 row.into_iter().collect::<String>(),
662 TextStyle::color(Color::Named(NamedColor::White))
663 )
664 }).collect();
665
666 if self.show_legend && !self.labels.is_empty() {
668 let legend_parts: Vec<String> = self.labels.iter().enumerate()
669 .map(|(i, label)| {
670 let char = ['●', '◆', '■', '▲'][i % 4];
671 format!("{} {}", char, label)
672 })
673 .collect();
674
675 children.push(VNode::styled_text(
676 legend_parts.join(" "),
677 TextStyle { color: Some(Color::Named(NamedColor::Gray)), dim: true, ..Default::default() }
678 ));
679 }
680
681 VNode::Box(BoxNode {
682 children,
683 style: BoxStyle::default(),
684 ..Default::default()
685 })
686 }
687}
688
689#[derive(Debug, Clone)]
695pub struct Heatmap {
696 data: Vec<Vec<f64>>,
697 row_labels: Vec<String>,
698 col_labels: Vec<String>,
699 #[allow(dead_code)]
700 colors: Vec<Color>,
701}
702
703impl Default for Heatmap {
704 fn default() -> Self {
705 Self {
706 data: Vec::new(),
707 row_labels: Vec::new(),
708 col_labels: Vec::new(),
709 colors: vec![
710 Color::Named(NamedColor::Blue),
711 Color::Named(NamedColor::Cyan),
712 Color::Named(NamedColor::Green),
713 Color::Named(NamedColor::Yellow),
714 Color::Named(NamedColor::Red),
715 ],
716 }
717 }
718}
719
720impl Heatmap {
721 pub fn new() -> Self {
723 Self::default()
724 }
725
726 pub fn data(mut self, data: Vec<Vec<f64>>) -> Self {
728 self.data = data;
729 self
730 }
731
732 pub fn rows<I, S>(mut self, labels: I) -> Self
734 where
735 I: IntoIterator<Item = S>,
736 S: Into<String>,
737 {
738 self.row_labels = labels.into_iter().map(Into::into).collect();
739 self
740 }
741
742 pub fn cols<I, S>(mut self, labels: I) -> Self
744 where
745 I: IntoIterator<Item = S>,
746 S: Into<String>,
747 {
748 self.col_labels = labels.into_iter().map(Into::into).collect();
749 self
750 }
751
752 pub fn build(self) -> VNode {
754 if self.data.is_empty() {
755 return VNode::styled_text("No data", TextStyle::color(Color::Named(NamedColor::Gray)));
756 }
757
758 let heat_chars = ['░', '▒', '▓', '█'];
759
760 let all_values: Vec<f64> = self.data.iter().flatten().copied().collect();
761 let min = all_values.iter().copied().fold(f64::INFINITY, f64::min);
762 let max = all_values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
763 let range = max - min;
764
765 let mut children = Vec::new();
766
767 for (row_idx, row) in self.data.iter().enumerate() {
768 let row_label = self.row_labels.get(row_idx)
769 .map(|s| format!("{:>8} ", s))
770 .unwrap_or_default();
771
772 let cells: String = row.iter().map(|&v| {
773 let normalized = if range > 0.0 { (v - min) / range } else { 0.5 };
774 let idx = (normalized * (heat_chars.len() - 1) as f64) as usize;
775 heat_chars[idx.min(heat_chars.len() - 1)]
776 }).collect();
777
778 children.push(VNode::styled_text(
779 format!("{}{}", row_label, cells),
780 TextStyle::color(Color::Named(NamedColor::Yellow))
781 ));
782 }
783
784 VNode::Box(BoxNode {
785 children,
786 style: BoxStyle::default(),
787 ..Default::default()
788 })
789 }
790}