use crate::buffer::ScreenBuffer;
use crate::cell::Cell;
use crate::geometry::Rect;
use crate::style::Style;
use super::Widget;
const BAR_CHARS: &[&str] = &["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
pub struct Sparkline {
data: Vec<f32>,
max_width: usize,
height: u16,
chart_style: Style,
}
impl Sparkline {
pub fn new(data: Vec<f32>) -> Self {
Self {
data,
max_width: 80,
height: 1,
chart_style: Style::default(),
}
}
#[must_use]
pub fn with_max_width(mut self, width: usize) -> Self {
self.max_width = width;
self
}
#[must_use]
pub fn with_height(mut self, height: u16) -> Self {
self.height = height.max(1);
self
}
#[must_use]
pub fn with_chart_style(mut self, style: Style) -> Self {
self.chart_style = style;
self
}
pub fn push(&mut self, value: f32) {
self.data.push(value);
if self.data.len() > self.max_width {
self.data.remove(0);
}
}
pub fn set_data(&mut self, data: Vec<f32>) {
self.data = data;
}
pub fn data(&self) -> &[f32] {
&self.data
}
pub fn clear(&mut self) {
self.data.clear();
}
fn value_to_bar_index(value: f32, min: f32, range: f32) -> usize {
if range <= 0.0 {
return BAR_CHARS.len() / 2; }
let normalized = ((value - min) / range).clamp(0.0, 1.0);
let idx = (normalized * (BAR_CHARS.len() - 1) as f32).round() as usize;
idx.min(BAR_CHARS.len() - 1)
}
}
impl Widget for Sparkline {
fn render(&self, area: Rect, buf: &mut ScreenBuffer) {
if area.size.width == 0 || area.size.height == 0 || self.data.is_empty() {
return;
}
let w = area.size.width as usize;
let x0 = area.position.x;
let y = area.position.y;
let display_count = self.data.len().min(w).min(self.max_width);
let start = self.data.len().saturating_sub(display_count);
let visible = &self.data[start..];
let mut min = f32::MAX;
let mut max = f32::MIN;
for &v in visible {
if v < min {
min = v;
}
if v > max {
max = v;
}
}
let range = max - min;
for (i, &value) in visible.iter().enumerate() {
if i >= w {
break;
}
let bar_idx = Self::value_to_bar_index(value, min, range);
let ch = BAR_CHARS[bar_idx];
buf.set(x0 + i as u16, y, Cell::new(ch, self.chart_style.clone()));
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::geometry::Size;
#[test]
fn create_with_data() {
let s = Sparkline::new(vec![1.0, 2.0, 3.0]);
assert_eq!(s.data().len(), 3);
}
#[test]
fn render_bars() {
let s = Sparkline::new(vec![0.0, 0.5, 1.0]);
let mut buf = ScreenBuffer::new(Size::new(10, 1));
s.render(Rect::new(0, 0, 10, 1), &mut buf);
let first = buf.get(0, 0).unwrap().grapheme.clone();
let last = buf.get(2, 0).unwrap().grapheme.clone();
assert_eq!(first, "▁"); assert_eq!(last, "█"); }
#[test]
fn push_drops_oldest() {
let mut s = Sparkline::new(vec![1.0, 2.0, 3.0]).with_max_width(3);
s.push(4.0);
assert_eq!(s.data().len(), 3);
assert_eq!(s.data()[0], 2.0); assert_eq!(s.data()[2], 4.0);
}
#[test]
fn set_data_replaces() {
let mut s = Sparkline::new(vec![1.0, 2.0]);
s.set_data(vec![10.0, 20.0, 30.0]);
assert_eq!(s.data().len(), 3);
assert_eq!(s.data()[0], 10.0);
}
#[test]
fn clear_removes_all() {
let mut s = Sparkline::new(vec![1.0, 2.0, 3.0]);
s.clear();
assert!(s.data().is_empty());
}
#[test]
fn empty_renders_blank() {
let s = Sparkline::new(vec![]);
let mut buf = ScreenBuffer::new(Size::new(10, 1));
s.render(Rect::new(0, 0, 10, 1), &mut buf);
assert_eq!(buf.get(0, 0).unwrap().grapheme, " ");
}
#[test]
fn scaling() {
let s = Sparkline::new(vec![5.0, 5.0, 5.0]);
let mut buf = ScreenBuffer::new(Size::new(10, 1));
s.render(Rect::new(0, 0, 10, 1), &mut buf);
let ch = buf.get(0, 0).unwrap().grapheme.clone();
assert!(BAR_CHARS.contains(&ch.as_str()));
}
#[test]
fn max_width_respected() {
let s = Sparkline::new(vec![1.0; 100]).with_max_width(5);
let mut buf = ScreenBuffer::new(Size::new(3, 1));
s.render(Rect::new(0, 0, 3, 1), &mut buf);
let ch = buf.get(0, 0).unwrap().grapheme.clone();
assert!(BAR_CHARS.contains(&ch.as_str()));
}
#[test]
fn custom_style_applied() {
let style = Style::default().bold(true);
let s = Sparkline::new(vec![1.0, 2.0]).with_chart_style(style);
let mut buf = ScreenBuffer::new(Size::new(10, 1));
s.render(Rect::new(0, 0, 10, 1), &mut buf);
assert!(buf.get(0, 0).unwrap().style.bold);
}
#[test]
fn zero_values() {
let s = Sparkline::new(vec![0.0, 0.0, 0.0]);
let mut buf = ScreenBuffer::new(Size::new(5, 1));
s.render(Rect::new(0, 0, 5, 1), &mut buf);
let ch = buf.get(0, 0).unwrap().grapheme.clone();
assert!(BAR_CHARS.contains(&ch.as_str()));
}
#[test]
fn negative_values() {
let s = Sparkline::new(vec![-10.0, 0.0, 10.0]);
let mut buf = ScreenBuffer::new(Size::new(5, 1));
s.render(Rect::new(0, 0, 5, 1), &mut buf);
let first = buf.get(0, 0).unwrap().grapheme.clone();
let last = buf.get(2, 0).unwrap().grapheme.clone();
assert_eq!(first, "▁"); assert_eq!(last, "█"); }
#[test]
fn single_data_point() {
let s = Sparkline::new(vec![42.0]);
let mut buf = ScreenBuffer::new(Size::new(5, 1));
s.render(Rect::new(0, 0, 5, 1), &mut buf);
let ch = buf.get(0, 0).unwrap().grapheme.clone();
assert!(BAR_CHARS.contains(&ch.as_str()));
}
}