fast_rich/
bar.rs

1//! Bar charts for visualizing data.
2//!
3//! Provides horizontal bar charts with customizable colors and widths.
4
5use crate::console::RenderContext;
6use crate::renderable::{Renderable, Segment};
7use crate::style::{Color, Style};
8use crate::text::Span;
9
10/// A single bar in a bar chart.
11#[derive(Debug, Clone)]
12pub struct BarData {
13    /// Label for this bar
14    pub label: String,
15    /// Value (will be scaled to fit width)
16    pub value: f64,
17    /// Optional color for this bar
18    pub color: Option<Color>,
19}
20
21impl BarData {
22    /// Create a new bar with label and value.
23    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    /// Set the color for this bar.
32    pub fn color(mut self, color: Color) -> Self {
33        self.color = Some(color);
34        self
35    }
36}
37
38/// A horizontal bar chart.
39#[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    /// Create a new empty bar chart.
50    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    /// Add a bar to the chart.
61    pub fn add_bar(&mut self, bar: BarData) -> &mut Self {
62        self.bars.push(bar);
63        self
64    }
65
66    /// Add a simple bar with label and value.
67    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    /// Set the width for bars (excluding label).
73    pub fn width(mut self, width: usize) -> Self {
74        self.width = Some(width);
75        self
76    }
77
78    /// Set the character used for bars.
79    pub fn bar_char(mut self, c: char) -> Self {
80        self.bar_char = c;
81        self
82    }
83
84    /// Set the default color for bars.
85    pub fn default_color(mut self, color: Color) -> Self {
86        self.default_color = color;
87        self
88    }
89
90    /// Set whether to show values next to bars.
91    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        // Find max value for scaling
110        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        // Find max label width
116        let max_label_width = self.bars.iter().map(|b| b.label.len()).max().unwrap_or(0);
117
118        // Calculate bar width
119        let value_width = if self.show_values { 12 } else { 0 }; // Space for value display
120        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            // Calculate bar length
130            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            // Choose color
134            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            // Label (left-aligned, padded)
140            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            // Bar
145            let bar_str = self.bar_char.to_string().repeat(bar_length);
146            spans.push(Span::styled(bar_str, style));
147
148            // Value (if enabled)
149            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}