use super::theme::Gradient;
use ratatui::style::Style;
use ratatui::text::{Line, Span};
const LEFT_COL_BITS: [u16; 4] = [0x40, 0x04, 0x02, 0x01];
const DOT_BITS: [[u16; 4]; 2] = [[0x01, 0x02, 0x04, 0x40], [0x08, 0x10, 0x20, 0x80]];
const BRAILLE_BASE: u16 = 0x2800;
fn braille(bits: u16) -> char {
char::from_u32((BRAILLE_BASE | bits) as u32).unwrap_or(' ')
}
pub fn sparkline(
values: &[f64],
width: usize,
max: f64,
gradient: Gradient,
now_index: Option<usize>,
) -> Line<'static> {
let baseline = braille(LEFT_COL_BITS[0]);
let base_style = Style::default().fg(gradient.baseline());
let now_style = Style::default()
.fg(ratatui::style::Color::White)
.add_modifier(ratatui::style::Modifier::BOLD);
if width == 0 {
return Line::default();
}
if values.is_empty() {
return Line::from(vec![Span::styled(
baseline.to_string().repeat(width),
base_style,
)]);
}
let start = values.len().saturating_sub(width);
let visible = &values[start..];
let hi = if max > 0.0 {
max
} else {
let peak = visible.iter().cloned().fold(0.0f64, f64::max);
if peak <= 0.0 { 1.0 } else { peak * 1.1 }
};
let lead = width - visible.len();
let now_col = now_index
.and_then(|n| n.checked_sub(start))
.map(|n| lead + n);
let mut spans = Vec::with_capacity(width);
if lead > 0 {
spans.push(Span::styled(baseline.to_string().repeat(lead), base_style));
}
for (i, &v) in visible.iter().enumerate() {
let is_now = now_col == Some(lead + i);
if v <= 0.0 {
spans.push(Span::styled(
baseline.to_string(),
if is_now { now_style } else { base_style },
));
continue;
}
let ratio = (v / hi).clamp(0.0, 1.0);
let filled = ((ratio * 4.0).round() as usize).clamp(1, 4);
let bits = LEFT_COL_BITS[..filled].iter().fold(0u16, |a, b| a | b);
let style = if is_now {
now_style
} else {
Style::default().fg(gradient.color(ratio))
};
spans.push(Span::styled(braille(bits).to_string(), style));
}
Line::from(spans)
}
pub fn line_chart(
values: &[f64],
total_width: usize,
rows: usize,
max: f64,
gradient: Gradient,
axis_width: Option<usize>,
) -> Vec<Line<'static>> {
let hi = if max > 0.0 {
max
} else {
crate::util::nice_max(values.iter().cloned().fold(1.0f64, f64::max))
};
let label_len = format!("{}", hi.ceil() as i64).len().max(1);
let axis_w = axis_width.unwrap_or(label_len + 2);
let cols = total_width.saturating_sub(axis_w).max(4);
let dot_w = cols * 2;
let dot_h = rows * 4;
let mut grid = vec![vec![0u16; cols]; rows];
let mut col_ratio = vec![0.0f64; cols];
let start = values.len().saturating_sub(dot_w);
let visible = &values[start..];
let set_dot = |grid: &mut Vec<Vec<u16>>, x: usize, y: usize| {
if x < dot_w && y < dot_h {
grid[y / 4][x / 2] |= DOT_BITS[x % 2][y % 4];
}
};
let mut prev_y: Option<usize> = None;
for (x, &v) in visible.iter().enumerate() {
let ratio = (v / hi).clamp(0.0, 1.0);
let y = ((1.0 - ratio) * (dot_h.saturating_sub(1)) as f64).round() as usize;
set_dot(&mut grid, x, y);
let tc = x / 2;
if tc < cols && ratio > col_ratio[tc] {
col_ratio[tc] = ratio;
}
if let Some(py) = prev_y
&& py != y
{
let (lo, hi_y) = (py.min(y), py.max(y));
for yy in (lo + 1)..hi_y {
let frac = (yy as f64 - py as f64) / (y as f64 - py as f64);
let xx = (x as f64 - 1.0 + frac).round().max(0.0) as usize;
set_dot(&mut grid, xx, yy);
}
}
prev_y = Some(y);
}
let axis_style = Style::default().fg(ratatui::style::Color::Indexed(238));
let label_style = Style::default().fg(ratatui::style::Color::Indexed(239));
let empty_style = Style::default().fg(ratatui::style::Color::Indexed(236));
let mut out = Vec::with_capacity(rows + 1);
for (r, row) in grid.iter().enumerate() {
let tick = (hi * (rows - r) as f64 / rows as f64).round() as i64;
let mut spans = vec![
Span::styled(format!("{tick:>label_len$}"), label_style),
Span::styled(" ┤".to_string(), axis_style),
];
for (c, &bits) in row.iter().enumerate() {
let style = if bits == 0 {
empty_style
} else {
Style::default().fg(gradient.color(col_ratio[c]))
};
spans.push(Span::styled(braille(bits).to_string(), style));
}
out.push(Line::from(spans));
}
out.push(Line::from(vec![
Span::styled(format!("{:>label_len$}", 0), label_style),
Span::styled(format!(" └{}", "─".repeat(cols)), axis_style),
]));
out
}
#[derive(Debug, Clone)]
pub struct History {
values: Vec<f64>,
capacity: usize,
}
impl History {
pub fn new(capacity: usize) -> Self {
History {
values: Vec::with_capacity(capacity),
capacity,
}
}
pub fn push(&mut self, v: f64) {
if self.values.len() == self.capacity {
self.values.remove(0);
}
self.values.push(v);
}
pub fn values(&self) -> &[f64] {
&self.values
}
}
impl Default for History {
fn default() -> Self {
History::new(300)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn text(line: &Line) -> String {
line.spans.iter().map(|s| s.content.as_ref()).collect()
}
#[test]
fn sparkline_pads_left_when_underfilled() {
let l = sparkline(&[1.0, 2.0], 6, 0.0, Gradient::Cpu, None);
assert_eq!(text(&l).chars().count(), 6);
}
#[test]
fn sparkline_clips_to_newest_samples() {
let values: Vec<f64> = (1..=20).map(|v| v as f64).collect();
let l = sparkline(&values, 5, 0.0, Gradient::Cpu, None);
assert_eq!(text(&l).chars().count(), 5);
}
#[test]
fn sparkline_height_tracks_magnitude() {
let full = sparkline(&[100.0], 1, 100.0, Gradient::Cpu, None);
let low = sparkline(&[10.0], 1, 100.0, Gradient::Cpu, None);
assert_eq!(text(&full), braille(0x40 | 0x04 | 0x02 | 0x01).to_string());
assert_eq!(text(&low), braille(0x40).to_string());
}
#[test]
fn zero_samples_still_draw_a_baseline() {
let l = sparkline(&[0.0, 0.0], 2, 10.0, Gradient::Spend, None);
assert_eq!(text(&l), braille(0x40).to_string().repeat(2));
}
#[test]
fn empty_and_zero_width_are_safe() {
assert_eq!(
text(&sparkline(&[], 4, 0.0, Gradient::Cpu, None))
.chars()
.count(),
4
);
assert_eq!(text(&sparkline(&[1.0], 0, 0.0, Gradient::Cpu, None)), "");
}
#[test]
fn line_chart_returns_rows_plus_axis() {
let values: Vec<f64> = (0..40).map(|v| v as f64).collect();
let lines = line_chart(&values, 30, 4, 100.0, Gradient::Cpu, None);
assert_eq!(lines.len(), 5);
let widths: Vec<usize> = lines.iter().map(|l| text(l).chars().count()).collect();
assert!(
widths.windows(2).all(|w| w[0] == w[1]),
"ragged chart rows: {widths:?}"
);
}
#[test]
fn line_chart_survives_empty_input() {
let lines = line_chart(&[], 20, 3, 0.0, Gradient::Accent, None);
assert_eq!(lines.len(), 4);
}
#[test]
fn history_keeps_newest_within_capacity() {
let mut h = History::new(3);
for i in 1..=5 {
h.push(i as f64);
}
assert_eq!(h.values(), &[3.0, 4.0, 5.0]);
}
}