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 = if max_height == 0 {
390 0
391 } else {
392 *value * u64::from(spark_area.height) * 8 / max_height
393 };
394 (height, None, *style)
395 }
396 _ => (
397 u64::from(spark_area.height) * 8,
398 Some(self.absent_value_symbol.0.as_str()),
399 Some(self.absent_value_style),
400 ),
401 };
402
403 for j in (0..spark_area.height).rev() {
411 let symbol = symbol.unwrap_or_else(|| self.symbol_for_height(height));
412 if height > 8 {
413 height -= 8;
414 } else {
415 height = 0;
416 }
417 buf[(x, spark_area.top() + j)]
418 .set_symbol(symbol)
419 .set_style(self.style.patch(style.unwrap_or_default()));
420 }
421 }
422 }
423
424 const fn symbol_for_height(&self, height: u64) -> &str {
425 match height {
426 0 => self.bar_set.empty,
427 1 => self.bar_set.one_eighth,
428 2 => self.bar_set.one_quarter,
429 3 => self.bar_set.three_eighths,
430 4 => self.bar_set.half,
431 5 => self.bar_set.five_eighths,
432 6 => self.bar_set.three_quarters,
433 7 => self.bar_set.seven_eighths,
434 _ => self.bar_set.full,
435 }
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use alloc::vec;
442
443 use ratatui_core::buffer::Cell;
444 use ratatui_core::style::{Color, Modifier, Stylize};
445 use strum::ParseError;
446
447 use super::*;
448
449 #[test]
450 fn render_direction_to_string() {
451 assert_eq!(RenderDirection::LeftToRight.to_string(), "LeftToRight");
452 assert_eq!(RenderDirection::RightToLeft.to_string(), "RightToLeft");
453 }
454
455 #[test]
456 fn render_direction_from_str() {
457 assert_eq!(
458 "LeftToRight".parse::<RenderDirection>(),
459 Ok(RenderDirection::LeftToRight)
460 );
461 assert_eq!(
462 "RightToLeft".parse::<RenderDirection>(),
463 Ok(RenderDirection::RightToLeft)
464 );
465 assert_eq!(
466 "".parse::<RenderDirection>(),
467 Err(ParseError::VariantNotFound)
468 );
469 }
470
471 #[test]
472 fn it_can_be_created_from_vec_of_u64() {
473 let data = vec![1_u64, 2, 3];
474 let spark_data = Sparkline::default().data(data).data;
475 let expected = vec![
476 SparklineBar::from(1),
477 SparklineBar::from(2),
478 SparklineBar::from(3),
479 ];
480 assert_eq!(spark_data, expected);
481 }
482
483 #[test]
484 fn it_can_be_created_from_vec_of_option_u64() {
485 let data = vec![Some(1_u64), None, Some(3)];
486 let spark_data = Sparkline::default().data(data).data;
487 let expected = vec![
488 SparklineBar::from(1),
489 SparklineBar::from(None),
490 SparklineBar::from(3),
491 ];
492 assert_eq!(spark_data, expected);
493 }
494
495 #[test]
496 fn it_can_be_created_from_array_of_u64() {
497 let data = [1_u64, 2, 3];
498 let spark_data = Sparkline::default().data(data).data;
499 let expected = vec![
500 SparklineBar::from(1),
501 SparklineBar::from(2),
502 SparklineBar::from(3),
503 ];
504 assert_eq!(spark_data, expected);
505 }
506
507 #[test]
508 fn it_can_be_created_from_array_of_option_u64() {
509 let data = [Some(1_u64), None, Some(3)];
510 let spark_data = Sparkline::default().data(data).data;
511 let expected = vec![
512 SparklineBar::from(1),
513 SparklineBar::from(None),
514 SparklineBar::from(3),
515 ];
516 assert_eq!(spark_data, expected);
517 }
518
519 #[test]
520 fn it_can_be_created_from_slice_of_u64() {
521 let data = vec![1_u64, 2, 3];
522 let spark_data = Sparkline::default().data(&data).data;
523 let expected = vec![
524 SparklineBar::from(1),
525 SparklineBar::from(2),
526 SparklineBar::from(3),
527 ];
528 assert_eq!(spark_data, expected);
529 }
530
531 #[test]
532 fn it_can_be_created_from_slice_of_option_u64() {
533 let data = vec![Some(1_u64), None, Some(3)];
534 let spark_data = Sparkline::default().data(&data).data;
535 let expected = vec![
536 SparklineBar::from(1),
537 SparklineBar::from(None),
538 SparklineBar::from(3),
539 ];
540 assert_eq!(spark_data, expected);
541 }
542
543 fn render(widget: Sparkline<'_>, width: u16) -> Buffer {
546 let area = Rect::new(0, 0, width, 1);
547 let mut buffer = Buffer::filled(area, Cell::new("x"));
548 widget.render(area, &mut buffer);
549 buffer
550 }
551
552 #[test]
553 fn it_does_not_panic_if_max_is_zero() {
554 let widget = Sparkline::default().data([0, 0, 0]);
555 let buffer = render(widget, 6);
556 assert_eq!(buffer, Buffer::with_lines([" xxx"]));
557 }
558
559 #[test]
560 fn it_does_not_panic_if_max_is_set_to_zero() {
561 let widget = Sparkline::default().data([0, 1, 2]).max(0);
563 let buffer = render(widget, 6);
564 assert_eq!(buffer, Buffer::with_lines([" xxx"]));
565 }
566
567 #[test]
568 fn it_draws() {
569 let widget = Sparkline::default().data([0, 1, 2, 3, 4, 5, 6, 7, 8]);
570 let buffer = render(widget, 12);
571 assert_eq!(buffer, Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]));
572 }
573
574 #[test]
575 fn it_draws_double_height() {
576 let widget = Sparkline::default().data([0, 1, 2, 3, 4, 5, 6, 7, 8]);
577 let area = Rect::new(0, 0, 12, 2);
578 let mut buffer = Buffer::filled(area, Cell::new("x"));
579 widget.render(area, &mut buffer);
580 assert_eq!(buffer, Buffer::with_lines([" ▂▄▆█xxx", " ▂▄▆█████xxx"]));
581 }
582
583 #[test]
584 fn it_renders_left_to_right() {
585 let widget = Sparkline::default()
586 .data([0, 1, 2, 3, 4, 5, 6, 7, 8])
587 .direction(RenderDirection::LeftToRight);
588 let buffer = render(widget, 12);
589 assert_eq!(buffer, Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]));
590 }
591
592 #[test]
593 fn it_renders_right_to_left() {
594 let widget = Sparkline::default()
595 .data([0, 1, 2, 3, 4, 5, 6, 7, 8])
596 .direction(RenderDirection::RightToLeft);
597 let buffer = render(widget, 12);
598 assert_eq!(buffer, Buffer::with_lines(["xxx█▇▆▅▄▃▂▁ "]));
599 }
600
601 #[test]
602 fn it_renders_with_absent_value_style() {
603 let widget = Sparkline::default()
604 .absent_value_style(Style::default().fg(Color::Red))
605 .absent_value_symbol(symbols::shade::FULL)
606 .data([
607 None,
608 Some(1),
609 Some(2),
610 Some(3),
611 Some(4),
612 Some(5),
613 Some(6),
614 Some(7),
615 Some(8),
616 ]);
617 let buffer = render(widget, 12);
618 let mut expected = Buffer::with_lines(["█▁▂▃▄▅▆▇█xxx"]);
619 expected.set_style(Rect::new(0, 0, 1, 1), Style::default().fg(Color::Red));
620 assert_eq!(buffer, expected);
621 }
622
623 #[test]
624 fn it_renders_with_absent_value_style_double_height() {
625 let widget = Sparkline::default()
626 .absent_value_style(Style::default().fg(Color::Red))
627 .absent_value_symbol(symbols::shade::FULL)
628 .data([
629 None,
630 Some(1),
631 Some(2),
632 Some(3),
633 Some(4),
634 Some(5),
635 Some(6),
636 Some(7),
637 Some(8),
638 ]);
639 let area = Rect::new(0, 0, 12, 2);
640 let mut buffer = Buffer::filled(area, Cell::new("x"));
641 widget.render(area, &mut buffer);
642 let mut expected = Buffer::with_lines(["█ ▂▄▆█xxx", "█▂▄▆█████xxx"]);
643 expected.set_style(Rect::new(0, 0, 1, 2), Style::default().fg(Color::Red));
644 assert_eq!(buffer, expected);
645 }
646
647 #[test]
648 fn it_renders_with_custom_absent_value_style() {
649 let widget = Sparkline::default().absent_value_symbol('*').data([
650 None,
651 Some(1),
652 Some(2),
653 Some(3),
654 Some(4),
655 Some(5),
656 Some(6),
657 Some(7),
658 Some(8),
659 ]);
660 let buffer = render(widget, 12);
661 let expected = Buffer::with_lines(["*▁▂▃▄▅▆▇█xxx"]);
662 assert_eq!(buffer, expected);
663 }
664
665 #[test]
666 fn it_renders_with_custom_bar_styles() {
667 let widget = Sparkline::default().data(vec![
668 SparklineBar::from(Some(0)).style(Some(Style::default().fg(Color::Red))),
669 SparklineBar::from(Some(1)).style(Some(Style::default().fg(Color::Red))),
670 SparklineBar::from(Some(2)).style(Some(Style::default().fg(Color::Red))),
671 SparklineBar::from(Some(3)).style(Some(Style::default().fg(Color::Green))),
672 SparklineBar::from(Some(4)).style(Some(Style::default().fg(Color::Green))),
673 SparklineBar::from(Some(5)).style(Some(Style::default().fg(Color::Green))),
674 SparklineBar::from(Some(6)).style(Some(Style::default().fg(Color::Blue))),
675 SparklineBar::from(Some(7)).style(Some(Style::default().fg(Color::Blue))),
676 SparklineBar::from(Some(8)).style(Some(Style::default().fg(Color::Blue))),
677 ]);
678 let buffer = render(widget, 12);
679 let mut expected = Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]);
680 expected.set_style(Rect::new(0, 0, 3, 1), Style::default().fg(Color::Red));
681 expected.set_style(Rect::new(3, 0, 3, 1), Style::default().fg(Color::Green));
682 expected.set_style(Rect::new(6, 0, 3, 1), Style::default().fg(Color::Blue));
683 assert_eq!(buffer, expected);
684 }
685
686 #[test]
687 fn can_be_stylized() {
688 assert_eq!(
689 Sparkline::default()
690 .black()
691 .on_white()
692 .bold()
693 .not_dim()
694 .style,
695 Style::default()
696 .fg(Color::Black)
697 .bg(Color::White)
698 .add_modifier(Modifier::BOLD)
699 .remove_modifier(Modifier::DIM)
700 );
701 }
702
703 #[test]
704 fn render_in_minimal_buffer() {
705 let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
706 let sparkline = Sparkline::default()
707 .data([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
708 .max(10);
709 sparkline.render(buffer.area, &mut buffer);
711 assert_eq!(buffer, Buffer::with_lines([" "]));
712 }
713
714 #[test]
715 fn render_in_zero_size_buffer() {
716 let mut buffer = Buffer::empty(Rect::ZERO);
717 let sparkline = Sparkline::default()
718 .data([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
719 .max(10);
720 sparkline.render(buffer.area, &mut buffer);
722 }
723}