1use crate::color::Color;
7use crate::console::{ConsoleOptions, Renderable, RenderResult};
8use crate::segment::Segment;
9use crate::style::Style;
10
11#[derive(Debug, Clone)]
17pub struct Bar {
18 pub label: String,
20 pub value: f64,
22 pub color: Color,
24 pub style: Style,
26}
27
28impl Bar {
29 pub fn new(label: impl Into<String>, value: f64) -> Self {
31 Self {
32 label: label.into(),
33 value,
34 color: Color::default(),
35 style: Style::new(),
36 }
37 }
38
39 pub fn color(mut self, color: Color) -> Self {
41 self.color = color;
42 self
43 }
44}
45
46#[derive(Debug, Clone)]
67pub struct BarChart {
68 bars: Vec<Bar>,
69 width: Option<usize>,
70 max_value: Option<f64>,
71 title: Option<String>,
72 show_values: bool,
73 bar_char: char,
74 bar_width: usize,
75}
76
77impl Default for BarChart {
78 fn default() -> Self {
79 Self::new()
80 }
81}
82
83impl BarChart {
84 pub fn new() -> Self {
86 Self {
87 bars: Vec::new(),
88 width: None,
89 max_value: None,
90 title: None,
91 show_values: false,
92 bar_char: '\u{2588}',
93 bar_width: 40,
94 }
95 }
96
97 pub fn add(&mut self, bar: Bar) -> &mut Self {
99 self.bars.push(bar);
100 self
101 }
102
103 pub fn width(mut self, width: usize) -> Self {
105 self.width = Some(width);
106 self
107 }
108
109 pub fn title(mut self, title: impl Into<String>) -> Self {
111 self.title = Some(title.into());
112 self
113 }
114
115 pub fn show_values(mut self, show: bool) -> Self {
117 self.show_values = show;
118 self
119 }
120
121 pub fn bar_char(mut self, ch: char) -> Self {
123 self.bar_char = ch;
124 self
125 }
126
127 pub fn max_value(mut self, max: f64) -> Self {
129 self.max_value = Some(max);
130 self
131 }
132
133 pub fn bar_width(mut self, width: usize) -> Self {
135 self.bar_width = width;
136 self
137 }
138
139 fn compute_max(&self) -> f64 {
141 self.max_value.unwrap_or_else(|| {
142 self.bars
143 .iter()
144 .map(|b| b.value)
145 .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
146 .unwrap_or(1.0)
147 })
148 }
149}
150
151impl Renderable for BarChart {
152 fn render(&self, options: &ConsoleOptions) -> RenderResult {
153 let max = self.compute_max();
154 let available = self.width.unwrap_or(options.max_width).saturating_sub(20);
155 let bar_width = self.bar_width.max(available);
156
157 let mut lines = Vec::new();
158
159 if let Some(ref title) = self.title {
161 lines.push(vec![Segment::styled(title, Style::new().bold(true))]);
162 lines.push(vec![Segment::line()]);
163 }
164
165 for bar in &self.bars {
166 let filled = ((bar.value / max) * bar_width as f64) as usize;
167 let bar_str: String = self.bar_char.to_string().repeat(filled);
168 let label = format!("{:>15} ", bar.label);
169 let value_str = if self.show_values {
170 format!(" {:.1}", bar.value)
171 } else {
172 String::new()
173 };
174 let line_str = format!("{}{}{}", label, bar_str, value_str);
175
176 let mut seg = Segment::new(line_str);
177 seg.style = Some(bar.style.clone().color(bar.color));
178 lines.push(vec![seg, Segment::line()]);
179 }
180
181 RenderResult {
182 lines,
183 items: Vec::new(),
184 }
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use crate::console::ConsoleOptions;
192
193 #[test]
194 fn test_bar_creation() {
195 let bar = Bar::new("Test", 42.0).color(Color::parse("red").unwrap());
196 assert_eq!(bar.label, "Test");
197 assert_eq!(bar.value, 42.0);
198 }
199
200 #[test]
201 fn test_barchart_creation() {
202 let mut chart = BarChart::new().title("Chart").show_values(true);
203 chart.add(Bar::new("A", 10.0));
204 chart.add(Bar::new("B", 20.0));
205 assert_eq!(chart.bars.len(), 2);
206 }
207
208 #[test]
209 fn test_barchart_render() {
210 let mut chart = BarChart::new().width(60);
211 chart.add(Bar::new("Foo", 50.0));
212 chart.add(Bar::new("Bar", 100.0));
213 let opts = ConsoleOptions::default();
214 let result = chart.render(&opts);
215 assert!(!result.lines.is_empty());
216 }
217
218 #[test]
219 fn test_compute_max() {
220 let chart = BarChart::new();
221 assert_eq!(chart.compute_max(), 1.0);
222
223 let mut chart = BarChart::new();
224 chart.add(Bar::new("A", 5.0));
225 chart.add(Bar::new("B", 15.0));
226 chart.add(Bar::new("C", 10.0));
227 assert!((chart.compute_max() - 15.0).abs() < f64::EPSILON);
228 }
229}