Skip to main content

rusty_rich/
bar.rs

1//! Bar chart renderable — horizontal bars with labels.
2//!
3//! Equivalent to Rich's `bar.py`. Renders a set of labeled horizontal bars
4//! with optional title, value display, and custom bar characters.
5
6use crate::color::Color;
7use crate::console::{ConsoleOptions, Renderable, RenderResult};
8use crate::segment::Segment;
9use crate::style::Style;
10
11// ---------------------------------------------------------------------------
12// Bar
13// ---------------------------------------------------------------------------
14
15/// A single bar in a bar chart.
16#[derive(Debug, Clone)]
17pub struct Bar {
18    /// The label displayed to the left of the bar.
19    pub label: String,
20    /// The numeric value determining bar length.
21    pub value: f64,
22    /// The color of the bar.
23    pub color: Color,
24    /// The style applied to the bar text.
25    pub style: Style,
26}
27
28impl Bar {
29    /// Create a new `Bar` with the given label and value.
30    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    /// Builder: set the bar color.
40    pub fn color(mut self, color: Color) -> Self {
41        self.color = color;
42        self
43    }
44}
45
46// ---------------------------------------------------------------------------
47// BarChart
48// ---------------------------------------------------------------------------
49
50/// A bar chart with multiple bars.
51///
52/// Displays horizontal bars with left-aligned labels, optionally showing
53/// a title, bar values, and a custom bar character.
54///
55/// # Example
56///
57/// ```rust
58/// use rusty_rich::{BarChart, Bar, Color};
59///
60/// let chart = BarChart::new()
61///     .title("Sales by Quarter")
62///     .show_values(true)
63///     .bar_char('\u{2588}')
64///     .width(60);
65/// ```
66#[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    /// Create a new empty `BarChart`.
85    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    /// Add a bar to the chart (mutable, chaining).
98    pub fn add(&mut self, bar: Bar) -> &mut Self {
99        self.bars.push(bar);
100        self
101    }
102
103    /// Builder: set the chart width.
104    pub fn width(mut self, width: usize) -> Self {
105        self.width = Some(width);
106        self
107    }
108
109    /// Builder: set the chart title.
110    pub fn title(mut self, title: impl Into<String>) -> Self {
111        self.title = Some(title.into());
112        self
113    }
114
115    /// Builder: show numeric values after each bar.
116    pub fn show_values(mut self, show: bool) -> Self {
117        self.show_values = show;
118        self
119    }
120
121    /// Builder: set the character used to draw bars.
122    pub fn bar_char(mut self, ch: char) -> Self {
123        self.bar_char = ch;
124        self
125    }
126
127    /// Builder: set the maximum value for bar scaling.
128    pub fn max_value(mut self, max: f64) -> Self {
129        self.max_value = Some(max);
130        self
131    }
132
133    /// Builder: set the width of the bar area in characters.
134    pub fn bar_width(mut self, width: usize) -> Self {
135        self.bar_width = width;
136        self
137    }
138
139    /// Auto-compute max value from bars.
140    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        // Optional title
160        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}