1use std::any::Any;
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::text::{Line, Span};
14use tuirealm::ratatui::widgets::{Axis, Chart as TuiChart, Dataset as TuiDataset};
15use tuirealm::state::State;
16
17use super::dataset::ChartDataset;
19use crate::components::props::{
20 CHART_X_BOUNDS, CHART_X_LABELS, CHART_X_STYLE, CHART_X_TITLE, CHART_Y_BOUNDS, CHART_Y_LABELS,
21 CHART_Y_STYLE, CHART_Y_TITLE,
22};
23use crate::prop_ext::CommonProps;
24
25#[derive(Default)]
27pub struct ChartStates {
28 pub cursor: usize,
29 pub data: Vec<ChartDataset>,
30}
31
32impl ChartStates {
33 pub fn move_cursor_left(&mut self) {
35 if self.cursor > 0 {
36 self.cursor -= 1;
37 }
38 }
39
40 pub fn move_cursor_right(&mut self, data_len: usize) {
42 if data_len > 0 && self.cursor + 1 < data_len {
43 self.cursor += 1;
44 }
45 }
46
47 pub fn reset_cursor(&mut self) {
49 self.cursor = 0;
50 }
51
52 pub fn cursor_at_end(&mut self, data_len: usize) {
54 if data_len > 0 {
55 self.cursor = data_len - 1;
56 } else {
57 self.cursor = 0;
58 }
59 }
60}
61
62#[derive(Default)]
76#[must_use]
77pub struct Chart {
78 common: CommonProps,
79 props: Props,
80 pub states: ChartStates,
81}
82
83impl Chart {
84 pub fn foreground(mut self, fg: Color) -> Self {
86 self.props.set(Attribute::Foreground, AttrValue::Color(fg));
87 self
88 }
89
90 pub fn background(mut self, bg: Color) -> Self {
92 self.props.set(Attribute::Background, AttrValue::Color(bg));
93 self
94 }
95
96 pub fn modifiers(mut self, m: TextModifiers) -> Self {
98 self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
99 self
100 }
101
102 pub fn style(mut self, style: Style) -> Self {
106 self.attr(Attribute::Style, AttrValue::Style(style));
107 self
108 }
109
110 pub fn borders(mut self, b: Borders) -> Self {
112 self.props.set(Attribute::Borders, AttrValue::Borders(b));
113 self
114 }
115
116 pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
118 self.attr(Attribute::Title, AttrValue::Title(title.into()));
119 self
120 }
121
122 pub fn disabled(mut self, disabled: bool) -> Self {
124 self.attr(Attribute::Disabled, AttrValue::Flag(disabled));
125 self
126 }
127
128 pub fn inactive(mut self, s: Style) -> Self {
130 self.props
131 .set(Attribute::UnfocusedBorderStyle, AttrValue::Style(s));
132 self
133 }
134
135 pub fn data(mut self, data: impl IntoIterator<Item = ChartDataset>) -> Self {
137 self.set_data(data.into_iter().collect());
138 self
139 }
140
141 pub fn x_bounds(mut self, bounds: (f64, f64)) -> Self {
143 self.props.set(
144 Attribute::Custom(CHART_X_BOUNDS),
145 AttrValue::Payload(PropPayload::Pair((
146 PropValue::F64(bounds.0),
147 PropValue::F64(bounds.1),
148 ))),
149 );
150 self
151 }
152
153 pub fn y_bounds(mut self, bounds: (f64, f64)) -> Self {
155 self.props.set(
156 Attribute::Custom(CHART_Y_BOUNDS),
157 AttrValue::Payload(PropPayload::Pair((
158 PropValue::F64(bounds.0),
159 PropValue::F64(bounds.1),
160 ))),
161 );
162 self
163 }
164
165 pub fn x_labels(mut self, labels: &[&str]) -> Self {
167 self.attr(
168 Attribute::Custom(CHART_X_LABELS),
169 AttrValue::Payload(PropPayload::Vec(
170 labels
171 .iter()
172 .map(|x| PropValue::Str((*x).to_string()))
173 .collect(),
174 )),
175 );
176 self
177 }
178
179 pub fn y_labels(mut self, labels: &[&str]) -> Self {
181 self.attr(
182 Attribute::Custom(CHART_Y_LABELS),
183 AttrValue::Payload(PropPayload::Vec(
184 labels
185 .iter()
186 .map(|x| PropValue::Str((*x).to_string()))
187 .collect(),
188 )),
189 );
190 self
191 }
192
193 pub fn x_style(mut self, s: Style) -> Self {
195 self.attr(Attribute::Custom(CHART_X_STYLE), AttrValue::Style(s));
196 self
197 }
198
199 pub fn y_style(mut self, s: Style) -> Self {
201 self.attr(Attribute::Custom(CHART_Y_STYLE), AttrValue::Style(s));
202 self
203 }
204
205 pub fn x_title<S: Into<String>>(mut self, t: S) -> Self {
207 self.props.set(
209 Attribute::Custom(CHART_X_TITLE),
210 AttrValue::String(t.into()),
211 );
212 self
213 }
214
215 pub fn y_title<S: Into<String>>(mut self, t: S) -> Self {
217 self.props.set(
219 Attribute::Custom(CHART_Y_TITLE),
220 AttrValue::String(t.into()),
221 );
222 self
223 }
224
225 fn set_data(&mut self, data: Vec<ChartDataset>) {
227 self.states.data = data;
228 self.states.reset_cursor();
229 }
230
231 fn is_disabled(&self) -> bool {
233 self.props
234 .get(Attribute::Disabled)
235 .and_then(AttrValue::as_flag)
236 .unwrap_or_default()
237 }
238
239 fn max_dataset_len(&self) -> usize {
241 self.states
242 .data
243 .iter()
244 .map(|v| v.get_data().len())
245 .max()
246 .unwrap_or(0)
247 }
248
249 fn get_tui_data(&self, start: usize) -> Vec<TuiDataset<'_>> {
251 self.states
252 .data
253 .iter()
254 .map(|x| x.as_tuichart(start))
255 .collect()
256 }
257
258 fn try_downcast(value: Box<dyn Any + Send + Sync>) -> Option<Vec<ChartDataset>> {
260 value
261 .downcast::<Vec<ChartDataset>>()
262 .map(|v| *v)
263 .or_else(|value| value.downcast::<ChartDataset>().map(|v| vec![*v]))
264 .ok()
265 }
266
267 fn data_from_attr(&mut self, attr: AttrValue) {
269 if let AttrValue::Payload(PropPayload::Any(val)) = attr
270 && let Some(data) = Self::try_downcast(val)
271 {
272 self.set_data(data);
273 }
274 }
275
276 fn data_to_attr(&self) -> AttrValue {
278 AttrValue::Payload(PropPayload::Any(Box::new(self.states.data.to_vec())))
279 }
280}
281
282impl Component for Chart {
283 fn view(&mut self, render: &mut Frame, area: Rect) {
284 if !self.common.display {
285 return;
286 }
287
288 let normal_style = self.common.style;
289
290 let mut x_axis: Axis = Axis::default();
293 if let Some((PropValue::F64(floor), PropValue::F64(ceil))) = self
294 .props
295 .get(Attribute::Custom(CHART_X_BOUNDS))
296 .and_then(AttrValue::as_payload)
297 .and_then(PropPayload::as_pair)
298 {
299 let why_using_vecs_when_you_can_use_useless_arrays: [f64; 2] = [*floor, *ceil];
300 x_axis = x_axis.bounds(why_using_vecs_when_you_can_use_useless_arrays);
301 }
302 if let Some(labels) = self
303 .props
304 .get(Attribute::Custom(CHART_X_LABELS))
305 .and_then(AttrValue::as_payload)
306 .and_then(PropPayload::as_vec)
307 {
308 x_axis = x_axis.labels(labels.iter().cloned().map(|x| Line::from(x.unwrap_str())));
309 }
310 if let Some(s) = self
311 .props
312 .get(Attribute::Custom(CHART_X_STYLE))
313 .and_then(AttrValue::as_style)
314 {
315 x_axis = x_axis.style(s);
316 }
317 if let Some(title) = self
318 .props
319 .get(Attribute::Custom(CHART_X_TITLE))
320 .and_then(AttrValue::as_string)
321 {
322 x_axis = x_axis.title(Span::styled(title, normal_style));
323 }
324 let mut y_axis: Axis = Axis::default();
326 if let Some((PropValue::F64(floor), PropValue::F64(ceil))) = self
327 .props
328 .get(Attribute::Custom(CHART_Y_BOUNDS))
329 .and_then(AttrValue::as_payload)
330 .and_then(PropPayload::as_pair)
331 {
332 let why_using_vecs_when_you_can_use_useless_arrays: [f64; 2] = [*floor, *ceil];
333 y_axis = y_axis.bounds(why_using_vecs_when_you_can_use_useless_arrays);
334 }
335 if let Some(labels) = self
336 .props
337 .get(Attribute::Custom(CHART_Y_LABELS))
338 .and_then(AttrValue::as_payload)
339 .and_then(PropPayload::as_vec)
340 {
341 y_axis = y_axis.labels(labels.iter().cloned().map(|x| Line::from(x.unwrap_str())));
342 }
343 if let Some(s) = self
344 .props
345 .get(Attribute::Custom(CHART_Y_STYLE))
346 .and_then(AttrValue::as_style)
347 {
348 y_axis = y_axis.style(s);
349 }
350 if let Some(title) = self
351 .props
352 .get(Attribute::Custom(CHART_Y_TITLE))
353 .and_then(AttrValue::as_string)
354 {
355 y_axis = y_axis.title(Span::styled(title, normal_style));
356 }
357
358 let data: Vec<TuiDataset> = self.get_tui_data(self.states.cursor);
360 let mut widget: TuiChart = TuiChart::new(data)
362 .style(normal_style)
363 .x_axis(x_axis)
364 .y_axis(y_axis);
365
366 if let Some(block) = self.common.get_block() {
367 widget = widget.block(block);
368 }
369
370 render.render_widget(widget, area);
372 }
373
374 fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
375 if let Some(value) = self.common.get_for_query(attr) {
376 return Some(value);
377 }
378
379 if attr == Attribute::Dataset {
380 return Some(self.data_to_attr().into());
381 }
382
383 self.props.get_for_query(attr)
384 }
385
386 fn attr(&mut self, attr: Attribute, value: AttrValue) {
387 if let Some(value) = self.common.set(attr, value) {
388 if attr == Attribute::Dataset {
389 self.data_from_attr(value);
390 return;
391 }
392 self.props.set(attr, value);
393 }
394 }
395
396 fn perform(&mut self, cmd: Cmd) -> CmdResult {
397 if !self.is_disabled() {
398 match cmd {
399 Cmd::Move(Direction::Left) => {
400 self.states.move_cursor_left();
401 }
402 Cmd::Move(Direction::Right) => {
403 self.states.move_cursor_right(self.max_dataset_len());
404 }
405 Cmd::GoTo(Position::Begin) => {
406 self.states.reset_cursor();
407 }
408 Cmd::GoTo(Position::End) => {
409 self.states.cursor_at_end(self.max_dataset_len());
410 }
411 _ => return CmdResult::Invalid(cmd),
412 }
413 return CmdResult::Visual;
414 }
415 CmdResult::NoChange
416 }
417
418 fn state(&self) -> State {
419 State::None
420 }
421}
422
423#[cfg(test)]
424mod test {
425
426 use pretty_assertions::assert_eq;
427 use tuirealm::props::HorizontalAlignment;
428 use tuirealm::ratatui::symbols::Marker;
429 use tuirealm::ratatui::widgets::GraphType;
430
431 use super::*;
432
433 #[test]
434 fn test_components_chart_states() {
435 let mut states: ChartStates = ChartStates::default();
436 assert_eq!(states.cursor, 0);
437 states.move_cursor_right(2);
439 assert_eq!(states.cursor, 1);
440 states.move_cursor_right(2);
442 assert_eq!(states.cursor, 1);
443 states.move_cursor_left();
445 assert_eq!(states.cursor, 0);
446 states.move_cursor_left();
448 assert_eq!(states.cursor, 0);
449 states.cursor_at_end(3);
451 assert_eq!(states.cursor, 2);
452 states.reset_cursor();
453 assert_eq!(states.cursor, 0);
454 }
455
456 #[test]
457 fn test_components_chart() {
458 let mut component: Chart = Chart::default()
459 .disabled(false)
460 .background(Color::Reset)
461 .foreground(Color::Reset)
462 .borders(Borders::default())
463 .title(
464 Title::from("average temperatures in Udine").alignment(HorizontalAlignment::Center),
465 )
466 .x_bounds((0.0, 11.0))
467 .x_labels(&[
468 "january",
469 "february",
470 "march",
471 "april",
472 "may",
473 "june",
474 "july",
475 "august",
476 "september",
477 "october",
478 "november",
479 "december",
480 ])
481 .x_style(Style::default().fg(Color::LightBlue))
482 .x_title("Temperature (°C)")
483 .y_bounds((-5.0, 35.0))
484 .y_labels(&["-5", "0", "5", "10", "15", "20", "25", "30", "35"])
485 .y_style(Style::default().fg(Color::LightYellow))
486 .y_title("Month")
487 .data([
488 ChartDataset::default()
489 .name("Minimum")
490 .graph_type(GraphType::Scatter)
491 .marker(Marker::Braille)
492 .style(Style::default().fg(Color::Cyan))
493 .data(vec![
494 (0.0, -1.0),
495 (1.0, 1.0),
496 (2.0, 3.0),
497 (3.0, 7.0),
498 (4.0, 11.0),
499 (5.0, 15.0),
500 (6.0, 17.0),
501 (7.0, 17.0),
502 (8.0, 13.0),
503 (9.0, 9.0),
504 (10.0, 4.0),
505 (11.0, 0.0),
506 ]),
507 ChartDataset::default()
508 .name("Maximum")
509 .graph_type(GraphType::Line)
510 .marker(Marker::Dot)
511 .style(Style::default().fg(Color::LightRed))
512 .data(vec![
513 (0.0, 7.0),
514 (1.0, 9.0),
515 (2.0, 13.0),
516 (3.0, 17.0),
517 (4.0, 22.0),
518 (5.0, 25.0),
519 (6.0, 28.0),
520 (7.0, 28.0),
521 (8.0, 24.0),
522 (9.0, 19.0),
523 (10.0, 13.0),
524 (11.0, 8.0),
525 ]),
526 ]);
527 assert_eq!(component.state(), State::None);
529 assert_eq!(
531 component.perform(Cmd::Move(Direction::Right)),
532 CmdResult::Visual
533 );
534 assert_eq!(component.states.cursor, 1);
535 assert_eq!(
537 component.perform(Cmd::Move(Direction::Left)),
538 CmdResult::Visual
539 );
540 assert_eq!(component.states.cursor, 0);
541 assert_eq!(
543 component.perform(Cmd::GoTo(Position::End)),
544 CmdResult::Visual
545 );
546 assert_eq!(component.states.cursor, 11);
547 assert_eq!(
549 component.perform(Cmd::GoTo(Position::Begin)),
550 CmdResult::Visual
551 );
552 assert_eq!(component.states.cursor, 0);
553 assert_eq!(component.max_dataset_len(), 12);
555 assert_eq!(component.is_disabled(), false);
556 assert_eq!(component.get_tui_data(2).len(), 2);
557
558 let comp = Chart::default().data([ChartDataset::default()
559 .name("Maximum")
560 .graph_type(GraphType::Line)
561 .marker(Marker::Dot)
562 .style(Style::default().fg(Color::LightRed))
563 .data(vec![(0.0, 7.0)])]);
564 assert!(!comp.get_tui_data(0).is_empty());
565
566 component.states.cursor_at_end(12);
568 component.attr(
569 Attribute::Dataset,
570 AttrValue::Payload(PropPayload::Any(Box::new(Vec::<ChartDataset>::new()))),
571 );
572 assert_eq!(component.max_dataset_len(), 0);
573 assert_eq!(component.states.cursor, 0);
575 }
576
577 #[test]
578 fn allowed_dataset_attrs() {
579 let mut component = Chart::default();
580 assert!(component.states.data.is_empty());
581
582 component.attr(
584 Attribute::Dataset,
585 AttrValue::Payload(PropPayload::Any(Box::new(vec![ChartDataset::default()]))),
586 );
587 assert_eq!(component.states.data.len(), 1);
588
589 component.attr(
590 Attribute::Dataset,
591 AttrValue::Payload(PropPayload::Any(Box::new(vec![ChartDataset::default()]))),
592 );
593 assert_eq!(component.states.data.len(), 1);
594
595 component.attr(
597 Attribute::Dataset,
598 AttrValue::Payload(PropPayload::Any(Box::new(ChartDataset::default()))),
599 );
600 assert_eq!(component.states.data.len(), 1);
601
602 component.attr(
603 Attribute::Dataset,
604 AttrValue::Payload(PropPayload::Any(Box::new(ChartDataset::default()))),
605 );
606 assert_eq!(component.states.data.len(), 1);
607 }
608}