use crate::render::Cell;
use crate::style::Color;
use crate::widget::theme::DISABLED_FG;
use crate::widget::traits::{RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
const SPARK_CHARS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SparklineStyle {
#[default]
Block,
Braille,
Ascii,
}
impl SparklineStyle {
fn chars(&self) -> &[char] {
match self {
SparklineStyle::Block => &SPARK_CHARS,
SparklineStyle::Braille => &['⠀', '⣀', '⣤', '⣶', '⣿', '⣿', '⣿', '⣿'],
SparklineStyle::Ascii => &['_', '.', '-', '=', '+', '*', '#', '@'],
}
}
}
#[derive(Clone)]
pub struct Sparkline {
data: Vec<f64>,
max: Option<f64>,
min: Option<f64>,
style: SparklineStyle,
fg: Option<Color>,
bg: Option<Color>,
show_bounds: bool,
props: WidgetProps,
}
impl Sparkline {
pub fn new<I>(data: I) -> Self
where
I: IntoIterator<Item = f64>,
{
Self {
data: data.into_iter().collect(),
max: None,
min: None,
style: SparklineStyle::default(),
fg: None,
bg: None,
show_bounds: false,
props: WidgetProps::new(),
}
}
pub fn empty() -> Self {
Self::new(Vec::new())
}
pub fn data<I>(mut self, data: I) -> Self
where
I: IntoIterator<Item = f64>,
{
self.data = data.into_iter().collect();
self
}
pub fn max(mut self, max: f64) -> Self {
self.max = Some(max);
self
}
pub fn min(mut self, min: f64) -> Self {
self.min = Some(min);
self
}
pub fn style(mut self, style: SparklineStyle) -> Self {
self.style = style;
self
}
pub fn fg(mut self, color: Color) -> Self {
self.fg = Some(color);
self
}
pub fn bg(mut self, color: Color) -> Self {
self.bg = Some(color);
self
}
pub fn show_bounds(mut self, show: bool) -> Self {
self.show_bounds = show;
self
}
pub fn push(&mut self, value: f64) {
self.data.push(value);
}
pub fn push_shift(&mut self, value: f64, max_len: usize) {
self.data.push(value);
while self.data.len() > max_len {
self.data.remove(0);
}
}
pub fn clear(&mut self) {
self.data.clear();
}
pub fn get_data(&self) -> &[f64] {
&self.data
}
fn calc_bounds(&self) -> (f64, f64) {
let data_min = self.data.iter().cloned().fold(f64::INFINITY, f64::min);
let data_max = self.data.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let min = self.min.unwrap_or(data_min.min(0.0));
let max = self.max.unwrap_or(data_max);
if (max - min).abs() < f64::EPSILON {
(min - 1.0, max + 1.0)
} else {
(min, max)
}
}
fn value_to_index(&self, value: f64, min: f64, max: f64) -> usize {
let range = max - min;
if range.abs() < f64::EPSILON {
return 4; }
let normalized = ((value - min) / range).clamp(0.0, 1.0);
let index = (normalized * 7.0).round() as usize;
index.min(7)
}
}
impl Default for Sparkline {
fn default() -> Self {
Self::empty()
}
}
impl View for Sparkline {
crate::impl_view_meta!("Sparkline");
fn render(&self, ctx: &mut RenderContext) {
let area = ctx.area;
if area.width == 0 || area.height == 0 {
return;
}
let fg = self.fg.unwrap_or(Color::CYAN);
let chars = self.style.chars();
let (min, max) = self.calc_bounds();
let bounds_width = if self.show_bounds {
let max_str = format!("{:.0}", max);
max_str.len() + 1
} else {
0
};
let available_width = (area.width as usize).saturating_sub(bounds_width);
let data_slice = if self.data.len() > available_width {
&self.data[self.data.len() - available_width..]
} else {
&self.data[..]
};
let mut x = 0u16;
if self.show_bounds {
let max_str = format!("{:.0}", max);
for ch in max_str.chars() {
if x < area.width {
let mut cell = Cell::new(ch);
cell.fg = Some(DISABLED_FG);
ctx.set(x, 0, cell);
x += 1;
}
}
ctx.set(x, 0, Cell::new(' '));
x += 1;
}
for &value in data_slice {
if x >= area.width {
break;
}
let char_index = self.value_to_index(value, min, max);
let ch = chars[char_index];
let mut cell = Cell::new(ch);
cell.fg = Some(fg);
if let Some(bg) = self.bg {
cell.bg = Some(bg);
}
ctx.set(x, 0, cell);
x += 1;
}
let remaining_data = available_width.saturating_sub(data_slice.len());
for _ in 0..remaining_data {
if x >= area.width {
break;
}
let mut cell = Cell::new(chars[0]); cell.fg = Some(Color::rgb(50, 50, 50));
if let Some(bg) = self.bg {
cell.bg = Some(bg);
}
ctx.set(x, 0, cell);
x += 1;
}
}
}
impl_styled_view!(Sparkline);
impl_props_builders!(Sparkline);
pub fn sparkline<I>(data: I) -> Sparkline
where
I: IntoIterator<Item = f64>,
{
Sparkline::new(data)
}