• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

veeso / tui-realm-stdlib / 13283025409

12 Feb 2025 10:15AM UTC coverage: 68.369% (-4.7%) from 73.043%
13283025409

push

github

web-flow
Fix coverage tui (stdlib) (#29)

* chore(workflows/coverage): replace "actions-rs/toolchain" with "dtolnay/rust-toolchain"

* chore(workflows/ratatui): remove unknown option "override"

* chore(workflows/coverage): update "coverallsapp/github-action" to 2.x

* chore(workflows/coverage): replace coverage generation with "cargo-llvm-cov"

3279 of 4796 relevant lines covered (68.37%)

1.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.99
/src/components/chart.rs
1
//! ## Chart
2
//!
3
//! A component to plot one or more dataset in a cartesian coordinate system
4

5
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
6
use tuirealm::props::{
7
    Alignment, AttrValue, Attribute, Borders, Color, Dataset, PropPayload, PropValue, Props, Style,
8
};
9
use tuirealm::ratatui::text::Line;
10
use tuirealm::ratatui::{
11
    layout::Rect,
12
    text::Span,
13
    widgets::{Axis, Chart as TuiChart, Dataset as TuiDataset},
14
};
15
use tuirealm::{Frame, MockComponent, State};
16

17
// -- Props
18
use super::props::{
19
    CHART_X_BOUNDS, CHART_X_LABELS, CHART_X_STYLE, CHART_X_TITLE, CHART_Y_BOUNDS, CHART_Y_LABELS,
20
    CHART_Y_STYLE, CHART_Y_TITLE,
21
};
22

23
/// ### ChartStates
24
///
25
/// chart states
26
#[derive(Default)]
27
pub struct ChartStates {
28
    pub cursor: usize,
29
    pub data: Vec<Dataset>,
30
}
31

32
impl ChartStates {
33
    /// ### move_cursor_left
34
    ///
35
    /// Move cursor to the left
36
    pub fn move_cursor_left(&mut self) {
3✔
37
        if self.cursor > 0 {
3✔
38
            self.cursor -= 1;
2✔
39
        }
2✔
40
    }
3✔
41

42
    /// ### move_cursor_right
43
    ///
44
    /// Move cursor to the right
45
    pub fn move_cursor_right(&mut self, data_len: usize) {
3✔
46
        if data_len > 0 && self.cursor + 1 < data_len {
3✔
47
            self.cursor += 1;
2✔
48
        }
2✔
49
    }
3✔
50

51
    /// ### reset_cursor
52
    ///
53
    /// Reset cursor to 0
54
    pub fn reset_cursor(&mut self) {
8✔
55
        self.cursor = 0;
8✔
56
    }
8✔
57

58
    /// ### cursor_at_end
59
    ///
60
    /// Move cursor to the end of the chart
61
    pub fn cursor_at_end(&mut self, data_len: usize) {
3✔
62
        if data_len > 0 {
3✔
63
            self.cursor = data_len - 1;
3✔
64
        } else {
3✔
65
            self.cursor = 0;
×
66
        }
×
67
    }
3✔
68
}
69

70
// -- component
71

72
/// ### Chart
73
///
74
/// A component to display a chart on a cartesian coordinate system.
75
/// The chart can work both in "active" and "disabled" mode.
76
///
77
/// #### Disabled mode
78
///
79
/// When in disabled mode, the chart won't be interactive, so you won't be able to move through data using keys.
80
/// 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
81
///
82
/// #### Active mode
83
///
84
/// While in active mode (default) you can put as many entries as you wish. You can move with arrows and END/HOME keys
85
#[derive(Default)]
86
pub struct Chart {
87
    props: Props,
88
    pub states: ChartStates,
89
}
90

91
impl Chart {
92
    pub fn foreground(mut self, fg: Color) -> Self {
1✔
93
        self.props.set(Attribute::Foreground, AttrValue::Color(fg));
1✔
94
        self
1✔
95
    }
1✔
96

97
    pub fn background(mut self, bg: Color) -> Self {
1✔
98
        self.props.set(Attribute::Background, AttrValue::Color(bg));
1✔
99
        self
1✔
100
    }
1✔
101

102
    pub fn borders(mut self, b: Borders) -> Self {
1✔
103
        self.props.set(Attribute::Borders, AttrValue::Borders(b));
1✔
104
        self
1✔
105
    }
1✔
106

107
    pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
1✔
108
        self.props
1✔
109
            .set(Attribute::Title, AttrValue::Title((t.into(), a)));
1✔
110
        self
1✔
111
    }
1✔
112

113
    pub fn disabled(mut self, disabled: bool) -> Self {
1✔
114
        self.attr(Attribute::Disabled, AttrValue::Flag(disabled));
1✔
115
        self
1✔
116
    }
1✔
117

118
    pub fn inactive(mut self, s: Style) -> Self {
×
119
        self.props.set(Attribute::FocusStyle, AttrValue::Style(s));
×
120
        self
×
121
    }
×
122

123
    pub fn data(mut self, data: &[Dataset]) -> Self {
2✔
124
        self.props.set(
2✔
125
            Attribute::Dataset,
2✔
126
            AttrValue::Payload(PropPayload::Vec(
2✔
127
                data.iter().cloned().map(PropValue::Dataset).collect(),
2✔
128
            )),
2✔
129
        );
2✔
130
        self
2✔
131
    }
2✔
132

133
    pub fn x_bounds(mut self, bounds: (f64, f64)) -> Self {
1✔
134
        self.props.set(
1✔
135
            Attribute::Custom(CHART_X_BOUNDS),
1✔
136
            AttrValue::Payload(PropPayload::Tup2((
1✔
137
                PropValue::F64(bounds.0),
1✔
138
                PropValue::F64(bounds.1),
1✔
139
            ))),
1✔
140
        );
1✔
141
        self
1✔
142
    }
1✔
143

144
    pub fn y_bounds(mut self, bounds: (f64, f64)) -> Self {
1✔
145
        self.props.set(
1✔
146
            Attribute::Custom(CHART_Y_BOUNDS),
1✔
147
            AttrValue::Payload(PropPayload::Tup2((
1✔
148
                PropValue::F64(bounds.0),
1✔
149
                PropValue::F64(bounds.1),
1✔
150
            ))),
1✔
151
        );
1✔
152
        self
1✔
153
    }
1✔
154

155
    pub fn x_labels(mut self, labels: &[&str]) -> Self {
1✔
156
        self.attr(
1✔
157
            Attribute::Custom(CHART_X_LABELS),
1✔
158
            AttrValue::Payload(PropPayload::Vec(
1✔
159
                labels
1✔
160
                    .iter()
1✔
161
                    .map(|x| PropValue::Str(x.to_string()))
12✔
162
                    .collect(),
1✔
163
            )),
1✔
164
        );
1✔
165
        self
1✔
166
    }
1✔
167

168
    pub fn y_labels(mut self, labels: &[&str]) -> Self {
1✔
169
        self.attr(
1✔
170
            Attribute::Custom(CHART_Y_LABELS),
1✔
171
            AttrValue::Payload(PropPayload::Vec(
1✔
172
                labels
1✔
173
                    .iter()
1✔
174
                    .map(|x| PropValue::Str(x.to_string()))
9✔
175
                    .collect(),
1✔
176
            )),
1✔
177
        );
1✔
178
        self
1✔
179
    }
1✔
180

181
    pub fn x_style(mut self, s: Style) -> Self {
1✔
182
        self.attr(Attribute::Custom(CHART_X_STYLE), AttrValue::Style(s));
1✔
183
        self
1✔
184
    }
1✔
185

186
    pub fn y_style(mut self, s: Style) -> Self {
1✔
187
        self.attr(Attribute::Custom(CHART_Y_STYLE), AttrValue::Style(s));
1✔
188
        self
1✔
189
    }
1✔
190

191
    pub fn x_title<S: Into<String>>(mut self, t: S) -> Self {
1✔
192
        self.props.set(
1✔
193
            Attribute::Custom(CHART_X_TITLE),
1✔
194
            AttrValue::String(t.into()),
1✔
195
        );
1✔
196
        self
1✔
197
    }
1✔
198

199
    pub fn y_title<S: Into<String>>(mut self, t: S) -> Self {
1✔
200
        self.props.set(
1✔
201
            Attribute::Custom(CHART_Y_TITLE),
1✔
202
            AttrValue::String(t.into()),
1✔
203
        );
1✔
204
        self
1✔
205
    }
1✔
206

207
    fn is_disabled(&self) -> bool {
5✔
208
        self.props
5✔
209
            .get_or(Attribute::Disabled, AttrValue::Flag(false))
5✔
210
            .unwrap_flag()
5✔
211
    }
5✔
212

213
    /// ### max_dataset_len
214
    ///
215
    /// Get the maximum len among the datasets
216
    fn max_dataset_len(&self) -> usize {
4✔
217
        self.props
4✔
218
            .get(Attribute::Dataset)
4✔
219
            .map(|x| {
4✔
220
                x.unwrap_payload()
4✔
221
                    .unwrap_vec()
4✔
222
                    .iter()
4✔
223
                    .cloned()
4✔
224
                    .map(|x| x.unwrap_dataset().get_data().len())
6✔
225
                    .max()
4✔
226
            })
4✔
227
            .unwrap_or(None)
4✔
228
            .unwrap_or(0)
4✔
229
    }
4✔
230

231
    /// ### data
232
    ///
233
    /// Get data to be displayed, starting from provided index at `start` with a max length of `len`
234
    fn get_data(&mut self, start: usize, len: usize) -> Vec<TuiDataset> {
2✔
235
        self.states.data = self
2✔
236
            .props
2✔
237
            .get(Attribute::Dataset)
2✔
238
            .map(|x| {
2✔
239
                x.unwrap_payload()
2✔
240
                    .unwrap_vec()
2✔
241
                    .into_iter()
2✔
242
                    .map(|x| x.unwrap_dataset())
3✔
243
                    .collect()
2✔
244
            })
2✔
245
            .unwrap_or_default();
2✔
246
        self.states
2✔
247
            .data
2✔
248
            .iter()
2✔
249
            .map(|x| Self::get_tui_dataset(x, start, len))
3✔
250
            .collect()
2✔
251
    }
2✔
252
}
253

254
impl<'a> Chart {
255
    /// ### get_tui_dataset
256
    ///
257
    /// Create tui_dataset from dataset
258
    /// Only elements from `start` to `len` are preserved from dataset
259
    fn get_tui_dataset(dataset: &'a Dataset, start: usize, len: usize) -> TuiDataset<'a> {
3✔
260
        // Recalc len
3✔
261
        let points = dataset.get_data();
3✔
262
        let end: usize = match points.len() > start {
3✔
263
            true => std::cmp::min(len, points.len() - start),
3✔
264
            false => 0,
×
265
        };
266

267
        // Prepare data storage
268
        TuiDataset::default()
3✔
269
            .name(dataset.name.clone())
3✔
270
            .marker(dataset.marker)
3✔
271
            .graph_type(dataset.graph_type)
3✔
272
            .style(dataset.style)
3✔
273
            .data(&points[start..end])
3✔
274
    }
3✔
275
}
276

277
impl MockComponent for Chart {
278
    fn view(&mut self, render: &mut Frame, area: Rect) {
×
279
        if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
×
280
            let foreground = self
×
281
                .props
×
282
                .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
×
283
                .unwrap_color();
×
284
            let background = self
×
285
                .props
×
286
                .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
×
287
                .unwrap_color();
×
288
            let borders = self
×
289
                .props
×
290
                .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
×
291
                .unwrap_borders();
×
292
            let title = self.props.get(Attribute::Title).map(|x| x.unwrap_title());
×
293
            let focus = self
×
294
                .props
×
295
                .get_or(Attribute::Focus, AttrValue::Flag(false))
×
296
                .unwrap_flag();
×
297
            let inactive_style = self
×
298
                .props
×
299
                .get(Attribute::FocusStyle)
×
300
                .map(|x| x.unwrap_style());
×
301
            let active: bool = match self.is_disabled() {
×
302
                true => true,
×
303
                false => focus,
×
304
            };
305
            let div = crate::utils::get_block(borders, title, active, inactive_style);
×
306
            // Create widget
×
307
            // -- x axis
×
308
            let mut x_axis: Axis = Axis::default();
×
309
            if let Some((PropValue::F64(floor), PropValue::F64(ceil))) = self
×
310
                .props
×
311
                .get(Attribute::Custom(CHART_X_BOUNDS))
×
312
                .map(|x| x.unwrap_payload().unwrap_tup2())
×
313
            {
×
314
                let why_using_vecs_when_you_can_use_useless_arrays: [f64; 2] = [floor, ceil];
×
315
                x_axis = x_axis.bounds(why_using_vecs_when_you_can_use_useless_arrays);
×
316
            }
×
317
            if let Some(PropPayload::Vec(labels)) = self
×
318
                .props
×
319
                .get(Attribute::Custom(CHART_X_LABELS))
×
320
                .map(|x| x.unwrap_payload())
×
321
            {
×
322
                x_axis = x_axis.labels(labels.iter().cloned().map(|x| Line::from(x.unwrap_str())));
×
323
            }
×
324
            if let Some(s) = self
×
325
                .props
×
326
                .get(Attribute::Custom(CHART_X_STYLE))
×
327
                .map(|x| x.unwrap_style())
×
328
            {
×
329
                x_axis = x_axis.style(s);
×
330
            }
×
331
            if let Some(title) = self
×
332
                .props
×
333
                .get(Attribute::Custom(CHART_X_TITLE))
×
334
                .map(|x| x.unwrap_string())
×
335
            {
×
336
                x_axis = x_axis.title(Span::styled(
×
337
                    title,
×
338
                    Style::default().fg(foreground).bg(background),
×
339
                ));
×
340
            }
×
341
            // -- y axis
342
            let mut y_axis: Axis = Axis::default();
×
343
            if let Some((PropValue::F64(floor), PropValue::F64(ceil))) = self
×
344
                .props
×
345
                .get(Attribute::Custom(CHART_Y_BOUNDS))
×
346
                .map(|x| x.unwrap_payload().unwrap_tup2())
×
347
            {
×
348
                let why_using_vecs_when_you_can_use_useless_arrays: [f64; 2] = [floor, ceil];
×
349
                y_axis = y_axis.bounds(why_using_vecs_when_you_can_use_useless_arrays);
×
350
            }
×
351
            if let Some(PropPayload::Vec(labels)) = self
×
352
                .props
×
353
                .get(Attribute::Custom(CHART_Y_LABELS))
×
354
                .map(|x| x.unwrap_payload())
×
355
            {
×
356
                y_axis = y_axis.labels(labels.iter().cloned().map(|x| Line::from(x.unwrap_str())));
×
357
            }
×
358
            if let Some(s) = self
×
359
                .props
×
360
                .get(Attribute::Custom(CHART_Y_STYLE))
×
361
                .map(|x| x.unwrap_style())
×
362
            {
×
363
                y_axis = y_axis.style(s);
×
364
            }
×
365
            if let Some(title) = self
×
366
                .props
×
367
                .get(Attribute::Custom(CHART_Y_TITLE))
×
368
                .map(|x| x.unwrap_string())
×
369
            {
×
370
                y_axis = y_axis.title(Span::styled(
×
371
                    title,
×
372
                    Style::default().fg(foreground).bg(background),
×
373
                ));
×
374
            }
×
375
            // Get data
376
            let data: Vec<TuiDataset> = self.get_data(self.states.cursor, area.width as usize);
×
377
            // Build widget
×
378
            let widget: TuiChart = TuiChart::new(data).block(div).x_axis(x_axis).y_axis(y_axis);
×
379
            // Render
×
380
            render.render_widget(widget, area);
×
381
        }
×
382
    }
×
383

384
    fn query(&self, attr: Attribute) -> Option<AttrValue> {
×
385
        self.props.get(attr)
×
386
    }
×
387

388
    fn attr(&mut self, attr: Attribute, value: AttrValue) {
6✔
389
        self.props.set(attr, value);
6✔
390
        self.states.reset_cursor();
6✔
391
    }
6✔
392

393
    fn perform(&mut self, cmd: Cmd) -> CmdResult {
4✔
394
        if !self.is_disabled() {
4✔
395
            match cmd {
2✔
396
                Cmd::Move(Direction::Left) => {
1✔
397
                    self.states.move_cursor_left();
1✔
398
                }
1✔
399
                Cmd::Move(Direction::Right) => {
1✔
400
                    self.states.move_cursor_right(self.max_dataset_len());
1✔
401
                }
1✔
402
                Cmd::GoTo(Position::Begin) => {
1✔
403
                    self.states.reset_cursor();
1✔
404
                }
1✔
405
                Cmd::GoTo(Position::End) => {
1✔
406
                    self.states.cursor_at_end(self.max_dataset_len());
1✔
407
                }
1✔
408
                _ => {}
×
409
            }
410
        }
×
411
        CmdResult::None
4✔
412
    }
4✔
413

414
    fn state(&self) -> State {
1✔
415
        State::None
1✔
416
    }
1✔
417
}
418

419
#[cfg(test)]
420
mod test {
421

422
    use super::*;
423

424
    use pretty_assertions::assert_eq;
425
    use tuirealm::ratatui::{symbols::Marker, widgets::GraphType};
426

427
    #[test]
428
    fn test_components_chart_states() {
1✔
429
        let mut states: ChartStates = ChartStates::default();
1✔
430
        assert_eq!(states.cursor, 0);
1✔
431
        // Incr
432
        states.move_cursor_right(2);
1✔
433
        assert_eq!(states.cursor, 1);
1✔
434
        // At end
435
        states.move_cursor_right(2);
1✔
436
        assert_eq!(states.cursor, 1);
1✔
437
        // Decr
438
        states.move_cursor_left();
1✔
439
        assert_eq!(states.cursor, 0);
1✔
440
        // At begin
441
        states.move_cursor_left();
1✔
442
        assert_eq!(states.cursor, 0);
1✔
443
        // Move at end
444
        states.cursor_at_end(3);
1✔
445
        assert_eq!(states.cursor, 2);
1✔
446
        states.reset_cursor();
1✔
447
        assert_eq!(states.cursor, 0);
1✔
448
    }
1✔
449

450
    #[test]
451
    fn test_components_chart() {
1✔
452
        let mut component: Chart = Chart::default()
1✔
453
            .disabled(false)
1✔
454
            .background(Color::Reset)
1✔
455
            .foreground(Color::Reset)
1✔
456
            .borders(Borders::default())
1✔
457
            .title("average temperatures in Udine", Alignment::Center)
1✔
458
            .x_bounds((0.0, 11.0))
1✔
459
            .x_labels(&[
1✔
460
                "january",
1✔
461
                "february",
1✔
462
                "march",
1✔
463
                "april",
1✔
464
                "may",
1✔
465
                "june",
1✔
466
                "july",
1✔
467
                "august",
1✔
468
                "september",
1✔
469
                "october",
1✔
470
                "november",
1✔
471
                "december",
1✔
472
            ])
1✔
473
            .x_style(Style::default().fg(Color::LightBlue))
1✔
474
            .x_title("Temperature (°C)")
1✔
475
            .y_bounds((-5.0, 35.0))
1✔
476
            .y_labels(&["-5", "0", "5", "10", "15", "20", "25", "30", "35"])
1✔
477
            .y_style(Style::default().fg(Color::LightYellow))
1✔
478
            .y_title("Month")
1✔
479
            .data(&[
1✔
480
                Dataset::default()
1✔
481
                    .name("Minimum")
1✔
482
                    .graph_type(GraphType::Scatter)
1✔
483
                    .marker(Marker::Braille)
1✔
484
                    .style(Style::default().fg(Color::Cyan))
1✔
485
                    .data(vec![
1✔
486
                        (0.0, -1.0),
1✔
487
                        (1.0, 1.0),
1✔
488
                        (2.0, 3.0),
1✔
489
                        (3.0, 7.0),
1✔
490
                        (4.0, 11.0),
1✔
491
                        (5.0, 15.0),
1✔
492
                        (6.0, 17.0),
1✔
493
                        (7.0, 17.0),
1✔
494
                        (8.0, 13.0),
1✔
495
                        (9.0, 9.0),
1✔
496
                        (10.0, 4.0),
1✔
497
                        (11.0, 0.0),
1✔
498
                    ]),
1✔
499
                Dataset::default()
1✔
500
                    .name("Maximum")
1✔
501
                    .graph_type(GraphType::Line)
1✔
502
                    .marker(Marker::Dot)
1✔
503
                    .style(Style::default().fg(Color::LightRed))
1✔
504
                    .data(vec![
1✔
505
                        (0.0, 7.0),
1✔
506
                        (1.0, 9.0),
1✔
507
                        (2.0, 13.0),
1✔
508
                        (3.0, 17.0),
1✔
509
                        (4.0, 22.0),
1✔
510
                        (5.0, 25.0),
1✔
511
                        (6.0, 28.0),
1✔
512
                        (7.0, 28.0),
1✔
513
                        (8.0, 24.0),
1✔
514
                        (9.0, 19.0),
1✔
515
                        (10.0, 13.0),
1✔
516
                        (11.0, 8.0),
1✔
517
                    ]),
1✔
518
            ]);
1✔
519
        // Commands
1✔
520
        assert_eq!(component.state(), State::None);
1✔
521
        // -> Right
522
        assert_eq!(
1✔
523
            component.perform(Cmd::Move(Direction::Right)),
1✔
524
            CmdResult::None
1✔
525
        );
1✔
526
        assert_eq!(component.states.cursor, 1);
1✔
527
        // <- Left
528
        assert_eq!(
1✔
529
            component.perform(Cmd::Move(Direction::Left)),
1✔
530
            CmdResult::None
1✔
531
        );
1✔
532
        assert_eq!(component.states.cursor, 0);
1✔
533
        // End
534
        assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
1✔
535
        assert_eq!(component.states.cursor, 11);
1✔
536
        // Home
537
        assert_eq!(
1✔
538
            component.perform(Cmd::GoTo(Position::Begin)),
1✔
539
            CmdResult::None
1✔
540
        );
1✔
541
        assert_eq!(component.states.cursor, 0);
1✔
542
        // component funcs
543
        assert_eq!(component.max_dataset_len(), 12);
1✔
544
        assert_eq!(component.is_disabled(), false);
1✔
545
        assert_eq!(component.get_data(2, 4).len(), 2);
1✔
546

547
        let mut comp = Chart::default().data(&[Dataset::default()
1✔
548
            .name("Maximum")
1✔
549
            .graph_type(GraphType::Line)
1✔
550
            .marker(Marker::Dot)
1✔
551
            .style(Style::default().fg(Color::LightRed))
1✔
552
            .data(vec![(0.0, 7.0)])]);
1✔
553
        assert!(comp.get_data(0, 1).len() > 0);
1✔
554

555
        // Update and test empty data
556
        component.states.cursor_at_end(12);
1✔
557
        component.attr(
1✔
558
            Attribute::Dataset,
1✔
559
            AttrValue::Payload(PropPayload::Vec(vec![])),
1✔
560
        );
1✔
561
        assert_eq!(component.max_dataset_len(), 0);
1✔
562
        // Cursor is reset
563
        assert_eq!(component.states.cursor, 0);
1✔
564
    }
1✔
565
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc