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 normal_style = Style::default().bg(background).fg(foreground);
248            let div = crate::utils::get_block(borders, title, active, inactive_style);
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 = TuiBarChart::default()
259                .style(normal_style)
260                .block(div)
261                .data(data_ref.as_slice());
262            if let Some(gap) = self
263                .props
264                .get(Attribute::Custom(BAR_CHART_BARS_GAP))
265                .map(|x| x.unwrap_size())
266            {
267                widget = widget.bar_gap(gap);
268            }
269            if let Some(width) = self.props.get(Attribute::Width).map(|x| x.unwrap_size()) {
270                widget = widget.bar_width(width);
271            }
272            if let Some(style) = self
273                .props
274                .get(Attribute::Custom(BAR_CHART_BARS_STYLE))
275                .map(|x| x.unwrap_style())
276            {
277                widget = widget.bar_style(style);
278            }
279            if let Some(style) = self
280                .props
281                .get(Attribute::Custom(BAR_CHART_LABEL_STYLE))
282                .map(|x| x.unwrap_style())
283            {
284                widget = widget.label_style(style);
285            }
286            if let Some(style) = self
287                .props
288                .get(Attribute::Custom(BAR_CHART_VALUES_STYLE))
289                .map(|x| x.unwrap_style())
290            {
291                widget = widget.value_style(style);
292            }
293            // Render
294            render.render_widget(widget, area);
295        }
296    }
297
298    fn query(&self, attr: Attribute) -> Option<AttrValue> {
299        self.props.get(attr)
300    }
301
302    fn attr(&mut self, attr: Attribute, value: AttrValue) {
303        self.props.set(attr, value);
304    }
305
306    fn perform(&mut self, cmd: Cmd) -> CmdResult {
307        if !self.is_disabled() {
308            match cmd {
309                Cmd::Move(Direction::Left) => {
310                    self.states.move_cursor_left();
311                }
312                Cmd::Move(Direction::Right) => {
313                    self.states.move_cursor_right(self.data_len());
314                }
315                Cmd::GoTo(Position::Begin) => {
316                    self.states.reset_cursor();
317                }
318                Cmd::GoTo(Position::End) => {
319                    self.states.cursor_at_end(self.data_len());
320                }
321                _ => {}
322            }
323        }
324        CmdResult::None
325    }
326
327    fn state(&self) -> State {
328        State::None
329    }
330}
331
332#[cfg(test)]
333mod test {
334
335    use super::*;
336
337    use pretty_assertions::assert_eq;
338
339    #[test]
340    fn test_components_bar_chart_states() {
341        let mut states: BarChartStates = BarChartStates::default();
342        assert_eq!(states.cursor, 0);
343        // Incr
344        states.move_cursor_right(2);
345        assert_eq!(states.cursor, 1);
346        // At end
347        states.move_cursor_right(2);
348        assert_eq!(states.cursor, 1);
349        // Decr
350        states.move_cursor_left();
351        assert_eq!(states.cursor, 0);
352        // At begin
353        states.move_cursor_left();
354        assert_eq!(states.cursor, 0);
355        // Move at end
356        states.cursor_at_end(3);
357        assert_eq!(states.cursor, 2);
358        states.reset_cursor();
359        assert_eq!(states.cursor, 0);
360    }
361
362    #[test]
363    fn test_components_bar_chart() {
364        let mut component: BarChart = BarChart::default()
365            .disabled(false)
366            .title("my incomes", Alignment::Center)
367            .label_style(Style::default().fg(Color::Yellow))
368            .bar_style(Style::default().fg(Color::LightYellow))
369            .bar_gap(2)
370            .width(4)
371            .borders(Borders::default())
372            .max_bars(6)
373            .value_style(Style::default().fg(Color::LightBlue))
374            .data(&[
375                ("january", 250),
376                ("february", 300),
377                ("march", 275),
378                ("april", 312),
379                ("may", 420),
380                ("june", 170),
381                ("july", 220),
382                ("august", 160),
383                ("september", 180),
384                ("october", 470),
385                ("november", 380),
386                ("december", 820),
387            ]);
388        // Commands
389        assert_eq!(component.state(), State::None);
390        // -> Right
391        assert_eq!(
392            component.perform(Cmd::Move(Direction::Right)),
393            CmdResult::None
394        );
395        assert_eq!(component.states.cursor, 1);
396        // <- Left
397        assert_eq!(
398            component.perform(Cmd::Move(Direction::Left)),
399            CmdResult::None
400        );
401        assert_eq!(component.states.cursor, 0);
402        // End
403        assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
404        assert_eq!(component.states.cursor, 11);
405        // Home
406        assert_eq!(
407            component.perform(Cmd::GoTo(Position::Begin)),
408            CmdResult::None
409        );
410        assert_eq!(component.states.cursor, 0);
411    }
412}