1use crate::console::RenderContext;
6use crate::renderable::{Renderable, Segment};
7use crate::style::{Color, Style};
8use crate::text::Span;
9
10#[derive(Debug, Clone)]
12pub struct BarData {
13 pub label: String,
15 pub value: f64,
17 pub color: Option<Color>,
19}
20
21impl BarData {
22 pub fn new(label: impl Into<String>, value: f64) -> Self {
24 BarData {
25 label: label.into(),
26 value,
27 color: None,
28 }
29 }
30
31 pub fn color(mut self, color: Color) -> Self {
33 self.color = Some(color);
34 self
35 }
36}
37
38#[derive(Debug, Clone)]
40pub struct BarChart {
41 bars: Vec<BarData>,
42 width: Option<usize>,
43 bar_char: char,
44 default_color: Color,
45 show_values: bool,
46}
47
48impl BarChart {
49 pub fn new() -> Self {
51 BarChart {
52 bars: Vec::new(),
53 width: None,
54 bar_char: '█',
55 default_color: Color::Green,
56 show_values: true,
57 }
58 }
59
60 pub fn add_bar(&mut self, bar: BarData) -> &mut Self {
62 self.bars.push(bar);
63 self
64 }
65
66 pub fn bar(&mut self, label: impl Into<String>, value: f64) -> &mut Self {
68 self.add_bar(BarData::new(label, value));
69 self
70 }
71
72 pub fn width(mut self, width: usize) -> Self {
74 self.width = Some(width);
75 self
76 }
77
78 pub fn bar_char(mut self, c: char) -> Self {
80 self.bar_char = c;
81 self
82 }
83
84 pub fn default_color(mut self, color: Color) -> Self {
86 self.default_color = color;
87 self
88 }
89
90 pub fn show_values(mut self, show: bool) -> Self {
92 self.show_values = show;
93 self
94 }
95}
96
97impl Default for BarChart {
98 fn default() -> Self {
99 Self::new()
100 }
101}
102
103impl Renderable for BarChart {
104 fn render(&self, context: &RenderContext) -> Vec<Segment> {
105 if self.bars.is_empty() {
106 return vec![Segment::empty_line()];
107 }
108
109 let max_value = self.bars.iter().map(|b| b.value).fold(0.0, f64::max);
111 if max_value == 0.0 {
112 return vec![Segment::empty_line()];
113 }
114
115 let max_label_width = self.bars.iter().map(|b| b.label.len()).max().unwrap_or(0);
117
118 let value_width = if self.show_values { 12 } else { 0 }; let bar_width = self.width.unwrap_or_else(|| {
121 context
122 .width
123 .saturating_sub(max_label_width + 3 + value_width)
124 });
125
126 let mut segments = Vec::new();
127
128 for bar in &self.bars {
129 let bar_length = ((bar.value / max_value) * bar_width as f64).round() as usize;
131 let bar_length = bar_length.min(bar_width);
132
133 let color = bar.color.unwrap_or(self.default_color);
135 let style = Style::new().foreground(color);
136
137 let mut spans = Vec::new();
138
139 let label_padded = format!("{:<width$}", bar.label, width = max_label_width);
141 spans.push(Span::styled(label_padded, Style::new().dim()));
142 spans.push(Span::raw(" "));
143
144 let bar_str = self.bar_char.to_string().repeat(bar_length);
146 spans.push(Span::styled(bar_str, style));
147
148 if self.show_values {
150 let remaining = bar_width.saturating_sub(bar_length);
151 spans.push(Span::raw(" ".repeat(remaining.max(1))));
152 spans.push(Span::styled(
153 format!("{:.1}", bar.value),
154 Style::new().foreground(Color::Cyan),
155 ));
156 }
157
158 segments.push(Segment::line(spans));
159 }
160
161 segments
162 }
163}