1use ratatui::{
4 buffer::Buffer,
5 layout::Rect,
6 style::{Color, Style},
7 widgets::{Block, Widget},
8};
9
10#[derive(Debug, Clone)]
12pub struct BarChart {
13 data: Vec<(String, u64)>,
14 bar_width: u16,
15 bar_gap: u16,
16 bar_style: Style,
17 value_style: Style,
18 label_style: Style,
19 max_height: u16,
20 title: Option<String>,
21 block: Option<Block<'static>>,
22 highlight_indices: Vec<usize>,
23 highlight_style: Style,
24}
25
26impl BarChart {
27 pub fn new(data: Vec<(String, u64)>) -> Self {
29 Self {
30 data,
31 bar_width: 3,
32 bar_gap: 1,
33 bar_style: Style::default(),
34 value_style: Style::default(),
35 label_style: Style::default(),
36 max_height: 10,
37 title: None,
38 block: None,
39 highlight_indices: Vec::new(),
40 highlight_style: Style::default().fg(Color::Yellow),
41 }
42 }
43
44 pub fn bar_style(mut self, style: Style) -> Self {
46 self.bar_style = style;
47 self
48 }
49
50 pub fn value_style(mut self, style: Style) -> Self {
52 self.value_style = style;
53 self
54 }
55
56 pub fn label_style(mut self, style: Style) -> Self {
58 self.label_style = style;
59 self
60 }
61
62 pub fn max_height(mut self, height: u16) -> Self {
64 self.max_height = height;
65 self
66 }
67
68 pub fn bar_width(mut self, width: u16) -> Self {
70 self.bar_width = width.max(1);
71 self
72 }
73
74 pub fn bar_gap(mut self, gap: u16) -> Self {
76 self.bar_gap = gap;
77 self
78 }
79
80 pub fn title<T>(mut self, title: T) -> Self
82 where
83 T: Into<String>,
84 {
85 self.title = Some(title.into());
86 self
87 }
88
89 pub fn block(mut self, block: Block<'static>) -> Self {
91 self.block = Some(block);
92 self
93 }
94
95 pub fn highlight_indices(mut self, indices: Vec<usize>) -> Self {
97 self.highlight_indices = indices;
98 self
99 }
100
101 pub fn highlight_style(mut self, style: Style) -> Self {
103 self.highlight_style = style;
104 self
105 }
106
107 pub fn from_array_with_colors(array: &[i32], highlights: &[usize]) -> Self {
109 let data: Vec<(String, u64)> = array
110 .iter()
111 .map(|&value| (value.to_string(), value.max(0) as u64))
112 .collect();
113
114 let mut chart = Self::new(data);
115 chart.highlight_indices = highlights.to_vec();
116
117 if highlights.len() == 2 {
119 chart = chart.bar_style(Style::default().fg(Color::Blue));
121 } else if highlights.len() == 1 {
122 chart = chart.bar_style(Style::default().fg(Color::Red));
124 }
125
126 chart
127 }
128
129 pub fn from_array_compact(array: &[i32], highlights: &[usize], terminal_width: u16) -> (Self, String) {
131 let array_len = array.len();
132 let available_width = terminal_width.saturating_sub(4) as usize;
133
134 let (sampled_data, sample_rate) = if array_len <= available_width {
136 (array.to_vec(), 1)
138 } else {
139 let sample_rate = array_len.div_ceil(available_width);
141 let sampled: Vec<i32> = (0..available_width)
142 .map(|i| {
143 let idx = (i * sample_rate).min(array_len - 1);
144 array[idx]
145 })
146 .collect();
147 (sampled, sample_rate)
148 };
149
150 let min_val = *sampled_data.iter().min().unwrap_or(&0);
152 let max_val = *sampled_data.iter().max().unwrap_or(&1);
153 let range = (max_val - min_val).max(1) as f64;
154
155 let data: Vec<(String, u64)> = sampled_data
157 .iter()
158 .map(|&value| {
159 let normalized = ((value - min_val) as f64 / range * 8.0) as usize;
161 let block_char = match normalized {
162 0 => " ",
163 1 => "▁",
164 2 => "▂",
165 3 => "▃",
166 4 => "▄",
167 5 => "▅",
168 6 => "▆",
169 7 => "▇",
170 _ => "█",
171 };
172 (block_char.to_string(), value.max(0) as u64)
173 })
174 .collect();
175
176 let mut chart = Self::new(data);
177
178 if sample_rate > 1 {
180 chart.highlight_indices = highlights
181 .iter()
182 .map(|&idx| idx / sample_rate)
183 .filter(|&idx| idx < available_width)
184 .collect();
185 } else {
186 chart.highlight_indices = highlights.to_vec();
187 }
188
189 let indicator = if sample_rate > 1 {
190 format!("[Compact view: 1:{} sampling of {} elements]", sample_rate, array_len)
191 } else {
192 format!("[Compact view: {} elements]", array_len)
193 };
194
195 (chart, indicator)
196 }
197
198 pub fn from_array_with_viewport(
200 array: &[i32],
201 highlights: &[usize],
202 terminal_width: u16,
203 viewport_center: Option<usize>
204 ) -> (Self, String) {
205 let array_len = array.len();
206
207 if array_len > 500 {
209 return Self::from_array_compact(array, highlights, terminal_width);
210 }
211
212 let max_elements = (terminal_width / 4).min(100) as usize; let array_len = array.len();
217 let (start, end) = if array_len <= max_elements {
218 (0, array_len)
220 } else {
221 let center = viewport_center
223 .or_else(|| highlights.first().copied()) .unwrap_or(array_len / 2); let half_view = max_elements / 2;
227 let start = center.saturating_sub(half_view);
228 let end = (start + max_elements).min(array_len);
229
230 let start = if end == array_len {
232 array_len.saturating_sub(max_elements)
233 } else {
234 start
235 };
236
237 (start, end)
238 };
239
240 let visible_data: Vec<(String, u64)> = array[start..end]
242 .iter()
243 .map(|&value| (value.to_string(), value.max(0) as u64))
244 .collect();
245
246 let mut chart = Self::new(visible_data);
247
248 chart.highlight_indices = highlights
250 .iter()
251 .filter_map(|&idx| {
252 if idx >= start && idx < end {
253 Some(idx - start)
254 } else {
255 None
256 }
257 })
258 .collect();
259
260 if highlights.len() == 2 {
262 chart = chart.bar_style(Style::default().fg(Color::Blue));
263 } else if highlights.len() == 1 {
264 chart = chart.bar_style(Style::default().fg(Color::Red));
265 }
266
267 let indicator = if array_len > max_elements {
269 format!("[Showing {}-{} of {}]", start + 1, end, array_len)
270 } else {
271 String::new()
272 };
273
274 (chart, indicator)
275 }
276
277 pub fn scale_for_terminal(mut self, terminal_width: u16, terminal_height: u16) -> Self {
279 let available_width = terminal_width.saturating_sub(4); let bar_count = self.data.len() as u16;
282
283 if bar_count > 0 {
284 let max_digits = self.data.iter()
286 .map(|(_, value)| value.to_string().len() as u16)
287 .max()
288 .unwrap_or(1);
289
290 let min_label_space = max_digits + 1; let min_bar_width = max_digits.max(2); let total_label_space_needed = bar_count * min_label_space;
296 if total_label_space_needed > available_width {
297 let space_per_element = (available_width / bar_count).max(1);
299 self.bar_width = (space_per_element.saturating_sub(1)).max(1);
300 self.bar_gap = if space_per_element > 1 { 1 } else { 0 };
301 } else {
302 let remaining_space = available_width - total_label_space_needed;
304 let extra_bar_width = remaining_space / bar_count;
305
306 self.bar_width = min_bar_width + extra_bar_width;
307 self.bar_gap = 1; }
309 }
310
311 let available_height = terminal_height.saturating_sub(6); self.max_height = available_height.min(20); self
316 }
317
318 pub fn render(&self, area: Rect, buf: &mut Buffer) {
320 if area.width < 3 || area.height < 3 {
321 return; }
323
324 let inner_area = if let Some(ref block) = self.block {
325 let inner = block.inner(area);
326 block.render(area, buf);
327 inner
328 } else {
329 area
330 };
331
332 if self.data.is_empty() {
333 return;
334 }
335
336 let max_value = self.data.iter().map(|(_, value)| *value).max().unwrap_or(1);
338 if max_value == 0 {
339 return;
340 }
341
342 let available_height = inner_area.height.saturating_sub(2); let bar_height_scale = self.max_height.min(available_height) as f64;
345
346 let mut x_offset = inner_area.left();
347
348 for (i, (label, value)) in self.data.iter().enumerate() {
349 if x_offset + self.bar_width >= inner_area.right() {
350 break; }
352
353 let bar_height = if max_value > 0 {
355 (((*value as f64) / (max_value as f64)) * bar_height_scale).ceil() as u16
356 } else {
357 0
358 };
359
360 let current_bar_style = if self.highlight_indices.contains(&i) {
362 self.highlight_style
363 } else {
364 self.bar_style
365 };
366
367 for y in 0..bar_height {
369 let bar_y = inner_area.bottom().saturating_sub(2 + y);
370 if bar_y >= inner_area.top() && bar_y < inner_area.bottom() {
371 for x in x_offset..x_offset + self.bar_width {
372 if x < inner_area.right() {
373 buf[(x, bar_y)]
374 .set_symbol("█")
375 .set_style(current_bar_style);
376 }
377 }
378 }
379 }
380
381 if bar_height > 0 {
383 let value_y = inner_area.bottom().saturating_sub(2 + bar_height);
384 if value_y > inner_area.top() {
385 let value_str = value.to_string();
386
387 let display_str = if value_str.len() as u16 > self.bar_width {
389 value_str
391 } else {
392 value_str
393 };
394
395 let value_x = if display_str.len() as u16 <= self.bar_width {
396 x_offset + (self.bar_width.saturating_sub(display_str.len() as u16)) / 2
398 } else {
399 x_offset
401 };
402
403 for (char_idx, ch) in display_str.chars().enumerate() {
404 let char_x = value_x + char_idx as u16;
405 if char_x < inner_area.right() && value_y >= inner_area.top() {
406 buf[(char_x, value_y.saturating_sub(1))]
407 .set_symbol(&ch.to_string())
408 .set_style(self.value_style);
409 }
410 }
411 }
412 }
413
414 let label_y = inner_area.bottom().saturating_sub(1);
416 if label_y >= inner_area.top() && label_y < inner_area.bottom() {
417 let label_display = label.as_str();
418
419 let label_x = if label_display.len() as u16 <= self.bar_width {
421 x_offset + (self.bar_width.saturating_sub(label_display.len() as u16)) / 2
423 } else {
424 x_offset
426 };
427
428 for (char_idx, ch) in label_display.chars().enumerate() {
430 let char_x = label_x + char_idx as u16;
431 if char_x < inner_area.right() {
432 buf[(char_x, label_y)]
433 .set_symbol(&ch.to_string())
434 .set_style(self.label_style);
435 }
436 }
437
438 let space_x = label_x + label_display.len() as u16;
441 if space_x < inner_area.right() {
442 buf[(space_x, label_y)]
443 .set_symbol(" ")
444 .set_style(self.label_style);
445 }
446 }
447
448 let label_width = label.len() as u16 + 1; let min_advance = label_width.max(self.bar_width + self.bar_gap);
451 x_offset += min_advance;
452 }
453 }
454
455 pub fn data(&self) -> &[(String, u64)] {
457 &self.data
458 }
459}
460
461impl Widget for BarChart {
462 fn render(self, area: Rect, buf: &mut Buffer) {
463 BarChart::render(&self, area, buf);
464 }
465}
466
467impl Widget for &BarChart {
469 fn render(self, area: Rect, buf: &mut Buffer) {
470 BarChart::render(self, area, buf);
471 }
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477 use ratatui::{
478 buffer::Buffer,
479 layout::Rect,
480 style::Color,
481 };
482
483 #[test]
484 fn test_bar_chart_creation_from_array_data() {
485 let array_data = vec![5, 3, 8, 1, 9, 2];
486 let highlights = vec![];
487
488 let chart = BarChart::from_array_with_colors(&array_data, &highlights);
489
490 assert_eq!(chart.data.len(), 6);
491 assert_eq!(chart.data[0], ("5".to_string(), 5));
492 assert_eq!(chart.data[1], ("3".to_string(), 3));
493 assert_eq!(chart.data[4], ("9".to_string(), 9));
494 }
495
496 #[test]
497 fn test_color_mapping_for_operations() {
498 let array_data = vec![5, 3, 8, 1];
499
500 let comparison_highlights = vec![0, 2];
502 let comparison_chart = BarChart::from_array_with_colors(&array_data, &comparison_highlights);
503 assert_eq!(comparison_chart.bar_style.fg, Some(Color::Blue));
504
505 let swap_highlights = vec![1];
507 let swap_chart = BarChart::from_array_with_colors(&array_data, &swap_highlights);
508 assert_eq!(swap_chart.bar_style.fg, Some(Color::Red));
509
510 let no_highlights = vec![];
512 let default_chart = BarChart::from_array_with_colors(&array_data, &no_highlights);
513 assert_eq!(default_chart.bar_style.fg, None);
514 }
515
516 #[test]
517 fn test_height_scaling_for_different_terminal_sizes() {
518 let array_data = vec![1, 2, 3, 4, 5];
519 let highlights = vec![];
520
521 let chart_small = BarChart::from_array_with_colors(&array_data, &highlights)
523 .scale_for_terminal(40, 10);
524 assert!(chart_small.max_height <= 4);
525
526 let chart_large = BarChart::from_array_with_colors(&array_data, &highlights)
528 .scale_for_terminal(120, 30);
529 assert!(chart_large.max_height >= 10);
530 assert!(chart_large.max_height <= 20);
531 }
532
533 #[test]
534 fn test_bar_width_scaling_for_many_elements() {
535 let array_data: Vec<i32> = (0..50).collect();
536 let highlights = vec![];
537
538 let chart = BarChart::from_array_with_colors(&array_data, &highlights)
539 .scale_for_terminal(60, 20);
540
541 assert!(chart.bar_width <= 2);
542
543 let total_width = array_data.len() as u16 * (chart.bar_width + chart.bar_gap);
544 assert!(total_width <= 56);
545 }
546
547 #[test]
548 fn test_rendering_to_ratatui_buffer() {
549 let array_data = vec![5, 3, 8, 1];
550 let highlights = vec![0, 2];
551 let chart = BarChart::from_array_with_colors(&array_data, &highlights);
552
553 let area = Rect::new(0, 0, 40, 10);
554 let mut buffer = Buffer::empty(area);
555
556 chart.render(area, &mut buffer);
558
559 let content = buffer.content();
561 assert!(!content.is_empty());
562 }
563
564 #[test]
565 fn test_builder_pattern_methods() {
566 let data = vec![("A".to_string(), 10), ("B".to_string(), 20)];
567 let chart = BarChart::new(data)
568 .bar_width(5)
569 .bar_gap(2)
570 .max_height(15)
571 .bar_style(Style::default().fg(Color::Green))
572 .value_style(Style::default().fg(Color::Yellow))
573 .label_style(Style::default().fg(Color::Cyan));
574
575 assert_eq!(chart.bar_width, 5);
576 assert_eq!(chart.bar_gap, 2);
577 assert_eq!(chart.max_height, 15);
578 assert_eq!(chart.bar_style.fg, Some(Color::Green));
579 assert_eq!(chart.value_style.fg, Some(Color::Yellow));
580 assert_eq!(chart.label_style.fg, Some(Color::Cyan));
581 }
582
583 #[test]
584 fn test_empty_data_handling() {
585 let empty_data = vec![];
586 let highlights = vec![];
587 let chart = BarChart::from_array_with_colors(&empty_data, &highlights);
588
589 assert_eq!(chart.data.len(), 0);
590
591 let scaled_chart = chart.scale_for_terminal(80, 20);
592 assert!(scaled_chart.bar_width >= 1);
593 }
594
595 #[test]
596 fn test_negative_values_handling() {
597 let array_data = vec![-5, 3, -1, 8];
598 let highlights = vec![];
599 let chart = BarChart::from_array_with_colors(&array_data, &highlights);
600
601 assert_eq!(chart.data[0], ("-5".to_string(), 0));
603 assert_eq!(chart.data[1], ("3".to_string(), 3));
604 assert_eq!(chart.data[2], ("-1".to_string(), 0));
605 assert_eq!(chart.data[3], ("8".to_string(), 8));
606 }
607}