Skip to main content

rumatui_tui/widgets/
barchart.rs

1use std::cmp::{max, min};
2
3use unicode_width::UnicodeWidthStr;
4
5use crate::buffer::Buffer;
6use crate::layout::Rect;
7use crate::style::Style;
8use crate::symbols::bar;
9use crate::widgets::{Block, Widget};
10
11/// Display multiple bars in a single widgets
12///
13/// # Examples
14///
15/// ```
16/// # use rumatui_tui::widgets::{Block, Borders, BarChart};
17/// # use rumatui_tui::style::{Style, Color, Modifier};
18/// BarChart::default()
19///     .block(Block::default().title("BarChart").borders(Borders::ALL))
20///     .bar_width(3)
21///     .bar_gap(1)
22///     .style(Style::default().fg(Color::Yellow).bg(Color::Red))
23///     .value_style(Style::default().fg(Color::Red).modifier(Modifier::BOLD))
24///     .label_style(Style::default().fg(Color::White))
25///     .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)])
26///     .max(4);
27/// ```
28pub struct BarChart<'a> {
29    /// Block to wrap the widget in
30    block: Option<Block<'a>>,
31    /// The width of each bar
32    bar_width: u16,
33    /// The gap between each bar
34    bar_gap: u16,
35    /// Style of the values printed at the bottom of each bar
36    value_style: Style,
37    /// Style of the labels printed under each bar
38    label_style: Style,
39    /// Style for the widget
40    style: Style,
41    /// Slice of (label, value) pair to plot on the chart
42    data: &'a [(&'a str, u64)],
43    /// Value necessary for a bar to reach the maximum height (if no value is specified,
44    /// the maximum value in the data is taken as reference)
45    max: Option<u64>,
46    /// Values to display on the bar (computed when the data is passed to the widget)
47    values: Vec<String>,
48}
49
50impl<'a> Default for BarChart<'a> {
51    fn default() -> BarChart<'a> {
52        BarChart {
53            block: None,
54            max: None,
55            data: &[],
56            values: Vec::new(),
57            bar_width: 1,
58            bar_gap: 1,
59            value_style: Default::default(),
60            label_style: Default::default(),
61            style: Default::default(),
62        }
63    }
64}
65
66impl<'a> BarChart<'a> {
67    pub fn data(mut self, data: &'a [(&'a str, u64)]) -> BarChart<'a> {
68        self.data = data;
69        self.values = Vec::with_capacity(self.data.len());
70        for &(_, v) in self.data {
71            self.values.push(format!("{}", v));
72        }
73        self
74    }
75
76    pub fn block(mut self, block: Block<'a>) -> BarChart<'a> {
77        self.block = Some(block);
78        self
79    }
80    pub fn max(mut self, max: u64) -> BarChart<'a> {
81        self.max = Some(max);
82        self
83    }
84
85    pub fn bar_width(mut self, width: u16) -> BarChart<'a> {
86        self.bar_width = width;
87        self
88    }
89    pub fn bar_gap(mut self, gap: u16) -> BarChart<'a> {
90        self.bar_gap = gap;
91        self
92    }
93    pub fn value_style(mut self, style: Style) -> BarChart<'a> {
94        self.value_style = style;
95        self
96    }
97    pub fn label_style(mut self, style: Style) -> BarChart<'a> {
98        self.label_style = style;
99        self
100    }
101    pub fn style(mut self, style: Style) -> BarChart<'a> {
102        self.style = style;
103        self
104    }
105}
106
107impl<'a> Widget for BarChart<'a> {
108    fn render(mut self, area: Rect, buf: &mut Buffer) {
109        let chart_area = match self.block {
110            Some(ref mut b) => {
111                b.render(area, buf);
112                b.inner(area)
113            }
114            None => area,
115        };
116
117        if chart_area.height < 2 {
118            return;
119        }
120
121        buf.set_background(chart_area, self.style.bg);
122
123        let max = self
124            .max
125            .unwrap_or_else(|| self.data.iter().fold(0, |acc, &(_, v)| max(v, acc)));
126        let max_index = min(
127            (chart_area.width / (self.bar_width + self.bar_gap)) as usize,
128            self.data.len(),
129        );
130        let mut data = self
131            .data
132            .iter()
133            .take(max_index)
134            .map(|&(l, v)| {
135                (
136                    l,
137                    v * u64::from(chart_area.height) * 8 / std::cmp::max(max, 1),
138                )
139            })
140            .collect::<Vec<(&str, u64)>>();
141        for j in (0..chart_area.height - 1).rev() {
142            for (i, d) in data.iter_mut().enumerate() {
143                let symbol = match d.1 {
144                    0 => " ",
145                    1 => bar::ONE_EIGHTH,
146                    2 => bar::ONE_QUARTER,
147                    3 => bar::THREE_EIGHTHS,
148                    4 => bar::HALF,
149                    5 => bar::FIVE_EIGHTHS,
150                    6 => bar::THREE_QUARTERS,
151                    7 => bar::SEVEN_EIGHTHS,
152                    _ => bar::FULL,
153                };
154
155                for x in 0..self.bar_width {
156                    buf.get_mut(
157                        chart_area.left() + i as u16 * (self.bar_width + self.bar_gap) + x,
158                        chart_area.top() + j,
159                    )
160                    .set_symbol(symbol)
161                    .set_style(self.style);
162                }
163
164                if d.1 > 8 {
165                    d.1 -= 8;
166                } else {
167                    d.1 = 0;
168                }
169            }
170        }
171
172        for (i, &(label, value)) in self.data.iter().take(max_index).enumerate() {
173            if value != 0 {
174                let value_label = &self.values[i];
175                let width = value_label.width() as u16;
176                if width < self.bar_width {
177                    buf.set_string(
178                        chart_area.left()
179                            + i as u16 * (self.bar_width + self.bar_gap)
180                            + (self.bar_width - width) / 2,
181                        chart_area.bottom() - 2,
182                        value_label,
183                        self.value_style,
184                    );
185                }
186            }
187            buf.set_stringn(
188                chart_area.left() + i as u16 * (self.bar_width + self.bar_gap),
189                chart_area.bottom() - 1,
190                label,
191                self.bar_width as usize,
192                self.label_style,
193            );
194        }
195    }
196}