1use alloc::string::{String, ToString};
4use alloc::vec::Vec;
5use core::cmp::min;
6
7use ratatui_core::buffer::Buffer;
8use ratatui_core::layout::Rect;
9use ratatui_core::style::{Style, Styled};
10use ratatui_core::symbols;
11use ratatui_core::widgets::Widget;
12use strum::{Display, EnumString};
13
14use crate::block::{Block, BlockExt};
15
16#[derive(Debug, Default, Clone, Eq, PartialEq)]
66pub struct Sparkline<'a> {
67 block: Option<Block<'a>>,
69 style: Style,
71 absent_value_style: Style,
73 absent_value_symbol: AbsentValueSymbol,
75 data: Vec<SparklineBar>,
77 max: Option<u64>,
80 bar_set: symbols::bar::Set<'a>,
82 direction: RenderDirection,
84}
85
86#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
90#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
91pub enum RenderDirection {
92 #[default]
94 LeftToRight,
95 RightToLeft,
97}
98
99impl<'a> Sparkline<'a> {
100 #[must_use = "method moves the value of self and returns the modified value"]
102 pub fn block(mut self, block: Block<'a>) -> Self {
103 self.block = Some(block);
104 self
105 }
106
107 #[must_use = "method moves the value of self and returns the modified value"]
116 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
117 self.style = style.into();
118 self
119 }
120
121 #[must_use = "method moves the value of self and returns the modified value"]
132 pub fn absent_value_style<S: Into<Style>>(mut self, style: S) -> Self {
133 self.absent_value_style = style.into();
134 self
135 }
136
137 #[must_use = "method moves the value of self and returns the modified value"]
143 pub fn absent_value_symbol(mut self, symbol: impl Into<String>) -> Self {
144 self.absent_value_symbol = AbsentValueSymbol(symbol.into());
145 self
146 }
147
148 #[must_use = "method moves the value of self and returns the modified value"]
209 pub fn data<T>(mut self, data: T) -> Self
210 where
211 T: IntoIterator,
212 T::Item: Into<SparklineBar>,
213 {
214 self.data = data.into_iter().map(Into::into).collect();
215 self
216 }
217
218 #[must_use = "method moves the value of self and returns the modified value"]
223 pub const fn max(mut self, max: u64) -> Self {
224 self.max = Some(max);
225 self
226 }
227
228 #[must_use = "method moves the value of self and returns the modified value"]
233 pub const fn bar_set(mut self, bar_set: symbols::bar::Set<'a>) -> Self {
234 self.bar_set = bar_set;
235 self
236 }
237
238 #[must_use = "method moves the value of self and returns the modified value"]
242 pub const fn direction(mut self, direction: RenderDirection) -> Self {
243 self.direction = direction;
244 self
245 }
246}
247
248#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
253pub struct SparklineBar {
254 value: Option<u64>,
258 style: Option<Style>,
262}
263
264impl SparklineBar {
265 #[must_use = "method moves the value of self and returns the modified value"]
278 pub fn style<S: Into<Option<Style>>>(mut self, style: S) -> Self {
279 self.style = style.into();
280 self
281 }
282}
283
284impl From<Option<u64>> for SparklineBar {
285 fn from(value: Option<u64>) -> Self {
286 Self { value, style: None }
287 }
288}
289
290impl From<u64> for SparklineBar {
291 fn from(value: u64) -> Self {
292 Self {
293 value: Some(value),
294 style: None,
295 }
296 }
297}
298
299impl From<&u64> for SparklineBar {
300 fn from(value: &u64) -> Self {
301 Self {
302 value: Some(*value),
303 style: None,
304 }
305 }
306}
307
308impl From<&Option<u64>> for SparklineBar {
309 fn from(value: &Option<u64>) -> Self {
310 Self {
311 value: *value,
312 style: None,
313 }
314 }
315}
316
317impl Styled for Sparkline<'_> {
318 type Item = Self;
319
320 fn style(&self) -> Style {
321 self.style
322 }
323
324 fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
325 self.style(style)
326 }
327}
328
329impl Widget for Sparkline<'_> {
330 fn render(self, area: Rect, buf: &mut Buffer) {
331 Widget::render(&self, area, buf);
332 }
333}
334
335impl Widget for &Sparkline<'_> {
336 fn render(self, area: Rect, buf: &mut Buffer) {
337 self.block.as_ref().render(area, buf);
338 let inner = self.block.inner_if_some(area);
339 self.render_sparkline(inner, buf);
340 }
341}
342
343#[derive(Debug, Clone, Eq, PartialEq)]
345struct AbsentValueSymbol(String);
346
347impl Default for AbsentValueSymbol {
348 fn default() -> Self {
349 Self(symbols::shade::EMPTY.to_string())
350 }
351}
352
353impl Sparkline<'_> {
354 fn render_sparkline(&self, spark_area: Rect, buf: &mut Buffer) {
355 if spark_area.is_empty() {
356 return;
357 }
358 let max_height = self
360 .max
361 .unwrap_or_else(|| self.data.iter().filter_map(|s| s.value).max().unwrap_or(1));
362
363 let max_index = min(spark_area.width as usize, self.data.len());
365
366 for (i, item) in self.data.iter().take(max_index).enumerate() {
368 let x = match self.direction {
369 RenderDirection::LeftToRight => spark_area.left() + i as u16,
370 RenderDirection::RightToLeft => spark_area.right() - i as u16 - 1,
371 };
372
373 let (mut height, symbol, style) = match item {
385 SparklineBar {
386 value: Some(value),
387 style,
388 } => {
389 let height = Self::scale_height(*value, max_height, spark_area.height);
390 (height, None, *style)
391 }
392 _ => (
393 u64::from(spark_area.height) * 8,
394 Some(self.absent_value_symbol.0.as_str()),
395 Some(self.absent_value_style),
396 ),
397 };
398
399 for j in (0..spark_area.height).rev() {
407 let symbol = symbol.unwrap_or_else(|| self.symbol_for_height(height));
408 if height > 8 {
409 height -= 8;
410 } else {
411 height = 0;
412 }
413 buf[(x, spark_area.top() + j)]
414 .set_symbol(symbol)
415 .set_style(self.style.patch(style.unwrap_or_default()));
416 }
417 }
418 }
419
420 const fn symbol_for_height(&self, height: u64) -> &str {
421 match height {
422 0 => self.bar_set.empty,
423 1 => self.bar_set.one_eighth,
424 2 => self.bar_set.one_quarter,
425 3 => self.bar_set.three_eighths,
426 4 => self.bar_set.half,
427 5 => self.bar_set.five_eighths,
428 6 => self.bar_set.three_quarters,
429 7 => self.bar_set.seven_eighths,
430 _ => self.bar_set.full,
431 }
432 }
433
434 fn scale_height(value: u64, max: u64, max_height: u16) -> u64 {
435 if max == 0 {
436 return 0;
437 }
438
439 let max_ticks = u128::from(max_height) * 8;
440 let ticks = u128::from(value) * max_ticks / u128::from(max);
441 ticks.min(max_ticks) as u64
442 }
443}
444
445#[cfg(test)]
446mod tests {
447 use alloc::vec;
448
449 use ratatui_core::buffer::Cell;
450 use ratatui_core::style::{Color, Modifier, Stylize};
451 use strum::ParseError;
452
453 use super::*;
454
455 #[test]
456 fn render_direction_to_string() {
457 assert_eq!(RenderDirection::LeftToRight.to_string(), "LeftToRight");
458 assert_eq!(RenderDirection::RightToLeft.to_string(), "RightToLeft");
459 }
460
461 #[test]
462 fn render_direction_from_str() {
463 assert_eq!(
464 "LeftToRight".parse::<RenderDirection>(),
465 Ok(RenderDirection::LeftToRight)
466 );
467 assert_eq!(
468 "RightToLeft".parse::<RenderDirection>(),
469 Ok(RenderDirection::RightToLeft)
470 );
471 assert_eq!(
472 "".parse::<RenderDirection>(),
473 Err(ParseError::VariantNotFound)
474 );
475 }
476
477 #[test]
478 fn it_can_be_created_from_vec_of_u64() {
479 let data = vec![1_u64, 2, 3];
480 let spark_data = Sparkline::default().data(data).data;
481 let expected = vec![
482 SparklineBar::from(1),
483 SparklineBar::from(2),
484 SparklineBar::from(3),
485 ];
486 assert_eq!(spark_data, expected);
487 }
488
489 #[test]
490 fn it_can_be_created_from_vec_of_option_u64() {
491 let data = vec![Some(1_u64), None, Some(3)];
492 let spark_data = Sparkline::default().data(data).data;
493 let expected = vec![
494 SparklineBar::from(1),
495 SparklineBar::from(None),
496 SparklineBar::from(3),
497 ];
498 assert_eq!(spark_data, expected);
499 }
500
501 #[test]
502 fn it_can_be_created_from_array_of_u64() {
503 let data = [1_u64, 2, 3];
504 let spark_data = Sparkline::default().data(data).data;
505 let expected = vec![
506 SparklineBar::from(1),
507 SparklineBar::from(2),
508 SparklineBar::from(3),
509 ];
510 assert_eq!(spark_data, expected);
511 }
512
513 #[test]
514 fn it_can_be_created_from_array_of_option_u64() {
515 let data = [Some(1_u64), None, Some(3)];
516 let spark_data = Sparkline::default().data(data).data;
517 let expected = vec![
518 SparklineBar::from(1),
519 SparklineBar::from(None),
520 SparklineBar::from(3),
521 ];
522 assert_eq!(spark_data, expected);
523 }
524
525 #[test]
526 fn it_can_be_created_from_slice_of_u64() {
527 let data = vec![1_u64, 2, 3];
528 let spark_data = Sparkline::default().data(&data).data;
529 let expected = vec![
530 SparklineBar::from(1),
531 SparklineBar::from(2),
532 SparklineBar::from(3),
533 ];
534 assert_eq!(spark_data, expected);
535 }
536
537 #[test]
538 fn it_can_be_created_from_slice_of_option_u64() {
539 let data = vec![Some(1_u64), None, Some(3)];
540 let spark_data = Sparkline::default().data(&data).data;
541 let expected = vec![
542 SparklineBar::from(1),
543 SparklineBar::from(None),
544 SparklineBar::from(3),
545 ];
546 assert_eq!(spark_data, expected);
547 }
548
549 fn render(widget: Sparkline<'_>, width: u16) -> Buffer {
552 let area = Rect::new(0, 0, width, 1);
553 let mut buffer = Buffer::filled(area, Cell::new("x"));
554 widget.render(area, &mut buffer);
555 buffer
556 }
557
558 #[test]
559 fn it_does_not_panic_if_max_is_zero() {
560 let widget = Sparkline::default().data([0, 0, 0]);
561 let buffer = render(widget, 6);
562 assert_eq!(buffer, Buffer::with_lines([" xxx"]));
563 }
564
565 #[test]
566 fn it_does_not_panic_if_max_is_set_to_zero() {
567 let widget = Sparkline::default().data([0, 1, 2]).max(0);
569 let buffer = render(widget, 6);
570 assert_eq!(buffer, Buffer::with_lines([" xxx"]));
571 }
572
573 #[test]
574 fn it_draws() {
575 let widget = Sparkline::default().data([0, 1, 2, 3, 4, 5, 6, 7, 8]);
576 let buffer = render(widget, 12);
577 assert_eq!(buffer, Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]));
578 }
579
580 #[test]
581 fn it_draws_double_height() {
582 let widget = Sparkline::default().data([0, 1, 2, 3, 4, 5, 6, 7, 8]);
583 let area = Rect::new(0, 0, 12, 2);
584 let mut buffer = Buffer::filled(area, Cell::new("x"));
585 widget.render(area, &mut buffer);
586 assert_eq!(buffer, Buffer::with_lines([" ▂▄▆█xxx", " ▂▄▆█████xxx"]));
587 }
588
589 #[test]
590 fn render_handles_u64_max_value() {
591 let widget = Sparkline::default().data([u64::MAX]).max(u64::MAX);
592 let area = Rect::new(0, 0, 1, 3);
593 let mut buffer = Buffer::empty(area);
594
595 widget.render(area, &mut buffer);
596
597 assert_eq!(buffer, Buffer::with_lines(["█"; 3]));
598 }
599
600 #[test]
601 fn render_keeps_integer_precision_for_large_values() {
602 let widget = Sparkline::default().data([u64::MAX - 1]).max(u64::MAX);
603 let area = Rect::new(0, 0, 1, 1);
604 let mut buffer = Buffer::empty(area);
605
606 widget.render(area, &mut buffer);
607
608 assert_eq!(buffer, Buffer::with_lines(["▇"]));
609 }
610
611 #[test]
612 fn it_renders_left_to_right() {
613 let widget = Sparkline::default()
614 .data([0, 1, 2, 3, 4, 5, 6, 7, 8])
615 .direction(RenderDirection::LeftToRight);
616 let buffer = render(widget, 12);
617 assert_eq!(buffer, Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]));
618 }
619
620 #[test]
621 fn it_renders_right_to_left() {
622 let widget = Sparkline::default()
623 .data([0, 1, 2, 3, 4, 5, 6, 7, 8])
624 .direction(RenderDirection::RightToLeft);
625 let buffer = render(widget, 12);
626 assert_eq!(buffer, Buffer::with_lines(["xxx█▇▆▅▄▃▂▁ "]));
627 }
628
629 #[test]
630 fn it_renders_with_absent_value_style() {
631 let widget = Sparkline::default()
632 .absent_value_style(Style::default().fg(Color::Red))
633 .absent_value_symbol(symbols::shade::FULL)
634 .data([
635 None,
636 Some(1),
637 Some(2),
638 Some(3),
639 Some(4),
640 Some(5),
641 Some(6),
642 Some(7),
643 Some(8),
644 ]);
645 let buffer = render(widget, 12);
646 let mut expected = Buffer::with_lines(["█▁▂▃▄▅▆▇█xxx"]);
647 expected.set_style(Rect::new(0, 0, 1, 1), Style::default().fg(Color::Red));
648 assert_eq!(buffer, expected);
649 }
650
651 #[test]
652 fn it_renders_with_absent_value_style_double_height() {
653 let widget = Sparkline::default()
654 .absent_value_style(Style::default().fg(Color::Red))
655 .absent_value_symbol(symbols::shade::FULL)
656 .data([
657 None,
658 Some(1),
659 Some(2),
660 Some(3),
661 Some(4),
662 Some(5),
663 Some(6),
664 Some(7),
665 Some(8),
666 ]);
667 let area = Rect::new(0, 0, 12, 2);
668 let mut buffer = Buffer::filled(area, Cell::new("x"));
669 widget.render(area, &mut buffer);
670 let mut expected = Buffer::with_lines(["█ ▂▄▆█xxx", "█▂▄▆█████xxx"]);
671 expected.set_style(Rect::new(0, 0, 1, 2), Style::default().fg(Color::Red));
672 assert_eq!(buffer, expected);
673 }
674
675 #[test]
676 fn it_renders_with_custom_absent_value_style() {
677 let widget = Sparkline::default().absent_value_symbol('*').data([
678 None,
679 Some(1),
680 Some(2),
681 Some(3),
682 Some(4),
683 Some(5),
684 Some(6),
685 Some(7),
686 Some(8),
687 ]);
688 let buffer = render(widget, 12);
689 let expected = Buffer::with_lines(["*▁▂▃▄▅▆▇█xxx"]);
690 assert_eq!(buffer, expected);
691 }
692
693 #[test]
694 fn it_renders_with_custom_bar_styles() {
695 let widget = Sparkline::default().data(vec![
696 SparklineBar::from(Some(0)).style(Some(Style::default().fg(Color::Red))),
697 SparklineBar::from(Some(1)).style(Some(Style::default().fg(Color::Red))),
698 SparklineBar::from(Some(2)).style(Some(Style::default().fg(Color::Red))),
699 SparklineBar::from(Some(3)).style(Some(Style::default().fg(Color::Green))),
700 SparklineBar::from(Some(4)).style(Some(Style::default().fg(Color::Green))),
701 SparklineBar::from(Some(5)).style(Some(Style::default().fg(Color::Green))),
702 SparklineBar::from(Some(6)).style(Some(Style::default().fg(Color::Blue))),
703 SparklineBar::from(Some(7)).style(Some(Style::default().fg(Color::Blue))),
704 SparklineBar::from(Some(8)).style(Some(Style::default().fg(Color::Blue))),
705 ]);
706 let buffer = render(widget, 12);
707 let mut expected = Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]);
708 expected.set_style(Rect::new(0, 0, 3, 1), Style::default().fg(Color::Red));
709 expected.set_style(Rect::new(3, 0, 3, 1), Style::default().fg(Color::Green));
710 expected.set_style(Rect::new(6, 0, 3, 1), Style::default().fg(Color::Blue));
711 assert_eq!(buffer, expected);
712 }
713
714 #[test]
715 fn can_be_stylized() {
716 assert_eq!(
717 Sparkline::default()
718 .black()
719 .on_white()
720 .bold()
721 .not_dim()
722 .style,
723 Style::default()
724 .fg(Color::Black)
725 .bg(Color::White)
726 .add_modifier(Modifier::BOLD)
727 .remove_modifier(Modifier::DIM)
728 );
729 }
730
731 #[test]
732 fn render_in_minimal_buffer() {
733 let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
734 let sparkline = Sparkline::default()
735 .data([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
736 .max(10);
737 sparkline.render(buffer.area, &mut buffer);
739 assert_eq!(buffer, Buffer::with_lines([" "]));
740 }
741
742 #[test]
743 fn render_in_zero_size_buffer() {
744 let mut buffer = Buffer::empty(Rect::ZERO);
745 let sparkline = Sparkline::default()
746 .data([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
747 .max(10);
748 sparkline.render(buffer.area, &mut buffer);
750 }
751}