1use crate::render::Cell;
6use crate::style::Color;
7use crate::widget::traits::{RenderContext, View, WidgetProps};
8use crate::{impl_props_builders, impl_styled_view};
9
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12pub enum BarOrientation {
13 #[default]
15 Horizontal,
16 Vertical,
18}
19
20#[derive(Clone, Debug)]
22pub struct Bar {
23 pub label: String,
25 pub value: f64,
27 pub color: Option<Color>,
29}
30
31impl Bar {
32 pub fn new(label: impl Into<String>, value: f64) -> Self {
34 Self {
35 label: label.into(),
36 value,
37 color: None,
38 }
39 }
40
41 pub fn color(mut self, color: Color) -> Self {
43 self.color = Some(color);
44 self
45 }
46}
47
48pub struct BarChart {
64 bars: Vec<Bar>,
65 orientation: BarOrientation,
66 max: Option<f64>,
67 bar_width: u16,
68 gap: u16,
69 show_values: bool,
70 fg: Color,
71 label_width: Option<u16>,
72 props: WidgetProps,
74}
75
76impl BarChart {
77 pub fn new() -> Self {
79 Self {
80 bars: Vec::new(),
81 orientation: BarOrientation::default(),
82 max: None,
83 bar_width: 1,
84 gap: 1,
85 show_values: true,
86 fg: Color::CYAN,
87 label_width: None,
88 props: WidgetProps::new(),
89 }
90 }
91
92 pub fn bar(mut self, label: impl Into<String>, value: f64) -> Self {
94 self.bars.push(Bar::new(label, value));
95 self
96 }
97
98 pub fn bar_colored(mut self, label: impl Into<String>, value: f64, color: Color) -> Self {
100 self.bars.push(Bar::new(label, value).color(color));
101 self
102 }
103
104 pub fn data<I, S>(mut self, data: I) -> Self
106 where
107 I: IntoIterator<Item = (S, f64)>,
108 S: Into<String>,
109 {
110 for (label, value) in data {
111 self.bars.push(Bar::new(label, value));
112 }
113 self
114 }
115
116 pub fn orientation(mut self, orientation: BarOrientation) -> Self {
118 self.orientation = orientation;
119 self
120 }
121
122 pub fn horizontal(mut self) -> Self {
124 self.orientation = BarOrientation::Horizontal;
125 self
126 }
127
128 pub fn vertical(mut self) -> Self {
130 self.orientation = BarOrientation::Vertical;
131 self
132 }
133
134 pub fn max(mut self, max: f64) -> Self {
136 self.max = Some(max);
137 self
138 }
139
140 pub fn bar_width(mut self, width: u16) -> Self {
142 self.bar_width = width.max(1);
143 self
144 }
145
146 pub fn gap(mut self, gap: u16) -> Self {
148 self.gap = gap;
149 self
150 }
151
152 pub fn show_values(mut self, show: bool) -> Self {
154 self.show_values = show;
155 self
156 }
157
158 pub fn fg(mut self, color: Color) -> Self {
160 self.fg = color;
161 self
162 }
163
164 pub fn label_width(mut self, width: u16) -> Self {
166 self.label_width = Some(width);
167 self
168 }
169
170 fn calculate_max(&self) -> f64 {
172 self.max.unwrap_or_else(|| {
173 self.bars
174 .iter()
175 .map(|b| b.value)
176 .fold(0.0, f64::max)
177 .max(1.0)
178 })
179 }
180
181 fn render_horizontal(&self, ctx: &mut RenderContext) {
183 let area = ctx.area;
184 if area.width == 0 || area.height == 0 || self.bars.is_empty() {
185 return;
186 }
187
188 let max_value = self.calculate_max();
189
190 let label_width = self.label_width.unwrap_or_else(|| {
192 self.bars
193 .iter()
194 .map(|b| b.label.len() as u16)
195 .max()
196 .unwrap_or(0)
197 .min(area.width / 3)
198 });
199
200 let value_width = if self.show_values { 8 } else { 0 };
202 let bar_area_width = area.width.saturating_sub(label_width + 2 + value_width);
203
204 let mut y = 0u16;
205 for bar in &self.bars {
206 if y >= area.height {
207 break;
208 }
209
210 let bar_length = if max_value > 0.0 {
212 ((bar.value / max_value) * bar_area_width as f64) as u16
213 } else {
214 0
215 };
216
217 let color = bar.color.unwrap_or(self.fg);
218
219 for row in 0..self.bar_width {
221 if y + row >= area.height {
222 break;
223 }
224
225 if row == 0 {
227 let label: String = if bar.label.len() > label_width as usize {
228 bar.label.chars().take(label_width as usize).collect()
229 } else {
230 format!("{:>width$}", bar.label, width = label_width as usize)
231 };
232
233 for (i, ch) in label.chars().enumerate() {
234 if (i as u16) < area.width {
235 ctx.buffer.set(area.x + i as u16, area.y + y, Cell::new(ch));
236 }
237 }
238 }
239
240 let bar_start = label_width + 1;
242 for i in 0..bar_length {
243 if bar_start + i < area.width {
244 let mut cell = Cell::new('█');
245 cell.fg = Some(color);
246 ctx.buffer
247 .set(area.x + bar_start + i, area.y + y + row, cell);
248 }
249 }
250
251 if row == 0 && self.show_values {
253 let value_str = format!(" {:.1}", bar.value);
254 let value_x = bar_start + bar_length;
255 for (i, ch) in value_str.chars().enumerate() {
256 if value_x + (i as u16) < area.width {
257 ctx.buffer.set(
258 area.x + value_x + (i as u16),
259 area.y + y,
260 Cell::new(ch),
261 );
262 }
263 }
264 }
265 }
266
267 y += self.bar_width + self.gap;
268 }
269 }
270
271 fn render_vertical(&self, ctx: &mut RenderContext) {
273 let area = ctx.area;
274 if area.width == 0 || area.height == 0 || self.bars.is_empty() {
275 return;
276 }
277
278 let max_value = self.calculate_max();
279
280 let label_height = 1;
282 let value_height = if self.show_values { 1 } else { 0 };
283 let bar_area_height = area.height.saturating_sub(label_height + value_height);
284
285 let total_bar_width = self.bar_width + self.gap;
286 let mut x = 0u16;
287
288 for bar in &self.bars {
289 if x + self.bar_width > area.width {
290 break;
291 }
292
293 let bar_height = if max_value > 0.0 {
295 ((bar.value / max_value) * bar_area_height as f64) as u16
296 } else {
297 0
298 };
299
300 let color = bar.color.unwrap_or(self.fg);
301
302 for row in 0..bar_height {
304 let y = area.y + bar_area_height - 1 - row;
305 for col in 0..self.bar_width {
306 if x + col < area.width {
307 let mut cell = Cell::new('█');
308 cell.fg = Some(color);
309 ctx.buffer.set(area.x + x + col, y, cell);
310 }
311 }
312 }
313
314 if self.show_values && bar_area_height > 0 {
316 let value_str = format!("{:.0}", bar.value);
317 let value_y =
318 area.y + bar_area_height - bar_height.saturating_sub(1).min(bar_area_height);
319 for (i, ch) in value_str.chars().enumerate() {
320 if x + (i as u16) < area.width && value_y > area.y {
321 ctx.buffer
322 .set(area.x + x + (i as u16), value_y - 1, Cell::new(ch));
323 }
324 }
325 }
326
327 if label_height > 0 {
329 let label_y = area.y + area.height - 1;
330 let label: String = bar.label.chars().take(self.bar_width as usize).collect();
331 for (i, ch) in label.chars().enumerate() {
332 if x + (i as u16) < area.width {
333 ctx.buffer
334 .set(area.x + x + (i as u16), label_y, Cell::new(ch));
335 }
336 }
337 }
338
339 x += total_bar_width;
340 }
341 }
342}
343
344impl Default for BarChart {
345 fn default() -> Self {
346 Self::new()
347 }
348}
349
350impl View for BarChart {
351 crate::impl_view_meta!("BarChart");
352
353 fn render(&self, ctx: &mut RenderContext) {
354 match self.orientation {
355 BarOrientation::Horizontal => self.render_horizontal(ctx),
356 BarOrientation::Vertical => self.render_vertical(ctx),
357 }
358 }
359}
360
361impl_styled_view!(BarChart);
362impl_props_builders!(BarChart);
363
364pub fn barchart() -> BarChart {
366 BarChart::new()
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372 use crate::layout::Rect;
373 use crate::render::Buffer;
374
375 #[test]
376 fn test_barchart_new() {
377 let chart = BarChart::new();
378 assert!(chart.bars.is_empty());
379 assert_eq!(chart.orientation, BarOrientation::Horizontal);
380 }
381
382 #[test]
383 fn test_barchart_bar() {
384 let chart = BarChart::new().bar("A", 10.0).bar("B", 20.0).bar("C", 30.0);
385
386 assert_eq!(chart.bars.len(), 3);
387 assert_eq!(chart.bars[0].label, "A");
388 assert_eq!(chart.bars[0].value, 10.0);
389 }
390
391 #[test]
392 fn test_barchart_data() {
393 let data = vec![("Sales", 100.0), ("Revenue", 200.0)];
394
395 let chart = BarChart::new().data(data);
396 assert_eq!(chart.bars.len(), 2);
397 }
398
399 #[test]
400 fn test_barchart_orientation() {
401 let h = BarChart::new().horizontal();
402 assert_eq!(h.orientation, BarOrientation::Horizontal);
403
404 let v = BarChart::new().vertical();
405 assert_eq!(v.orientation, BarOrientation::Vertical);
406 }
407
408 #[test]
409 fn test_barchart_styling() {
410 let chart = BarChart::new()
411 .max(100.0)
412 .bar_width(2)
413 .gap(1)
414 .fg(Color::GREEN)
415 .show_values(true);
416
417 assert_eq!(chart.max, Some(100.0));
418 assert_eq!(chart.bar_width, 2);
419 assert_eq!(chart.gap, 1);
420 assert_eq!(chart.fg, Color::GREEN);
421 assert!(chart.show_values);
422 }
423
424 #[test]
425 fn test_barchart_render_horizontal() {
426 let chart = BarChart::new()
427 .bar("A", 50.0)
428 .bar("B", 100.0)
429 .max(100.0)
430 .bar_width(1);
431
432 let mut buffer = Buffer::new(40, 5);
433 let area = Rect::new(0, 0, 40, 5);
434 let mut ctx = RenderContext::new(&mut buffer, area);
435
436 chart.render(&mut ctx);
437 }
439
440 #[test]
441 fn test_barchart_render_vertical() {
442 let chart = BarChart::new()
443 .bar("A", 50.0)
444 .bar("B", 100.0)
445 .vertical()
446 .max(100.0)
447 .bar_width(3);
448
449 let mut buffer = Buffer::new(20, 10);
450 let area = Rect::new(0, 0, 20, 10);
451 let mut ctx = RenderContext::new(&mut buffer, area);
452
453 chart.render(&mut ctx);
454 }
456
457 #[test]
458 fn test_barchart_colored() {
459 let chart = BarChart::new()
460 .bar_colored("Red", 50.0, Color::RED)
461 .bar_colored("Green", 75.0, Color::GREEN)
462 .bar_colored("Blue", 100.0, Color::BLUE);
463
464 assert_eq!(chart.bars.len(), 3);
465 assert_eq!(chart.bars[0].color, Some(Color::RED));
466 }
467
468 #[test]
469 fn test_barchart_helper() {
470 let chart = barchart().bar("Test", 42.0);
471
472 assert_eq!(chart.bars.len(), 1);
473 }
474
475 #[test]
476 fn test_barchart_calculate_max() {
477 let chart = BarChart::new().bar("A", 10.0).bar("B", 50.0).bar("C", 30.0);
478
479 assert_eq!(chart.calculate_max(), 50.0);
480
481 let chart_with_max = chart.max(100.0);
482 assert_eq!(chart_with_max.calculate_max(), 100.0);
483 }
484
485 #[test]
486 fn test_barchart_empty() {
487 let chart = BarChart::new();
488
489 let mut buffer = Buffer::new(20, 10);
490 let area = Rect::new(0, 0, 20, 10);
491 let mut ctx = RenderContext::new(&mut buffer, area);
492
493 chart.render(&mut ctx);
494 }
496
497 #[test]
498 fn test_bar_struct() {
499 let bar = Bar::new("Test", 42.0).color(Color::YELLOW);
500 assert_eq!(bar.label, "Test");
501 assert_eq!(bar.value, 42.0);
502 assert_eq!(bar.color, Some(Color::YELLOW));
503 }
504}