Skip to main content

tui_realm_stdlib/components/
bar_chart.rs

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