use ratatui::{
buffer::Buffer,
layout::Rect,
style::Style,
widgets::{Block, Widget},
};
#[derive(Debug)]
pub struct Sparkline {
width: usize,
height: usize,
data: Vec<f64>,
style: Style,
block: Option<Block<'static>>,
}
impl Sparkline {
pub fn new(width: usize, height: usize) -> Self {
Self {
width: width.max(1),
height: height.max(1),
data: Vec::new(),
style: Style::default(),
block: None,
}
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn block(mut self, block: Block<'static>) -> Self {
self.block = Some(block);
self
}
pub fn add_data_point(&mut self, value: f64) {
self.data.push(value);
if self.data.len() > self.width {
self.data.remove(0);
}
}
pub fn set_data(&mut self, data: Vec<f64>) {
self.data = data;
if self.data.len() > self.width {
let start = self.data.len() - self.width;
self.data = self.data[start..].to_vec();
}
}
pub fn clear(&mut self) {
self.data.clear();
}
pub fn render_string(&self) -> String {
if self.data.is_empty() {
return " ".repeat(self.width);
}
let min_val = self.data.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let max_val = self.data.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let range = if (max_val - min_val).abs() < f64::EPSILON {
1.0
} else {
max_val - min_val
};
let chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
let levels = chars.len() as f64;
let mut result = String::new();
for &value in &self.data {
let normalized = ((value - min_val) / range).clamp(0.0, 1.0);
let level = (normalized * (levels - 1.0)).round() as usize;
result.push(chars[level]);
}
while result.chars().count() < self.width {
result.push(' ');
}
result
}
pub fn render_with_labels(&self, title: &str) -> String {
let sparkline = self.render_string();
let min_val = self.data.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let max_val = self.data.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
format!(
"{}: {} (min: {:.1}, max: {:.1})",
title, sparkline, min_val, max_val
)
}
pub fn get_data(&self) -> &[f64] {
&self.data
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
pub fn set_width(&mut self, width: usize) {
self.width = width.max(1);
if self.data.len() > self.width {
let start = self.data.len() - self.width;
self.data = self.data[start..].to_vec();
}
}
pub fn set_height(&mut self, height: usize) {
self.height = height.max(1);
}
pub fn render_widget(&self, area: Rect, buf: &mut Buffer) {
let inner_area = if let Some(ref block) = self.block {
let inner = block.inner(area);
block.render(area, buf);
inner
} else {
area
};
if self.data.is_empty() || inner_area.width == 0 || inner_area.height == 0 {
return;
}
let sparkline_str = self.render_string();
let chars: Vec<char> = sparkline_str.chars().collect();
let start_x = inner_area.left();
let y = inner_area.top() + inner_area.height / 2;
for (i, ch) in chars.iter().enumerate() {
let x = start_x + i as u16;
if x >= inner_area.right() {
break;
}
if y >= inner_area.top() && y < inner_area.bottom() {
buf[(x, y)]
.set_symbol(&ch.to_string())
.set_style(self.style);
}
}
}
}
impl Default for Sparkline {
fn default() -> Self {
Self::new(20, 1)
}
}
impl Widget for Sparkline {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_widget(area, buf);
}
}
impl Widget for &Sparkline {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_widget(area, buf);
}
}
#[derive(Debug)]
pub struct SparklineCollection {
sparklines: std::collections::HashMap<String, Sparkline>,
default_width: usize,
default_height: usize,
}
impl SparklineCollection {
pub fn new(default_width: usize, default_height: usize) -> Self {
Self {
sparklines: std::collections::HashMap::new(),
default_width,
default_height,
}
}
pub fn update(&mut self, key: &str, value: f64) {
let sparkline = self.sparklines.entry(key.to_string())
.or_insert_with(|| Sparkline::new(self.default_width, self.default_height));
sparkline.add_data_point(value);
}
pub fn get(&self, key: &str) -> Option<&Sparkline> {
self.sparklines.get(key)
}
pub fn get_mut(&mut self, key: &str) -> Option<&mut Sparkline> {
self.sparklines.get_mut(key)
}
pub fn remove(&mut self, key: &str) -> Option<Sparkline> {
self.sparklines.remove(key)
}
pub fn clear(&mut self) {
self.sparklines.clear();
}
pub fn keys(&self) -> impl Iterator<Item = &String> {
self.sparklines.keys()
}
pub fn render_all(&self) -> String {
let mut result = String::new();
let mut keys: Vec<_> = self.sparklines.keys().collect();
keys.sort();
for key in keys {
if let Some(sparkline) = self.sparklines.get(key) {
result.push_str(&sparkline.render_with_labels(key));
result.push('\n');
}
}
result
}
pub fn len(&self) -> usize {
self.sparklines.len()
}
pub fn is_empty(&self) -> bool {
self.sparklines.is_empty()
}
}
impl Default for SparklineCollection {
fn default() -> Self {
Self::new(20, 1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sparkline_creation() {
let sparkline = Sparkline::new(10, 1);
assert_eq!(sparkline.width(), 10);
assert_eq!(sparkline.height(), 1);
assert!(sparkline.is_empty());
}
#[test]
fn test_sparkline_add_data() {
let mut sparkline = Sparkline::new(5, 1);
sparkline.add_data_point(1.0);
sparkline.add_data_point(2.0);
sparkline.add_data_point(3.0);
assert_eq!(sparkline.len(), 3);
assert_eq!(sparkline.get_data(), &[1.0, 2.0, 3.0]);
}
#[test]
fn test_sparkline_width_limit() {
let mut sparkline = Sparkline::new(3, 1);
for i in 1..=5 {
sparkline.add_data_point(i as f64);
}
assert_eq!(sparkline.len(), 3);
assert_eq!(sparkline.get_data(), &[3.0, 4.0, 5.0]);
}
#[test]
fn test_sparkline_render() {
let mut sparkline = Sparkline::new(5, 1);
sparkline.set_data(vec![1.0, 2.0, 3.0]);
let rendered = sparkline.render_string(); assert_eq!(rendered.chars().count(), 5); }
#[test]
fn test_sparkline_collection() {
let mut collection = SparklineCollection::new(10, 1);
collection.update("test1", 5.0);
collection.update("test2", 10.0);
assert_eq!(collection.len(), 2);
assert!(collection.get("test1").is_some());
assert!(collection.get("test2").is_some());
}
}