use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::cmp::min;
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
use ratatui_core::style::{Style, Styled};
use ratatui_core::symbols;
use ratatui_core::widgets::Widget;
use strum::{Display, EnumString};
use crate::block::{Block, BlockExt};
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct Sparkline<'a> {
block: Option<Block<'a>>,
style: Style,
absent_value_style: Style,
absent_value_symbol: AbsentValueSymbol,
data: Vec<SparklineBar>,
max: Option<u64>,
bar_set: symbols::bar::Set<'a>,
direction: RenderDirection,
}
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum RenderDirection {
#[default]
LeftToRight,
RightToLeft,
}
impl<'a> Sparkline<'a> {
#[must_use = "method moves the value of self and returns the modified value"]
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn absent_value_style<S: Into<Style>>(mut self, style: S) -> Self {
self.absent_value_style = style.into();
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn absent_value_symbol(mut self, symbol: impl Into<String>) -> Self {
self.absent_value_symbol = AbsentValueSymbol(symbol.into());
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn data<T>(mut self, data: T) -> Self
where
T: IntoIterator,
T::Item: Into<SparklineBar>,
{
self.data = data.into_iter().map(Into::into).collect();
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn max(mut self, max: u64) -> Self {
self.max = Some(max);
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn bar_set(mut self, bar_set: symbols::bar::Set<'a>) -> Self {
self.bar_set = bar_set;
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn direction(mut self, direction: RenderDirection) -> Self {
self.direction = direction;
self
}
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
pub struct SparklineBar {
value: Option<u64>,
style: Option<Style>,
}
impl SparklineBar {
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Option<Style>>>(mut self, style: S) -> Self {
self.style = style.into();
self
}
}
impl From<Option<u64>> for SparklineBar {
fn from(value: Option<u64>) -> Self {
Self { value, style: None }
}
}
impl From<u64> for SparklineBar {
fn from(value: u64) -> Self {
Self {
value: Some(value),
style: None,
}
}
}
impl From<&u64> for SparklineBar {
fn from(value: &u64) -> Self {
Self {
value: Some(*value),
style: None,
}
}
}
impl From<&Option<u64>> for SparklineBar {
fn from(value: &Option<u64>) -> Self {
Self {
value: *value,
style: None,
}
}
}
impl Styled for Sparkline<'_> {
type Item = Self;
fn style(&self) -> Style {
self.style
}
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}
impl Widget for Sparkline<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
Widget::render(&self, area, buf);
}
}
impl Widget for &Sparkline<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.block.as_ref().render(area, buf);
let inner = self.block.inner_if_some(area);
self.render_sparkline(inner, buf);
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
struct AbsentValueSymbol(String);
impl Default for AbsentValueSymbol {
fn default() -> Self {
Self(symbols::shade::EMPTY.to_string())
}
}
impl Sparkline<'_> {
fn render_sparkline(&self, spark_area: Rect, buf: &mut Buffer) {
if spark_area.is_empty() {
return;
}
let max_height = self
.max
.unwrap_or_else(|| self.data.iter().filter_map(|s| s.value).max().unwrap_or(1));
let max_index = min(spark_area.width as usize, self.data.len());
for (i, item) in self.data.iter().take(max_index).enumerate() {
let x = match self.direction {
RenderDirection::LeftToRight => spark_area.left() + i as u16,
RenderDirection::RightToLeft => spark_area.right() - i as u16 - 1,
};
let (mut height, symbol, style) = match item {
SparklineBar {
value: Some(value),
style,
} => {
let height = if max_height == 0 {
0
} else {
*value * u64::from(spark_area.height) * 8 / max_height
};
(height, None, *style)
}
_ => (
u64::from(spark_area.height) * 8,
Some(self.absent_value_symbol.0.as_str()),
Some(self.absent_value_style),
),
};
for j in (0..spark_area.height).rev() {
let symbol = symbol.unwrap_or_else(|| self.symbol_for_height(height));
if height > 8 {
height -= 8;
} else {
height = 0;
}
buf[(x, spark_area.top() + j)]
.set_symbol(symbol)
.set_style(self.style.patch(style.unwrap_or_default()));
}
}
}
const fn symbol_for_height(&self, height: u64) -> &str {
match height {
0 => self.bar_set.empty,
1 => self.bar_set.one_eighth,
2 => self.bar_set.one_quarter,
3 => self.bar_set.three_eighths,
4 => self.bar_set.half,
5 => self.bar_set.five_eighths,
6 => self.bar_set.three_quarters,
7 => self.bar_set.seven_eighths,
_ => self.bar_set.full,
}
}
}
#[cfg(test)]
mod tests {
use alloc::vec;
use ratatui_core::buffer::Cell;
use ratatui_core::style::{Color, Modifier, Stylize};
use strum::ParseError;
use super::*;
#[test]
fn render_direction_to_string() {
assert_eq!(RenderDirection::LeftToRight.to_string(), "LeftToRight");
assert_eq!(RenderDirection::RightToLeft.to_string(), "RightToLeft");
}
#[test]
fn render_direction_from_str() {
assert_eq!(
"LeftToRight".parse::<RenderDirection>(),
Ok(RenderDirection::LeftToRight)
);
assert_eq!(
"RightToLeft".parse::<RenderDirection>(),
Ok(RenderDirection::RightToLeft)
);
assert_eq!(
"".parse::<RenderDirection>(),
Err(ParseError::VariantNotFound)
);
}
#[test]
fn it_can_be_created_from_vec_of_u64() {
let data = vec![1_u64, 2, 3];
let spark_data = Sparkline::default().data(data).data;
let expected = vec![
SparklineBar::from(1),
SparklineBar::from(2),
SparklineBar::from(3),
];
assert_eq!(spark_data, expected);
}
#[test]
fn it_can_be_created_from_vec_of_option_u64() {
let data = vec![Some(1_u64), None, Some(3)];
let spark_data = Sparkline::default().data(data).data;
let expected = vec![
SparklineBar::from(1),
SparklineBar::from(None),
SparklineBar::from(3),
];
assert_eq!(spark_data, expected);
}
#[test]
fn it_can_be_created_from_array_of_u64() {
let data = [1_u64, 2, 3];
let spark_data = Sparkline::default().data(data).data;
let expected = vec![
SparklineBar::from(1),
SparklineBar::from(2),
SparklineBar::from(3),
];
assert_eq!(spark_data, expected);
}
#[test]
fn it_can_be_created_from_array_of_option_u64() {
let data = [Some(1_u64), None, Some(3)];
let spark_data = Sparkline::default().data(data).data;
let expected = vec![
SparklineBar::from(1),
SparklineBar::from(None),
SparklineBar::from(3),
];
assert_eq!(spark_data, expected);
}
#[test]
fn it_can_be_created_from_slice_of_u64() {
let data = vec![1_u64, 2, 3];
let spark_data = Sparkline::default().data(&data).data;
let expected = vec![
SparklineBar::from(1),
SparklineBar::from(2),
SparklineBar::from(3),
];
assert_eq!(spark_data, expected);
}
#[test]
fn it_can_be_created_from_slice_of_option_u64() {
let data = vec![Some(1_u64), None, Some(3)];
let spark_data = Sparkline::default().data(&data).data;
let expected = vec![
SparklineBar::from(1),
SparklineBar::from(None),
SparklineBar::from(3),
];
assert_eq!(spark_data, expected);
}
fn render(widget: Sparkline<'_>, width: u16) -> Buffer {
let area = Rect::new(0, 0, width, 1);
let mut buffer = Buffer::filled(area, Cell::new("x"));
widget.render(area, &mut buffer);
buffer
}
#[test]
fn it_does_not_panic_if_max_is_zero() {
let widget = Sparkline::default().data([0, 0, 0]);
let buffer = render(widget, 6);
assert_eq!(buffer, Buffer::with_lines([" xxx"]));
}
#[test]
fn it_does_not_panic_if_max_is_set_to_zero() {
let widget = Sparkline::default().data([0, 1, 2]).max(0);
let buffer = render(widget, 6);
assert_eq!(buffer, Buffer::with_lines([" xxx"]));
}
#[test]
fn it_draws() {
let widget = Sparkline::default().data([0, 1, 2, 3, 4, 5, 6, 7, 8]);
let buffer = render(widget, 12);
assert_eq!(buffer, Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]));
}
#[test]
fn it_draws_double_height() {
let widget = Sparkline::default().data([0, 1, 2, 3, 4, 5, 6, 7, 8]);
let area = Rect::new(0, 0, 12, 2);
let mut buffer = Buffer::filled(area, Cell::new("x"));
widget.render(area, &mut buffer);
assert_eq!(buffer, Buffer::with_lines([" ▂▄▆█xxx", " ▂▄▆█████xxx"]));
}
#[test]
fn it_renders_left_to_right() {
let widget = Sparkline::default()
.data([0, 1, 2, 3, 4, 5, 6, 7, 8])
.direction(RenderDirection::LeftToRight);
let buffer = render(widget, 12);
assert_eq!(buffer, Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]));
}
#[test]
fn it_renders_right_to_left() {
let widget = Sparkline::default()
.data([0, 1, 2, 3, 4, 5, 6, 7, 8])
.direction(RenderDirection::RightToLeft);
let buffer = render(widget, 12);
assert_eq!(buffer, Buffer::with_lines(["xxx█▇▆▅▄▃▂▁ "]));
}
#[test]
fn it_renders_with_absent_value_style() {
let widget = Sparkline::default()
.absent_value_style(Style::default().fg(Color::Red))
.absent_value_symbol(symbols::shade::FULL)
.data([
None,
Some(1),
Some(2),
Some(3),
Some(4),
Some(5),
Some(6),
Some(7),
Some(8),
]);
let buffer = render(widget, 12);
let mut expected = Buffer::with_lines(["█▁▂▃▄▅▆▇█xxx"]);
expected.set_style(Rect::new(0, 0, 1, 1), Style::default().fg(Color::Red));
assert_eq!(buffer, expected);
}
#[test]
fn it_renders_with_absent_value_style_double_height() {
let widget = Sparkline::default()
.absent_value_style(Style::default().fg(Color::Red))
.absent_value_symbol(symbols::shade::FULL)
.data([
None,
Some(1),
Some(2),
Some(3),
Some(4),
Some(5),
Some(6),
Some(7),
Some(8),
]);
let area = Rect::new(0, 0, 12, 2);
let mut buffer = Buffer::filled(area, Cell::new("x"));
widget.render(area, &mut buffer);
let mut expected = Buffer::with_lines(["█ ▂▄▆█xxx", "█▂▄▆█████xxx"]);
expected.set_style(Rect::new(0, 0, 1, 2), Style::default().fg(Color::Red));
assert_eq!(buffer, expected);
}
#[test]
fn it_renders_with_custom_absent_value_style() {
let widget = Sparkline::default().absent_value_symbol('*').data([
None,
Some(1),
Some(2),
Some(3),
Some(4),
Some(5),
Some(6),
Some(7),
Some(8),
]);
let buffer = render(widget, 12);
let expected = Buffer::with_lines(["*▁▂▃▄▅▆▇█xxx"]);
assert_eq!(buffer, expected);
}
#[test]
fn it_renders_with_custom_bar_styles() {
let widget = Sparkline::default().data(vec![
SparklineBar::from(Some(0)).style(Some(Style::default().fg(Color::Red))),
SparklineBar::from(Some(1)).style(Some(Style::default().fg(Color::Red))),
SparklineBar::from(Some(2)).style(Some(Style::default().fg(Color::Red))),
SparklineBar::from(Some(3)).style(Some(Style::default().fg(Color::Green))),
SparklineBar::from(Some(4)).style(Some(Style::default().fg(Color::Green))),
SparklineBar::from(Some(5)).style(Some(Style::default().fg(Color::Green))),
SparklineBar::from(Some(6)).style(Some(Style::default().fg(Color::Blue))),
SparklineBar::from(Some(7)).style(Some(Style::default().fg(Color::Blue))),
SparklineBar::from(Some(8)).style(Some(Style::default().fg(Color::Blue))),
]);
let buffer = render(widget, 12);
let mut expected = Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]);
expected.set_style(Rect::new(0, 0, 3, 1), Style::default().fg(Color::Red));
expected.set_style(Rect::new(3, 0, 3, 1), Style::default().fg(Color::Green));
expected.set_style(Rect::new(6, 0, 3, 1), Style::default().fg(Color::Blue));
assert_eq!(buffer, expected);
}
#[test]
fn can_be_stylized() {
assert_eq!(
Sparkline::default()
.black()
.on_white()
.bold()
.not_dim()
.style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::DIM)
);
}
#[test]
fn render_in_minimal_buffer() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
let sparkline = Sparkline::default()
.data([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
.max(10);
sparkline.render(buffer.area, &mut buffer);
assert_eq!(buffer, Buffer::with_lines([" "]));
}
#[test]
fn render_in_zero_size_buffer() {
let mut buffer = Buffer::empty(Rect::ZERO);
let sparkline = Sparkline::default()
.data([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
.max(10);
sparkline.render(buffer.area, &mut buffer);
}
}