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

veeso / tui-realm-stdlib / 20395971316

20 Dec 2025 02:51PM UTC coverage: 68.439% (+0.5%) from 67.987%
20395971316

push

github

web-flow
fix: Fix Styling inconsistencies (#44)

7 of 84 new or added lines in 10 files covered. (8.33%)

6 existing lines in 5 files now uncovered.

3038 of 4439 relevant lines covered (68.44%)

1.84 hits per line

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

72.52
/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
#[must_use]
87
pub struct Chart {
88
    props: Props,
89
    pub states: ChartStates,
90
}
91

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

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

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

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

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

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

124
    pub fn data(mut self, data: impl IntoIterator<Item = Dataset>) -> Self {
2✔
125
        self.props.set(
2✔
126
            Attribute::Dataset,
2✔
127
            AttrValue::Payload(PropPayload::Vec(
2✔
128
                data.into_iter().map(PropValue::Dataset).collect(),
2✔
129
            )),
2✔
130
        );
131
        self
2✔
132
    }
2✔
133

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

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

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

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

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

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

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

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

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

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

231
    /// ### data
232
    ///
233
    /// Get data to be displayed, starting from provided index at `start`
234
    fn get_data(&mut self, start: 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))
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 the end
259
    fn get_tui_dataset(dataset: &'a Dataset, start: usize) -> TuiDataset<'a> {
3✔
260
        let points = dataset.get_data();
3✔
261

262
        // Prepare data storage
263
        TuiDataset::default()
3✔
264
            .name(dataset.name.clone())
3✔
265
            .marker(dataset.marker)
3✔
266
            .graph_type(dataset.graph_type)
3✔
267
            .style(dataset.style)
3✔
268
            .data(&points[start..])
3✔
269
    }
3✔
270
}
271

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

380
    fn query(&self, attr: Attribute) -> Option<AttrValue> {
×
381
        self.props.get(attr)
×
382
    }
×
383

384
    fn attr(&mut self, attr: Attribute, value: AttrValue) {
6✔
385
        self.props.set(attr, value);
6✔
386
        self.states.reset_cursor();
6✔
387
    }
6✔
388

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

410
    fn state(&self) -> State {
1✔
411
        State::None
1✔
412
    }
1✔
413
}
414

415
#[cfg(test)]
416
mod test {
417

418
    use super::*;
419

420
    use pretty_assertions::assert_eq;
421
    use tuirealm::ratatui::{symbols::Marker, widgets::GraphType};
422

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

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

543
        let mut comp = Chart::default().data([Dataset::default()
1✔
544
            .name("Maximum")
1✔
545
            .graph_type(GraphType::Line)
1✔
546
            .marker(Marker::Dot)
1✔
547
            .style(Style::default().fg(Color::LightRed))
1✔
548
            .data(vec![(0.0, 7.0)])]);
1✔
549
        assert!(!comp.get_data(0).is_empty());
1✔
550

551
        // Update and test empty data
552
        component.states.cursor_at_end(12);
1✔
553
        component.attr(
1✔
554
            Attribute::Dataset,
1✔
555
            AttrValue::Payload(PropPayload::Vec(vec![])),
1✔
556
        );
557
        assert_eq!(component.max_dataset_len(), 0);
1✔
558
        // Cursor is reset
559
        assert_eq!(component.states.cursor, 0);
1✔
560
    }
1✔
561
}
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