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)]
86#[must_use]
87pub struct Chart {
88 props: Props,
89 pub states: ChartStates,
90}
91
92impl Chart {
93 pub fn foreground(mut self, fg: Color) -> Self {
94 self.props.set(Attribute::Foreground, AttrValue::Color(fg));
95 self
96 }
97
98 pub fn background(mut self, bg: Color) -> Self {
99 self.props.set(Attribute::Background, AttrValue::Color(bg));
100 self
101 }
102
103 pub fn borders(mut self, b: Borders) -> Self {
104 self.props.set(Attribute::Borders, AttrValue::Borders(b));
105 self
106 }
107
108 pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
109 self.props
110 .set(Attribute::Title, AttrValue::Title((t.into(), a)));
111 self
112 }
113
114 pub fn disabled(mut self, disabled: bool) -> Self {
115 self.attr(Attribute::Disabled, AttrValue::Flag(disabled));
116 self
117 }
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 {
125 self.props.set(
126 Attribute::Dataset,
127 AttrValue::Payload(PropPayload::Vec(
128 data.into_iter().map(PropValue::Dataset).collect(),
129 )),
130 );
131 self
132 }
133
134 pub fn x_bounds(mut self, bounds: (f64, f64)) -> Self {
135 self.props.set(
136 Attribute::Custom(CHART_X_BOUNDS),
137 AttrValue::Payload(PropPayload::Tup2((
138 PropValue::F64(bounds.0),
139 PropValue::F64(bounds.1),
140 ))),
141 );
142 self
143 }
144
145 pub fn y_bounds(mut self, bounds: (f64, f64)) -> Self {
146 self.props.set(
147 Attribute::Custom(CHART_Y_BOUNDS),
148 AttrValue::Payload(PropPayload::Tup2((
149 PropValue::F64(bounds.0),
150 PropValue::F64(bounds.1),
151 ))),
152 );
153 self
154 }
155
156 pub fn x_labels(mut self, labels: &[&str]) -> Self {
157 self.attr(
158 Attribute::Custom(CHART_X_LABELS),
159 AttrValue::Payload(PropPayload::Vec(
160 labels
161 .iter()
162 .map(|x| PropValue::Str((*x).to_string()))
163 .collect(),
164 )),
165 );
166 self
167 }
168
169 pub fn y_labels(mut self, labels: &[&str]) -> Self {
170 self.attr(
171 Attribute::Custom(CHART_Y_LABELS),
172 AttrValue::Payload(PropPayload::Vec(
173 labels
174 .iter()
175 .map(|x| PropValue::Str((*x).to_string()))
176 .collect(),
177 )),
178 );
179 self
180 }
181
182 pub fn x_style(mut self, s: Style) -> Self {
183 self.attr(Attribute::Custom(CHART_X_STYLE), AttrValue::Style(s));
184 self
185 }
186
187 pub fn y_style(mut self, s: Style) -> Self {
188 self.attr(Attribute::Custom(CHART_Y_STYLE), AttrValue::Style(s));
189 self
190 }
191
192 pub fn x_title<S: Into<String>>(mut self, t: S) -> Self {
193 self.props.set(
194 Attribute::Custom(CHART_X_TITLE),
195 AttrValue::String(t.into()),
196 );
197 self
198 }
199
200 pub fn y_title<S: Into<String>>(mut self, t: S) -> Self {
201 self.props.set(
202 Attribute::Custom(CHART_Y_TITLE),
203 AttrValue::String(t.into()),
204 );
205 self
206 }
207
208 fn is_disabled(&self) -> bool {
209 self.props
210 .get_or(Attribute::Disabled, AttrValue::Flag(false))
211 .unwrap_flag()
212 }
213
214 fn max_dataset_len(&self) -> usize {
218 self.props
219 .get(Attribute::Dataset)
220 .and_then(|x| {
221 x.unwrap_payload()
222 .unwrap_vec()
223 .iter()
224 .cloned()
225 .map(|x| x.unwrap_dataset().get_data().len())
226 .max()
227 })
228 .unwrap_or(0)
229 }
230
231 fn get_data(&mut self, start: 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))
250 .collect()
251 }
252}
253
254impl<'a> Chart {
255 fn get_tui_dataset(dataset: &'a Dataset, start: usize) -> TuiDataset<'a> {
260 let points = dataset.get_data();
261
262 TuiDataset::default()
264 .name(dataset.name.clone())
265 .marker(dataset.marker)
266 .graph_type(dataset.graph_type)
267 .style(dataset.style)
268 .data(&points[start..])
269 }
270}
271
272impl 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 .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());
301 let active: bool = if self.is_disabled() { true } else { focus };
302 let div = crate::utils::get_block(borders, title.as_ref(), active, inactive_style);
303 let mut x_axis: Axis = Axis::default();
306 if let Some((PropValue::F64(floor), PropValue::F64(ceil))) = self
307 .props
308 .get(Attribute::Custom(CHART_X_BOUNDS))
309 .map(|x| x.unwrap_payload().unwrap_tup2())
310 {
311 let why_using_vecs_when_you_can_use_useless_arrays: [f64; 2] = [floor, ceil];
312 x_axis = x_axis.bounds(why_using_vecs_when_you_can_use_useless_arrays);
313 }
314 if let Some(PropPayload::Vec(labels)) = self
315 .props
316 .get(Attribute::Custom(CHART_X_LABELS))
317 .map(|x| x.unwrap_payload())
318 {
319 x_axis = x_axis.labels(labels.iter().cloned().map(|x| Line::from(x.unwrap_str())));
320 }
321 if let Some(s) = self
322 .props
323 .get(Attribute::Custom(CHART_X_STYLE))
324 .map(|x| x.unwrap_style())
325 {
326 x_axis = x_axis.style(s);
327 }
328 if let Some(title) = self
329 .props
330 .get(Attribute::Custom(CHART_X_TITLE))
331 .map(|x| x.unwrap_string())
332 {
333 x_axis = x_axis.title(Span::styled(
334 title,
335 Style::default().fg(foreground).bg(background),
336 ));
337 }
338 let mut y_axis: Axis = Axis::default();
340 if let Some((PropValue::F64(floor), PropValue::F64(ceil))) = self
341 .props
342 .get(Attribute::Custom(CHART_Y_BOUNDS))
343 .map(|x| x.unwrap_payload().unwrap_tup2())
344 {
345 let why_using_vecs_when_you_can_use_useless_arrays: [f64; 2] = [floor, ceil];
346 y_axis = y_axis.bounds(why_using_vecs_when_you_can_use_useless_arrays);
347 }
348 if let Some(PropPayload::Vec(labels)) = self
349 .props
350 .get(Attribute::Custom(CHART_Y_LABELS))
351 .map(|x| x.unwrap_payload())
352 {
353 y_axis = y_axis.labels(labels.iter().cloned().map(|x| Line::from(x.unwrap_str())));
354 }
355 if let Some(s) = self
356 .props
357 .get(Attribute::Custom(CHART_Y_STYLE))
358 .map(|x| x.unwrap_style())
359 {
360 y_axis = y_axis.style(s);
361 }
362 if let Some(title) = self
363 .props
364 .get(Attribute::Custom(CHART_Y_TITLE))
365 .map(|x| x.unwrap_string())
366 {
367 y_axis = y_axis.title(Span::styled(
368 title,
369 Style::default().fg(foreground).bg(background),
370 ));
371 }
372 let data: Vec<TuiDataset> = self.get_data(self.states.cursor);
374 let widget: TuiChart = TuiChart::new(data).block(div).x_axis(x_axis).y_axis(y_axis);
376 render.render_widget(widget, area);
378 }
379 }
380
381 fn query(&self, attr: Attribute) -> Option<AttrValue> {
382 self.props.get(attr)
383 }
384
385 fn attr(&mut self, attr: Attribute, value: AttrValue) {
386 self.props.set(attr, value);
387 self.states.reset_cursor();
388 }
389
390 fn perform(&mut self, cmd: Cmd) -> CmdResult {
391 if !self.is_disabled() {
392 match cmd {
393 Cmd::Move(Direction::Left) => {
394 self.states.move_cursor_left();
395 }
396 Cmd::Move(Direction::Right) => {
397 self.states.move_cursor_right(self.max_dataset_len());
398 }
399 Cmd::GoTo(Position::Begin) => {
400 self.states.reset_cursor();
401 }
402 Cmd::GoTo(Position::End) => {
403 self.states.cursor_at_end(self.max_dataset_len());
404 }
405 _ => {}
406 }
407 }
408 CmdResult::None
409 }
410
411 fn state(&self) -> State {
412 State::None
413 }
414}
415
416#[cfg(test)]
417mod test {
418
419 use super::*;
420
421 use pretty_assertions::assert_eq;
422 use tuirealm::ratatui::{symbols::Marker, widgets::GraphType};
423
424 #[test]
425 fn test_components_chart_states() {
426 let mut states: ChartStates = ChartStates::default();
427 assert_eq!(states.cursor, 0);
428 states.move_cursor_right(2);
430 assert_eq!(states.cursor, 1);
431 states.move_cursor_right(2);
433 assert_eq!(states.cursor, 1);
434 states.move_cursor_left();
436 assert_eq!(states.cursor, 0);
437 states.move_cursor_left();
439 assert_eq!(states.cursor, 0);
440 states.cursor_at_end(3);
442 assert_eq!(states.cursor, 2);
443 states.reset_cursor();
444 assert_eq!(states.cursor, 0);
445 }
446
447 #[test]
448 fn test_components_chart() {
449 let mut component: Chart = Chart::default()
450 .disabled(false)
451 .background(Color::Reset)
452 .foreground(Color::Reset)
453 .borders(Borders::default())
454 .title("average temperatures in Udine", Alignment::Center)
455 .x_bounds((0.0, 11.0))
456 .x_labels(&[
457 "january",
458 "february",
459 "march",
460 "april",
461 "may",
462 "june",
463 "july",
464 "august",
465 "september",
466 "october",
467 "november",
468 "december",
469 ])
470 .x_style(Style::default().fg(Color::LightBlue))
471 .x_title("Temperature (°C)")
472 .y_bounds((-5.0, 35.0))
473 .y_labels(&["-5", "0", "5", "10", "15", "20", "25", "30", "35"])
474 .y_style(Style::default().fg(Color::LightYellow))
475 .y_title("Month")
476 .data([
477 Dataset::default()
478 .name("Minimum")
479 .graph_type(GraphType::Scatter)
480 .marker(Marker::Braille)
481 .style(Style::default().fg(Color::Cyan))
482 .data(vec![
483 (0.0, -1.0),
484 (1.0, 1.0),
485 (2.0, 3.0),
486 (3.0, 7.0),
487 (4.0, 11.0),
488 (5.0, 15.0),
489 (6.0, 17.0),
490 (7.0, 17.0),
491 (8.0, 13.0),
492 (9.0, 9.0),
493 (10.0, 4.0),
494 (11.0, 0.0),
495 ]),
496 Dataset::default()
497 .name("Maximum")
498 .graph_type(GraphType::Line)
499 .marker(Marker::Dot)
500 .style(Style::default().fg(Color::LightRed))
501 .data(vec![
502 (0.0, 7.0),
503 (1.0, 9.0),
504 (2.0, 13.0),
505 (3.0, 17.0),
506 (4.0, 22.0),
507 (5.0, 25.0),
508 (6.0, 28.0),
509 (7.0, 28.0),
510 (8.0, 24.0),
511 (9.0, 19.0),
512 (10.0, 13.0),
513 (11.0, 8.0),
514 ]),
515 ]);
516 assert_eq!(component.state(), State::None);
518 assert_eq!(
520 component.perform(Cmd::Move(Direction::Right)),
521 CmdResult::None
522 );
523 assert_eq!(component.states.cursor, 1);
524 assert_eq!(
526 component.perform(Cmd::Move(Direction::Left)),
527 CmdResult::None
528 );
529 assert_eq!(component.states.cursor, 0);
530 assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
532 assert_eq!(component.states.cursor, 11);
533 assert_eq!(
535 component.perform(Cmd::GoTo(Position::Begin)),
536 CmdResult::None
537 );
538 assert_eq!(component.states.cursor, 0);
539 assert_eq!(component.max_dataset_len(), 12);
541 assert_eq!(component.is_disabled(), false);
542 assert_eq!(component.get_data(2).len(), 2);
543
544 let mut comp = Chart::default().data([Dataset::default()
545 .name("Maximum")
546 .graph_type(GraphType::Line)
547 .marker(Marker::Dot)
548 .style(Style::default().fg(Color::LightRed))
549 .data(vec![(0.0, 7.0)])]);
550 assert!(!comp.get_data(0).is_empty());
551
552 component.states.cursor_at_end(12);
554 component.attr(
555 Attribute::Dataset,
556 AttrValue::Payload(PropPayload::Vec(vec![])),
557 );
558 assert_eq!(component.max_dataset_len(), 0);
559 assert_eq!(component.states.cursor, 0);
561 }
562}