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 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 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 {
334 x_axis = x_axis.title(Span::styled(title, normal_style));
335 }
336 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 {
365 y_axis = y_axis.title(Span::styled(title, normal_style));
366 }
367 let data: Vec<TuiDataset> = self.get_data(self.states.cursor);
369 let widget: TuiChart = TuiChart::new(data)
371 .style(normal_style)
372 .block(div)
373 .x_axis(x_axis)
374 .y_axis(y_axis);
375 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) {
385 self.props.set(attr, value);
386 self.states.reset_cursor();
387 }
388
389 fn perform(&mut self, cmd: Cmd) -> CmdResult {
390 if !self.is_disabled() {
391 match cmd {
392 Cmd::Move(Direction::Left) => {
393 self.states.move_cursor_left();
394 }
395 Cmd::Move(Direction::Right) => {
396 self.states.move_cursor_right(self.max_dataset_len());
397 }
398 Cmd::GoTo(Position::Begin) => {
399 self.states.reset_cursor();
400 }
401 Cmd::GoTo(Position::End) => {
402 self.states.cursor_at_end(self.max_dataset_len());
403 }
404 _ => {}
405 }
406 }
407 CmdResult::None
408 }
409
410 fn state(&self) -> State {
411 State::None
412 }
413}
414
415#[cfg(test)]
416mod 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() {
425 let mut states: ChartStates = ChartStates::default();
426 assert_eq!(states.cursor, 0);
427 states.move_cursor_right(2);
429 assert_eq!(states.cursor, 1);
430 states.move_cursor_right(2);
432 assert_eq!(states.cursor, 1);
433 states.move_cursor_left();
435 assert_eq!(states.cursor, 0);
436 states.move_cursor_left();
438 assert_eq!(states.cursor, 0);
439 states.cursor_at_end(3);
441 assert_eq!(states.cursor, 2);
442 states.reset_cursor();
443 assert_eq!(states.cursor, 0);
444 }
445
446 #[test]
447 fn test_components_chart() {
448 let mut component: Chart = Chart::default()
449 .disabled(false)
450 .background(Color::Reset)
451 .foreground(Color::Reset)
452 .borders(Borders::default())
453 .title("average temperatures in Udine", Alignment::Center)
454 .x_bounds((0.0, 11.0))
455 .x_labels(&[
456 "january",
457 "february",
458 "march",
459 "april",
460 "may",
461 "june",
462 "july",
463 "august",
464 "september",
465 "october",
466 "november",
467 "december",
468 ])
469 .x_style(Style::default().fg(Color::LightBlue))
470 .x_title("Temperature (°C)")
471 .y_bounds((-5.0, 35.0))
472 .y_labels(&["-5", "0", "5", "10", "15", "20", "25", "30", "35"])
473 .y_style(Style::default().fg(Color::LightYellow))
474 .y_title("Month")
475 .data([
476 Dataset::default()
477 .name("Minimum")
478 .graph_type(GraphType::Scatter)
479 .marker(Marker::Braille)
480 .style(Style::default().fg(Color::Cyan))
481 .data(vec![
482 (0.0, -1.0),
483 (1.0, 1.0),
484 (2.0, 3.0),
485 (3.0, 7.0),
486 (4.0, 11.0),
487 (5.0, 15.0),
488 (6.0, 17.0),
489 (7.0, 17.0),
490 (8.0, 13.0),
491 (9.0, 9.0),
492 (10.0, 4.0),
493 (11.0, 0.0),
494 ]),
495 Dataset::default()
496 .name("Maximum")
497 .graph_type(GraphType::Line)
498 .marker(Marker::Dot)
499 .style(Style::default().fg(Color::LightRed))
500 .data(vec![
501 (0.0, 7.0),
502 (1.0, 9.0),
503 (2.0, 13.0),
504 (3.0, 17.0),
505 (4.0, 22.0),
506 (5.0, 25.0),
507 (6.0, 28.0),
508 (7.0, 28.0),
509 (8.0, 24.0),
510 (9.0, 19.0),
511 (10.0, 13.0),
512 (11.0, 8.0),
513 ]),
514 ]);
515 assert_eq!(component.state(), State::None);
517 assert_eq!(
519 component.perform(Cmd::Move(Direction::Right)),
520 CmdResult::None
521 );
522 assert_eq!(component.states.cursor, 1);
523 assert_eq!(
525 component.perform(Cmd::Move(Direction::Left)),
526 CmdResult::None
527 );
528 assert_eq!(component.states.cursor, 0);
529 assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
531 assert_eq!(component.states.cursor, 11);
532 assert_eq!(
534 component.perform(Cmd::GoTo(Position::Begin)),
535 CmdResult::None
536 );
537 assert_eq!(component.states.cursor, 0);
538 assert_eq!(component.max_dataset_len(), 12);
540 assert_eq!(component.is_disabled(), false);
541 assert_eq!(component.get_data(2).len(), 2);
542
543 let mut comp = Chart::default().data([Dataset::default()
544 .name("Maximum")
545 .graph_type(GraphType::Line)
546 .marker(Marker::Dot)
547 .style(Style::default().fg(Color::LightRed))
548 .data(vec![(0.0, 7.0)])]);
549 assert!(!comp.get_data(0).is_empty());
550
551 component.states.cursor_at_end(12);
553 component.attr(
554 Attribute::Dataset,
555 AttrValue::Payload(PropPayload::Vec(vec![])),
556 );
557 assert_eq!(component.max_dataset_len(), 0);
558 assert_eq!(component.states.cursor, 0);
560 }
561}