1use std::cmp::min;
2
3use strum::{Display, EnumString};
4
5use crate::{
6 buffer::Buffer,
7 layout::Rect,
8 style::{Style, Styled},
9 symbols::{self},
10 widgets::{block::BlockExt, Block, Widget, WidgetRef},
11};
12
13#[derive(Debug, Default, Clone, Eq, PartialEq)]
65pub struct Sparkline<'a> {
66 block: Option<Block<'a>>,
68 style: Style,
70 absent_value_style: Style,
72 absent_value_symbol: AbsentValueSymbol,
74 data: Vec<SparklineBar>,
76 max: Option<u64>,
79 bar_set: symbols::bar::Set,
81 direction: RenderDirection,
83}
84
85#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
89pub enum RenderDirection {
90 #[default]
92 LeftToRight,
93 RightToLeft,
95}
96
97impl<'a> Sparkline<'a> {
98 #[must_use = "method moves the value of self and returns the modified value"]
100 pub fn block(mut self, block: Block<'a>) -> Self {
101 self.block = Some(block);
102 self
103 }
104
105 #[must_use = "method moves the value of self and returns the modified value"]
114 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
115 self.style = style.into();
116 self
117 }
118
119 #[must_use = "method moves the value of self and returns the modified value"]
130 pub fn absent_value_style<S: Into<Style>>(mut self, style: S) -> Self {
131 self.absent_value_style = style.into();
132 self
133 }
134
135 #[must_use = "method moves the value of self and returns the modified value"]
141 pub fn absent_value_symbol(mut self, symbol: impl Into<String>) -> Self {
142 self.absent_value_symbol = AbsentValueSymbol(symbol.into());
143 self
144 }
145
146 #[must_use = "method moves the value of self and returns the modified value"]
205 pub fn data<T>(mut self, data: T) -> Self
206 where
207 T: IntoIterator,
208 T::Item: Into<SparklineBar>,
209 {
210 self.data = data.into_iter().map(Into::into).collect();
211 self
212 }
213
214 #[must_use = "method moves the value of self and returns the modified value"]
219 pub const fn max(mut self, max: u64) -> Self {
220 self.max = Some(max);
221 self
222 }
223
224 #[must_use = "method moves the value of self and returns the modified value"]
229 pub const fn bar_set(mut self, bar_set: symbols::bar::Set) -> Self {
230 self.bar_set = bar_set;
231 self
232 }
233
234 #[must_use = "method moves the value of self and returns the modified value"]
238 pub const fn direction(mut self, direction: RenderDirection) -> Self {
239 self.direction = direction;
240 self
241 }
242}
243
244#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
249pub struct SparklineBar {
250 value: Option<u64>,
254 style: Option<Style>,
258}
259
260impl SparklineBar {
261 #[must_use = "method moves the value of self and returns the modified value"]
274 pub fn style<S: Into<Option<Style>>>(mut self, style: S) -> Self {
275 self.style = style.into();
276 self
277 }
278}
279
280impl From<Option<u64>> for SparklineBar {
281 fn from(value: Option<u64>) -> Self {
282 Self { value, style: None }
283 }
284}
285
286impl From<u64> for SparklineBar {
287 fn from(value: u64) -> Self {
288 Self {
289 value: Some(value),
290 style: None,
291 }
292 }
293}
294
295impl From<&u64> for SparklineBar {
296 fn from(value: &u64) -> Self {
297 Self {
298 value: Some(*value),
299 style: None,
300 }
301 }
302}
303
304impl From<&Option<u64>> for SparklineBar {
305 fn from(value: &Option<u64>) -> Self {
306 Self {
307 value: *value,
308 style: None,
309 }
310 }
311}
312
313impl<'a> Styled for Sparkline<'a> {
314 type Item = Self;
315
316 fn style(&self) -> Style {
317 self.style
318 }
319
320 fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
321 self.style(style)
322 }
323}
324
325impl Widget for Sparkline<'_> {
326 fn render(self, area: Rect, buf: &mut Buffer) {
327 self.render_ref(area, buf);
328 }
329}
330
331impl WidgetRef for Sparkline<'_> {
332 fn render_ref(&self, area: Rect, buf: &mut Buffer) {
333 self.block.render_ref(area, buf);
334 let inner = self.block.inner_if_some(area);
335 self.render_sparkline(inner, buf);
336 }
337}
338
339#[derive(Debug, Clone, Eq, PartialEq)]
341struct AbsentValueSymbol(String);
342
343impl Default for AbsentValueSymbol {
344 fn default() -> Self {
345 Self(symbols::shade::EMPTY.to_string())
346 }
347}
348
349impl Sparkline<'_> {
350 fn render_sparkline(&self, spark_area: Rect, buf: &mut Buffer) {
351 if spark_area.is_empty() {
352 return;
353 }
354 let max_height = self
356 .max
357 .unwrap_or_else(|| self.data.iter().filter_map(|s| s.value).max().unwrap_or(1));
358
359 let max_index = min(spark_area.width as usize, self.data.len());
361
362 for (i, item) in self.data.iter().take(max_index).enumerate() {
364 let x = match self.direction {
365 RenderDirection::LeftToRight => spark_area.left() + i as u16,
366 RenderDirection::RightToLeft => spark_area.right() - i as u16 - 1,
367 };
368
369 let (mut height, symbol, style) = match item {
381 SparklineBar {
382 value: Some(value),
383 style,
384 } => {
385 let height = if max_height == 0 {
386 0
387 } else {
388 *value * u64::from(spark_area.height) * 8 / max_height
389 };
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
435#[cfg(test)]
436mod tests {
437 use strum::ParseError;
438
439 use super::*;
440 use crate::{
441 buffer::Cell,
442 style::{Color, Modifier, Stylize},
443 };
444
445 #[test]
446 fn render_direction_to_string() {
447 assert_eq!(RenderDirection::LeftToRight.to_string(), "LeftToRight");
448 assert_eq!(RenderDirection::RightToLeft.to_string(), "RightToLeft");
449 }
450
451 #[test]
452 fn render_direction_from_str() {
453 assert_eq!(
454 "LeftToRight".parse::<RenderDirection>(),
455 Ok(RenderDirection::LeftToRight)
456 );
457 assert_eq!(
458 "RightToLeft".parse::<RenderDirection>(),
459 Ok(RenderDirection::RightToLeft)
460 );
461 assert_eq!(
462 "".parse::<RenderDirection>(),
463 Err(ParseError::VariantNotFound)
464 );
465 }
466
467 #[test]
468 fn it_can_be_created_from_vec_of_u64() {
469 let data = vec![1_u64, 2, 3];
470 let spark_data = Sparkline::default().data(data).data;
471 let expected = vec![
472 SparklineBar::from(1),
473 SparklineBar::from(2),
474 SparklineBar::from(3),
475 ];
476 assert_eq!(spark_data, expected);
477 }
478
479 #[test]
480 fn it_can_be_created_from_vec_of_option_u64() {
481 let data = vec![Some(1_u64), None, Some(3)];
482 let spark_data = Sparkline::default().data(data).data;
483 let expected = vec![
484 SparklineBar::from(1),
485 SparklineBar::from(None),
486 SparklineBar::from(3),
487 ];
488 assert_eq!(spark_data, expected);
489 }
490
491 #[test]
492 fn it_can_be_created_from_array_of_u64() {
493 let data = [1_u64, 2, 3];
494 let spark_data = Sparkline::default().data(data).data;
495 let expected = vec![
496 SparklineBar::from(1),
497 SparklineBar::from(2),
498 SparklineBar::from(3),
499 ];
500 assert_eq!(spark_data, expected);
501 }
502
503 #[test]
504 fn it_can_be_created_from_array_of_option_u64() {
505 let data = [Some(1_u64), None, Some(3)];
506 let spark_data = Sparkline::default().data(data).data;
507 let expected = vec![
508 SparklineBar::from(1),
509 SparklineBar::from(None),
510 SparklineBar::from(3),
511 ];
512 assert_eq!(spark_data, expected);
513 }
514
515 #[test]
516 fn it_can_be_created_from_slice_of_u64() {
517 let data = vec![1_u64, 2, 3];
518 let spark_data = Sparkline::default().data(&data).data;
519 let expected = vec![
520 SparklineBar::from(1),
521 SparklineBar::from(2),
522 SparklineBar::from(3),
523 ];
524 assert_eq!(spark_data, expected);
525 }
526
527 #[test]
528 fn it_can_be_created_from_slice_of_option_u64() {
529 let data = vec![Some(1_u64), None, Some(3)];
530 let spark_data = Sparkline::default().data(&data).data;
531 let expected = vec![
532 SparklineBar::from(1),
533 SparklineBar::from(None),
534 SparklineBar::from(3),
535 ];
536 assert_eq!(spark_data, expected);
537 }
538
539 fn render(widget: Sparkline<'_>, width: u16) -> Buffer {
542 let area = Rect::new(0, 0, width, 1);
543 let mut buffer = Buffer::filled(area, Cell::new("x"));
544 widget.render(area, &mut buffer);
545 buffer
546 }
547
548 #[test]
549 fn it_does_not_panic_if_max_is_zero() {
550 let widget = Sparkline::default().data([0, 0, 0]);
551 let buffer = render(widget, 6);
552 assert_eq!(buffer, Buffer::with_lines([" xxx"]));
553 }
554
555 #[test]
556 fn it_does_not_panic_if_max_is_set_to_zero() {
557 #[allow(clippy::unnecessary_min_or_max)]
559 let widget = Sparkline::default().data([0, 1, 2]).max(0);
560 let buffer = render(widget, 6);
561 assert_eq!(buffer, Buffer::with_lines([" xxx"]));
562 }
563
564 #[test]
565 fn it_draws() {
566 let widget = Sparkline::default().data([0, 1, 2, 3, 4, 5, 6, 7, 8]);
567 let buffer = render(widget, 12);
568 assert_eq!(buffer, Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]));
569 }
570
571 #[test]
572 fn it_draws_double_height() {
573 let widget = Sparkline::default().data([0, 1, 2, 3, 4, 5, 6, 7, 8]);
574 let area = Rect::new(0, 0, 12, 2);
575 let mut buffer = Buffer::filled(area, Cell::new("x"));
576 widget.render(area, &mut buffer);
577 assert_eq!(buffer, Buffer::with_lines([" ▂▄▆█xxx", " ▂▄▆█████xxx"]));
578 }
579
580 #[test]
581 fn it_renders_left_to_right() {
582 let widget = Sparkline::default()
583 .data([0, 1, 2, 3, 4, 5, 6, 7, 8])
584 .direction(RenderDirection::LeftToRight);
585 let buffer = render(widget, 12);
586 assert_eq!(buffer, Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]));
587 }
588
589 #[test]
590 fn it_renders_right_to_left() {
591 let widget = Sparkline::default()
592 .data([0, 1, 2, 3, 4, 5, 6, 7, 8])
593 .direction(RenderDirection::RightToLeft);
594 let buffer = render(widget, 12);
595 assert_eq!(buffer, Buffer::with_lines(["xxx█▇▆▅▄▃▂▁ "]));
596 }
597
598 #[test]
599 fn it_renders_with_absent_value_style() {
600 let widget = Sparkline::default()
601 .absent_value_style(Style::default().fg(Color::Red))
602 .absent_value_symbol(symbols::shade::FULL)
603 .data([
604 None,
605 Some(1),
606 Some(2),
607 Some(3),
608 Some(4),
609 Some(5),
610 Some(6),
611 Some(7),
612 Some(8),
613 ]);
614 let buffer = render(widget, 12);
615 let mut expected = Buffer::with_lines(["█▁▂▃▄▅▆▇█xxx"]);
616 expected.set_style(Rect::new(0, 0, 1, 1), Style::default().fg(Color::Red));
617 assert_eq!(buffer, expected);
618 }
619
620 #[test]
621 fn it_renders_with_absent_value_style_double_height() {
622 let widget = Sparkline::default()
623 .absent_value_style(Style::default().fg(Color::Red))
624 .absent_value_symbol(symbols::shade::FULL)
625 .data([
626 None,
627 Some(1),
628 Some(2),
629 Some(3),
630 Some(4),
631 Some(5),
632 Some(6),
633 Some(7),
634 Some(8),
635 ]);
636 let area = Rect::new(0, 0, 12, 2);
637 let mut buffer = Buffer::filled(area, Cell::new("x"));
638 widget.render(area, &mut buffer);
639 let mut expected = Buffer::with_lines(["█ ▂▄▆█xxx", "█▂▄▆█████xxx"]);
640 expected.set_style(Rect::new(0, 0, 1, 2), Style::default().fg(Color::Red));
641 assert_eq!(buffer, expected);
642 }
643
644 #[test]
645 fn it_renders_with_custom_absent_value_style() {
646 let widget = Sparkline::default().absent_value_symbol('*').data([
647 None,
648 Some(1),
649 Some(2),
650 Some(3),
651 Some(4),
652 Some(5),
653 Some(6),
654 Some(7),
655 Some(8),
656 ]);
657 let buffer = render(widget, 12);
658 let expected = Buffer::with_lines(["*▁▂▃▄▅▆▇█xxx"]);
659 assert_eq!(buffer, expected);
660 }
661
662 #[test]
663 fn it_renders_with_custom_bar_styles() {
664 let widget = Sparkline::default().data(vec![
665 SparklineBar::from(Some(0)).style(Some(Style::default().fg(Color::Red))),
666 SparklineBar::from(Some(1)).style(Some(Style::default().fg(Color::Red))),
667 SparklineBar::from(Some(2)).style(Some(Style::default().fg(Color::Red))),
668 SparklineBar::from(Some(3)).style(Some(Style::default().fg(Color::Green))),
669 SparklineBar::from(Some(4)).style(Some(Style::default().fg(Color::Green))),
670 SparklineBar::from(Some(5)).style(Some(Style::default().fg(Color::Green))),
671 SparklineBar::from(Some(6)).style(Some(Style::default().fg(Color::Blue))),
672 SparklineBar::from(Some(7)).style(Some(Style::default().fg(Color::Blue))),
673 SparklineBar::from(Some(8)).style(Some(Style::default().fg(Color::Blue))),
674 ]);
675 let buffer = render(widget, 12);
676 let mut expected = Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]);
677 expected.set_style(Rect::new(0, 0, 3, 1), Style::default().fg(Color::Red));
678 expected.set_style(Rect::new(3, 0, 3, 1), Style::default().fg(Color::Green));
679 expected.set_style(Rect::new(6, 0, 3, 1), Style::default().fg(Color::Blue));
680 assert_eq!(buffer, expected);
681 }
682
683 #[test]
684 fn can_be_stylized() {
685 assert_eq!(
686 Sparkline::default()
687 .black()
688 .on_white()
689 .bold()
690 .not_dim()
691 .style,
692 Style::default()
693 .fg(Color::Black)
694 .bg(Color::White)
695 .add_modifier(Modifier::BOLD)
696 .remove_modifier(Modifier::DIM)
697 );
698 }
699}