1use tuirealm::command::{Cmd, CmdResult, Direction, Position};
6use tuirealm::props::{
7 Alignment, AttrValue, Attribute, Borders, Color, Dataset, PropPayload, PropValue, Props, Style,
8};
9use tuirealm::ratatui::text::Line;
10use tuirealm::ratatui::{
11 layout::Rect,
12 text::Span,
13 widgets::{Axis, Chart as TuiChart, Dataset as TuiDataset},
14};
15use tuirealm::{Frame, MockComponent, State};
16
17use 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#[derive(Default)]
27pub struct ChartStates {
28 pub cursor: usize,
29 pub data: Vec<Dataset>,
30}
31
32impl ChartStates {
33 pub fn move_cursor_left(&mut self) {
37 if self.cursor > 0 {
38 self.cursor -= 1;
39 }
40 }
41
42 pub fn move_cursor_right(&mut self, data_len: usize) {
46 if data_len > 0 && self.cursor + 1 < data_len {
47 self.cursor += 1;
48 }
49 }
50
51 pub fn reset_cursor(&mut self) {
55 self.cursor = 0;
56 }
57
58 pub fn cursor_at_end(&mut self, data_len: usize) {
62 if data_len > 0 {
63 self.cursor = data_len - 1;
64 } else {
65 self.cursor = 0;
66 }
67 }
68}
69
70#[derive(Default)]
86pub struct Chart {
87 props: Props,
88 pub states: ChartStates,
89}
90
91impl Chart {
92 pub fn foreground(mut self, fg: Color) -> Self {
93 self.props.set(Attribute::Foreground, AttrValue::Color(fg));
94 self
95 }
96
97 pub fn background(mut self, bg: Color) -> Self {
98 self.props.set(Attribute::Background, AttrValue::Color(bg));
99 self
100 }
101
102 pub fn borders(mut self, b: Borders) -> Self {
103 self.props.set(Attribute::Borders, AttrValue::Borders(b));
104 self
105 }
106
107 pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
108 self.props
109 .set(Attribute::Title, AttrValue::Title((t.into(), a)));
110 self
111 }
112
113 pub fn disabled(mut self, disabled: bool) -> Self {
114 self.attr(Attribute::Disabled, AttrValue::Flag(disabled));
115 self
116 }
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 {
124 self.props.set(
125 Attribute::Dataset,
126 AttrValue::Payload(PropPayload::Vec(
127 data.iter().cloned().map(PropValue::Dataset).collect(),
128 )),
129 );
130 self
131 }
132
133 pub fn x_bounds(mut self, bounds: (f64, f64)) -> Self {
134 self.props.set(
135 Attribute::Custom(CHART_X_BOUNDS),
136 AttrValue::Payload(PropPayload::Tup2((
137 PropValue::F64(bounds.0),
138 PropValue::F64(bounds.1),
139 ))),
140 );
141 self
142 }
143
144 pub fn y_bounds(mut self, bounds: (f64, f64)) -> Self {
145 self.props.set(
146 Attribute::Custom(CHART_Y_BOUNDS),
147 AttrValue::Payload(PropPayload::Tup2((
148 PropValue::F64(bounds.0),
149 PropValue::F64(bounds.1),
150 ))),
151 );
152 self
153 }
154
155 pub fn x_labels(mut self, labels: &[&str]) -> Self {
156 self.attr(
157 Attribute::Custom(CHART_X_LABELS),
158 AttrValue::Payload(PropPayload::Vec(
159 labels
160 .iter()
161 .map(|x| PropValue::Str(x.to_string()))
162 .collect(),
163 )),
164 );
165 self
166 }
167
168 pub fn y_labels(mut self, labels: &[&str]) -> Self {
169 self.attr(
170 Attribute::Custom(CHART_Y_LABELS),
171 AttrValue::Payload(PropPayload::Vec(
172 labels
173 .iter()
174 .map(|x| PropValue::Str(x.to_string()))
175 .collect(),
176 )),
177 );
178 self
179 }
180
181 pub fn x_style(mut self, s: Style) -> Self {
182 self.attr(Attribute::Custom(CHART_X_STYLE), AttrValue::Style(s));
183 self
184 }
185
186 pub fn y_style(mut self, s: Style) -> Self {
187 self.attr(Attribute::Custom(CHART_Y_STYLE), AttrValue::Style(s));
188 self
189 }
190
191 pub fn x_title<S: Into<String>>(mut self, t: S) -> Self {
192 self.props.set(
193 Attribute::Custom(CHART_X_TITLE),
194 AttrValue::String(t.into()),
195 );
196 self
197 }
198
199 pub fn y_title<S: Into<String>>(mut self, t: S) -> Self {
200 self.props.set(
201 Attribute::Custom(CHART_Y_TITLE),
202 AttrValue::String(t.into()),
203 );
204 self
205 }
206
207 fn is_disabled(&self) -> bool {
208 self.props
209 .get_or(Attribute::Disabled, AttrValue::Flag(false))
210 .unwrap_flag()
211 }
212
213 fn max_dataset_len(&self) -> usize {
217 self.props
218 .get(Attribute::Dataset)
219 .map(|x| {
220 x.unwrap_payload()
221 .unwrap_vec()
222 .iter()
223 .cloned()
224 .map(|x| x.unwrap_dataset().get_data().len())
225 .max()
226 })
227 .unwrap_or(None)
228 .unwrap_or(0)
229 }
230
231 fn get_data(&mut self, start: usize, len: usize) -> Vec<TuiDataset> {
235 self.states.data = self
236 .props
237 .get(Attribute::Dataset)
238 .map(|x| {
239 x.unwrap_payload()
240 .unwrap_vec()
241 .into_iter()
242 .map(|x| x.unwrap_dataset())
243 .collect()
244 })
245 .unwrap_or_default();
246 self.states
247 .data
248 .iter()
249 .map(|x| Self::get_tui_dataset(x, start, len))
250 .collect()
251 }
252}
253
254impl<'a> Chart {
255 fn get_tui_dataset(dataset: &'a Dataset, start: usize, len: usize) -> TuiDataset<'a> {
260 let points = dataset.get_data();
262 let end: usize = match points.len() > start {
263 true => std::cmp::min(len, points.len() - start),
264 false => 0,
265 };
266
267 TuiDataset::default()
269 .name(dataset.name.clone())
270 .marker(dataset.marker)
271 .graph_type(dataset.graph_type)
272 .style(dataset.style)
273 .data(&points[start..end])
274 }
275}
276
277impl 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 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 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 let data: Vec<TuiDataset> = self.get_data(self.states.cursor, area.width as usize);
377 let widget: TuiChart = TuiChart::new(data).block(div).x_axis(x_axis).y_axis(y_axis);
379 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) {
389 self.props.set(attr, value);
390 self.states.reset_cursor();
391 }
392
393 fn perform(&mut self, cmd: Cmd) -> CmdResult {
394 if !self.is_disabled() {
395 match cmd {
396 Cmd::Move(Direction::Left) => {
397 self.states.move_cursor_left();
398 }
399 Cmd::Move(Direction::Right) => {
400 self.states.move_cursor_right(self.max_dataset_len());
401 }
402 Cmd::GoTo(Position::Begin) => {
403 self.states.reset_cursor();
404 }
405 Cmd::GoTo(Position::End) => {
406 self.states.cursor_at_end(self.max_dataset_len());
407 }
408 _ => {}
409 }
410 }
411 CmdResult::None
412 }
413
414 fn state(&self) -> State {
415 State::None
416 }
417}
418
419#[cfg(test)]
420mod 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() {
429 let mut states: ChartStates = ChartStates::default();
430 assert_eq!(states.cursor, 0);
431 states.move_cursor_right(2);
433 assert_eq!(states.cursor, 1);
434 states.move_cursor_right(2);
436 assert_eq!(states.cursor, 1);
437 states.move_cursor_left();
439 assert_eq!(states.cursor, 0);
440 states.move_cursor_left();
442 assert_eq!(states.cursor, 0);
443 states.cursor_at_end(3);
445 assert_eq!(states.cursor, 2);
446 states.reset_cursor();
447 assert_eq!(states.cursor, 0);
448 }
449
450 #[test]
451 fn test_components_chart() {
452 let mut component: Chart = Chart::default()
453 .disabled(false)
454 .background(Color::Reset)
455 .foreground(Color::Reset)
456 .borders(Borders::default())
457 .title("average temperatures in Udine", Alignment::Center)
458 .x_bounds((0.0, 11.0))
459 .x_labels(&[
460 "january",
461 "february",
462 "march",
463 "april",
464 "may",
465 "june",
466 "july",
467 "august",
468 "september",
469 "october",
470 "november",
471 "december",
472 ])
473 .x_style(Style::default().fg(Color::LightBlue))
474 .x_title("Temperature (°C)")
475 .y_bounds((-5.0, 35.0))
476 .y_labels(&["-5", "0", "5", "10", "15", "20", "25", "30", "35"])
477 .y_style(Style::default().fg(Color::LightYellow))
478 .y_title("Month")
479 .data(&[
480 Dataset::default()
481 .name("Minimum")
482 .graph_type(GraphType::Scatter)
483 .marker(Marker::Braille)
484 .style(Style::default().fg(Color::Cyan))
485 .data(vec![
486 (0.0, -1.0),
487 (1.0, 1.0),
488 (2.0, 3.0),
489 (3.0, 7.0),
490 (4.0, 11.0),
491 (5.0, 15.0),
492 (6.0, 17.0),
493 (7.0, 17.0),
494 (8.0, 13.0),
495 (9.0, 9.0),
496 (10.0, 4.0),
497 (11.0, 0.0),
498 ]),
499 Dataset::default()
500 .name("Maximum")
501 .graph_type(GraphType::Line)
502 .marker(Marker::Dot)
503 .style(Style::default().fg(Color::LightRed))
504 .data(vec![
505 (0.0, 7.0),
506 (1.0, 9.0),
507 (2.0, 13.0),
508 (3.0, 17.0),
509 (4.0, 22.0),
510 (5.0, 25.0),
511 (6.0, 28.0),
512 (7.0, 28.0),
513 (8.0, 24.0),
514 (9.0, 19.0),
515 (10.0, 13.0),
516 (11.0, 8.0),
517 ]),
518 ]);
519 assert_eq!(component.state(), State::None);
521 assert_eq!(
523 component.perform(Cmd::Move(Direction::Right)),
524 CmdResult::None
525 );
526 assert_eq!(component.states.cursor, 1);
527 assert_eq!(
529 component.perform(Cmd::Move(Direction::Left)),
530 CmdResult::None
531 );
532 assert_eq!(component.states.cursor, 0);
533 assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
535 assert_eq!(component.states.cursor, 11);
536 assert_eq!(
538 component.perform(Cmd::GoTo(Position::Begin)),
539 CmdResult::None
540 );
541 assert_eq!(component.states.cursor, 0);
542 assert_eq!(component.max_dataset_len(), 12);
544 assert_eq!(component.is_disabled(), false);
545 assert_eq!(component.get_data(2, 4).len(), 2);
546
547 let mut comp = Chart::default().data(&[Dataset::default()
548 .name("Maximum")
549 .graph_type(GraphType::Line)
550 .marker(Marker::Dot)
551 .style(Style::default().fg(Color::LightRed))
552 .data(vec![(0.0, 7.0)])]);
553 assert!(comp.get_data(0, 1).len() > 0);
554
555 component.states.cursor_at_end(12);
557 component.attr(
558 Attribute::Dataset,
559 AttrValue::Payload(PropPayload::Vec(vec![])),
560 );
561 assert_eq!(component.max_dataset_len(), 0);
562 assert_eq!(component.states.cursor, 0);
564 }
565}