tui_realm_stdlib/components/
bar_chart.rs

1//! ## BarChart
2//!
3//! A chart with bars
4
5use std::collections::LinkedList;
6use tuirealm::command::{Cmd, CmdResult, Direction, Position};
7use tuirealm::props::{
8    Alignment, AttrValue, Attribute, Borders, Color, PropPayload, PropValue, Props, Style,
9};
10use tuirealm::ratatui::{layout::Rect, widgets::BarChart as TuiBarChart};
11use tuirealm::{Frame, MockComponent, State};
12
13// -- Props
14
15use super::props::{
16    BAR_CHART_BARS_GAP, BAR_CHART_BARS_STYLE, BAR_CHART_LABEL_STYLE, BAR_CHART_MAX_BARS,
17    BAR_CHART_VALUES_STYLE,
18};
19
20// -- states
21
22/// ### BarChartStates
23///
24/// Bar chart states
25#[derive(Default)]
26pub struct BarChartStates {
27    pub cursor: usize,
28}
29
30impl BarChartStates {
31    /// ### move_cursor_left
32    ///
33    /// Move cursor to the left
34    pub fn move_cursor_left(&mut self) {
35        if self.cursor > 0 {
36            self.cursor -= 1;
37        }
38    }
39
40    /// ### move_cursor_right
41    ///
42    /// Move cursor to the right
43    pub fn move_cursor_right(&mut self, data_len: usize) {
44        if data_len > 0 && self.cursor + 1 < data_len {
45            self.cursor += 1;
46        }
47    }
48
49    /// ### reset_cursor
50    ///
51    /// Reset cursor to 0
52    pub fn reset_cursor(&mut self) {
53        self.cursor = 0;
54    }
55
56    /// ### cursor_at_end
57    ///
58    /// Move cursor to the end of the chart
59    pub fn cursor_at_end(&mut self, data_len: usize) {
60        if data_len > 0 {
61            self.cursor = data_len - 1;
62        } else {
63            self.cursor = 0;
64        }
65    }
66}
67
68// -- component
69
70/// ### BarChart
71///
72/// A component to display a chart with bars.
73/// The bar chart can work both in "active" and "disabled" mode.
74///
75/// #### Disabled mode
76///
77/// When in disabled mode, the chart won't be interactive, so you won't be able to move through data using keys.
78/// If you have more data than the maximum amount of bars that can be displayed, you'll have to update data to display the remaining entries
79///
80/// #### Active mode
81///
82/// While in active mode (default) you can put as many entries as you wish. You can move with arrows and END/HOME keys
83#[derive(Default)]
84#[must_use]
85pub struct BarChart {
86    props: Props,
87    pub states: BarChartStates,
88}
89
90impl BarChart {
91    pub fn foreground(mut self, fg: Color) -> Self {
92        self.attr(Attribute::Foreground, AttrValue::Color(fg));
93        self
94    }
95
96    pub fn background(mut self, bg: Color) -> Self {
97        self.attr(Attribute::Background, AttrValue::Color(bg));
98        self
99    }
100
101    pub fn borders(mut self, b: Borders) -> Self {
102        self.attr(Attribute::Borders, AttrValue::Borders(b));
103        self
104    }
105
106    pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
107        self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
108        self
109    }
110
111    pub fn disabled(mut self, disabled: bool) -> Self {
112        self.attr(Attribute::Disabled, AttrValue::Flag(disabled));
113        self
114    }
115
116    pub fn inactive(mut self, s: Style) -> Self {
117        self.attr(Attribute::FocusStyle, AttrValue::Style(s));
118        self
119    }
120
121    pub fn data(mut self, data: &[(&str, u64)]) -> Self {
122        let mut list: LinkedList<PropPayload> = LinkedList::new();
123        for (a, b) in data {
124            list.push_back(PropPayload::Tup2((
125                PropValue::Str((*a).to_string()),
126                PropValue::U64(*b),
127            )));
128        }
129        self.attr(
130            Attribute::Dataset,
131            AttrValue::Payload(PropPayload::Linked(list)),
132        );
133        self
134    }
135
136    pub fn bar_gap(mut self, gap: u16) -> Self {
137        self.attr(Attribute::Custom(BAR_CHART_BARS_GAP), AttrValue::Size(gap));
138        self
139    }
140
141    pub fn bar_style(mut self, s: Style) -> Self {
142        self.attr(Attribute::Custom(BAR_CHART_BARS_STYLE), AttrValue::Style(s));
143        self
144    }
145
146    pub fn label_style(mut self, s: Style) -> Self {
147        self.attr(
148            Attribute::Custom(BAR_CHART_LABEL_STYLE),
149            AttrValue::Style(s),
150        );
151        self
152    }
153
154    pub fn max_bars(mut self, l: usize) -> Self {
155        self.attr(Attribute::Custom(BAR_CHART_MAX_BARS), AttrValue::Length(l));
156        self
157    }
158
159    pub fn value_style(mut self, s: Style) -> Self {
160        self.attr(
161            Attribute::Custom(BAR_CHART_VALUES_STYLE),
162            AttrValue::Style(s),
163        );
164        self
165    }
166
167    pub fn width(mut self, w: u16) -> Self {
168        self.attr(Attribute::Width, AttrValue::Size(w));
169        self
170    }
171
172    fn is_disabled(&self) -> bool {
173        self.props
174            .get_or(Attribute::Disabled, AttrValue::Flag(false))
175            .unwrap_flag()
176    }
177
178    /// ### data_len
179    ///
180    /// Retrieve current data len from properties
181    fn data_len(&self) -> usize {
182        self.props
183            .get(Attribute::Dataset)
184            .map_or(0, |x| x.unwrap_payload().unwrap_linked().len())
185    }
186
187    fn get_data(&self, start: usize, len: usize) -> Vec<(String, u64)> {
188        if let Some(PropPayload::Linked(list)) = self
189            .props
190            .get(Attribute::Dataset)
191            .map(|x| x.unwrap_payload())
192        {
193            // Recalc len
194            let len: usize = std::cmp::min(len, self.data_len() - start);
195            // Prepare data storage
196            let mut data: Vec<(String, u64)> = Vec::with_capacity(len);
197            for (cursor, item) in list.iter().enumerate() {
198                // If before start, continue
199                if cursor < start {
200                    continue;
201                }
202                // Push item
203                if let PropPayload::Tup2((PropValue::Str(label), PropValue::U64(value))) = item {
204                    data.push((label.clone(), *value));
205                }
206                // Break
207                if data.len() >= len {
208                    break;
209                }
210            }
211
212            data
213        } else {
214            Vec::new()
215        }
216    }
217}
218
219impl MockComponent for BarChart {
220    fn view(&mut self, render: &mut Frame, area: Rect) {
221        if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
222            let foreground = self
223                .props
224                .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
225                .unwrap_color();
226            let background = self
227                .props
228                .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
229                .unwrap_color();
230            let borders = self
231                .props
232                .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
233                .unwrap_borders();
234            let title = self
235                .props
236                .get_ref(Attribute::Title)
237                .and_then(|x| x.as_title());
238            let focus = self
239                .props
240                .get_or(Attribute::Focus, AttrValue::Flag(false))
241                .unwrap_flag();
242            let inactive_style = self
243                .props
244                .get(Attribute::FocusStyle)
245                .map(|x| x.unwrap_style());
246            let active: bool = if self.is_disabled() { true } else { focus };
247            let mut div = crate::utils::get_block(borders, title, active, inactive_style);
248            div = div.style(Style::default().bg(background).fg(foreground));
249            // Get max elements
250            let data_max_len = self
251                .props
252                .get(Attribute::Custom(BAR_CHART_MAX_BARS))
253                .map_or(self.data_len(), |x| x.unwrap_length());
254            // Get data
255            let data = self.get_data(self.states.cursor, data_max_len);
256            let data_ref: Vec<(&str, u64)> = data.iter().map(|x| (x.0.as_str(), x.1)).collect();
257            // Create widget
258            let mut widget: TuiBarChart =
259                TuiBarChart::default().block(div).data(data_ref.as_slice());
260            if let Some(gap) = self
261                .props
262                .get(Attribute::Custom(BAR_CHART_BARS_GAP))
263                .map(|x| x.unwrap_size())
264            {
265                widget = widget.bar_gap(gap);
266            }
267            if let Some(width) = self.props.get(Attribute::Width).map(|x| x.unwrap_size()) {
268                widget = widget.bar_width(width);
269            }
270            if let Some(style) = self
271                .props
272                .get(Attribute::Custom(BAR_CHART_BARS_STYLE))
273                .map(|x| x.unwrap_style())
274            {
275                widget = widget.bar_style(style);
276            }
277            if let Some(style) = self
278                .props
279                .get(Attribute::Custom(BAR_CHART_LABEL_STYLE))
280                .map(|x| x.unwrap_style())
281            {
282                widget = widget.label_style(style);
283            }
284            if let Some(style) = self
285                .props
286                .get(Attribute::Custom(BAR_CHART_VALUES_STYLE))
287                .map(|x| x.unwrap_style())
288            {
289                widget = widget.value_style(style);
290            }
291            // Render
292            render.render_widget(widget, area);
293        }
294    }
295
296    fn query(&self, attr: Attribute) -> Option<AttrValue> {
297        self.props.get(attr)
298    }
299
300    fn attr(&mut self, attr: Attribute, value: AttrValue) {
301        self.props.set(attr, value);
302    }
303
304    fn perform(&mut self, cmd: Cmd) -> CmdResult {
305        if !self.is_disabled() {
306            match cmd {
307                Cmd::Move(Direction::Left) => {
308                    self.states.move_cursor_left();
309                }
310                Cmd::Move(Direction::Right) => {
311                    self.states.move_cursor_right(self.data_len());
312                }
313                Cmd::GoTo(Position::Begin) => {
314                    self.states.reset_cursor();
315                }
316                Cmd::GoTo(Position::End) => {
317                    self.states.cursor_at_end(self.data_len());
318                }
319                _ => {}
320            }
321        }
322        CmdResult::None
323    }
324
325    fn state(&self) -> State {
326        State::None
327    }
328}
329
330#[cfg(test)]
331mod test {
332
333    use super::*;
334
335    use pretty_assertions::assert_eq;
336
337    #[test]
338    fn test_components_bar_chart_states() {
339        let mut states: BarChartStates = BarChartStates::default();
340        assert_eq!(states.cursor, 0);
341        // Incr
342        states.move_cursor_right(2);
343        assert_eq!(states.cursor, 1);
344        // At end
345        states.move_cursor_right(2);
346        assert_eq!(states.cursor, 1);
347        // Decr
348        states.move_cursor_left();
349        assert_eq!(states.cursor, 0);
350        // At begin
351        states.move_cursor_left();
352        assert_eq!(states.cursor, 0);
353        // Move at end
354        states.cursor_at_end(3);
355        assert_eq!(states.cursor, 2);
356        states.reset_cursor();
357        assert_eq!(states.cursor, 0);
358    }
359
360    #[test]
361    fn test_components_bar_chart() {
362        let mut component: BarChart = BarChart::default()
363            .disabled(false)
364            .title("my incomes", Alignment::Center)
365            .label_style(Style::default().fg(Color::Yellow))
366            .bar_style(Style::default().fg(Color::LightYellow))
367            .bar_gap(2)
368            .width(4)
369            .borders(Borders::default())
370            .max_bars(6)
371            .value_style(Style::default().fg(Color::LightBlue))
372            .data(&[
373                ("january", 250),
374                ("february", 300),
375                ("march", 275),
376                ("april", 312),
377                ("may", 420),
378                ("june", 170),
379                ("july", 220),
380                ("august", 160),
381                ("september", 180),
382                ("october", 470),
383                ("november", 380),
384                ("december", 820),
385            ]);
386        // Commands
387        assert_eq!(component.state(), State::None);
388        // -> Right
389        assert_eq!(
390            component.perform(Cmd::Move(Direction::Right)),
391            CmdResult::None
392        );
393        assert_eq!(component.states.cursor, 1);
394        // <- Left
395        assert_eq!(
396            component.perform(Cmd::Move(Direction::Left)),
397            CmdResult::None
398        );
399        assert_eq!(component.states.cursor, 0);
400        // End
401        assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
402        assert_eq!(component.states.cursor, 11);
403        // Home
404        assert_eq!(
405            component.perform(Cmd::GoTo(Position::Begin)),
406            CmdResult::None
407        );
408        assert_eq!(component.states.cursor, 0);
409    }
410}