use retroglyph_core::color::{Color, Style};
use retroglyph_core::symbols::bar::NINE_LEVELS;
use super::{Meter, Widget};
use crate::Surface;
#[derive(Clone, Copy, Debug)]
pub struct Sparkline<'a> {
samples: &'a [f32],
style: Option<Style>,
fill_color: Option<fn(f32) -> Color>,
glyphs: &'a [char],
}
impl<'a> Sparkline<'a> {
#[must_use]
pub const fn new(samples: &'a [f32]) -> Self {
Self {
samples,
style: None,
fill_color: None,
glyphs: &NINE_LEVELS,
}
}
#[must_use]
pub const fn style(mut self, style: Style) -> Self {
self.style = Some(style);
self
}
#[must_use]
pub const fn fill_color(mut self, fill_color: fn(f32) -> Color) -> Self {
self.fill_color = Some(fill_color);
self
}
#[must_use]
pub const fn glyphs(mut self, glyphs: &'a [char]) -> Self {
self.glyphs = glyphs;
self
}
}
impl Widget for Sparkline<'_> {
fn render(&self, surface: &mut Surface<'_>) {
let width = usize::from(surface.width());
if width == 0 {
return;
}
let max = self
.samples
.iter()
.copied()
.fold(0.0_f32, f32::max)
.max(1e-6);
let start = self.samples.len().saturating_sub(width);
let recent = &self.samples[start..];
let pad = width - recent.len();
let glyphs: &[char] = if self.glyphs.is_empty() {
&[' ']
} else {
self.glyphs
};
let top = glyphs.len() - 1;
for i in 0..width {
#[allow(clippy::cast_possible_truncation)]
let x = i as u16;
if i < pad {
surface.put((x, 0), ' ', Style::new());
continue;
}
let ratio = (recent[i - pad] / max).clamp(0.0, 1.0);
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
let level = retroglyph_core::math::round(ratio * top as f32) as usize;
let color = self
.fill_color
.map_or_else(|| Meter::new(ratio).color(), |fill_color| fill_color(ratio));
let style = self.style.unwrap_or_else(|| Style::new().fg(color));
surface.put((x, 0), glyphs[level.min(top)], style);
}
}
}
#[cfg(test)]
mod tests {
use retroglyph_core::grid::{Grid, Pos, Rect};
use super::*;
#[test]
fn right_aligns_recent_samples_and_pads_the_rest() {
let area = Rect::new(0, 0, 5, 1);
let mut grid = Grid::new(5, 1);
Sparkline::new(&[1.0, 2.0]).render(&mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
assert_eq!(grid[Pos::new(2, 0)].glyph(), ' ');
assert_eq!(grid[Pos::new(3, 0)].glyph(), NINE_LEVELS[4]); assert_eq!(grid[Pos::new(4, 0)].glyph(), NINE_LEVELS[8]); }
#[test]
fn empty_samples_is_a_no_op_beyond_blank_padding() {
let area = Rect::new(0, 0, 3, 1);
let mut grid = Grid::new(3, 1);
Sparkline::new(&[]).render(&mut Surface::new(&mut grid, area, 0));
for x in 0..3 {
assert_eq!(grid[Pos::new(x, 0)].glyph(), ' ');
}
}
#[test]
fn style_overrides_the_default_meter_ramp_with_one_fixed_color() {
use retroglyph_core::color::Color;
let area = Rect::new(0, 0, 3, 1);
let mut grid = Grid::new(3, 1);
let accent = Style::new().fg(Color::rgb(90, 170, 250));
Sparkline::new(&[1.0, 4.0, 2.0])
.style(accent)
.render(&mut Surface::new(&mut grid, area, 0));
assert_ne!(grid[Pos::new(0, 0)].glyph(), grid[Pos::new(1, 0)].glyph());
for x in 0..3 {
assert_eq!(grid[Pos::new(x, 0)].style(), accent);
}
}
#[test]
fn fill_color_overrides_the_default_meter_ramp() {
use retroglyph_core::color::Color;
fn white_to_red(ratio: f32) -> Color {
Color::lerp(Color::WHITE, Color::RED, ratio)
}
let area = Rect::new(0, 0, 3, 1);
let mut grid = Grid::new(3, 1);
Sparkline::new(&[1.0, 4.0, 2.0])
.fill_color(white_to_red)
.render(&mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(1, 0)].style().foreground(), white_to_red(1.0));
}
#[test]
fn style_wins_over_fill_color_when_both_are_set() {
use retroglyph_core::color::Color;
fn white_to_red(ratio: f32) -> Color {
Color::lerp(Color::WHITE, Color::RED, ratio)
}
let accent = Style::new().fg(Color::rgb(90, 170, 250));
let area = Rect::new(0, 0, 3, 1);
let mut grid = Grid::new(3, 1);
Sparkline::new(&[1.0, 4.0, 2.0])
.fill_color(white_to_red)
.style(accent)
.render(&mut Surface::new(&mut grid, area, 0));
for x in 0..3 {
assert_eq!(grid[Pos::new(x, 0)].style(), accent);
}
}
#[test]
fn glyphs_overrides_the_default_eighth_block_ramp() {
use retroglyph_core::symbols::bar::SHADE_LEVELS;
let area = Rect::new(0, 0, 3, 1);
let mut grid = Grid::new(3, 1);
Sparkline::new(&[1.0, 4.0, 2.0])
.glyphs(&SHADE_LEVELS)
.render(&mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(1, 0)].glyph(), SHADE_LEVELS[4]);
for x in 0..3 {
assert!(SHADE_LEVELS.contains(&grid[Pos::new(x, 0)].glyph()));
}
}
#[test]
fn empty_glyphs_falls_back_to_a_single_blank_level() {
let area = Rect::new(0, 0, 3, 1);
let mut grid = Grid::new(3, 1);
Sparkline::new(&[1.0, 4.0, 2.0])
.glyphs(&[])
.render(&mut Surface::new(&mut grid, area, 0));
for x in 0..3 {
assert_eq!(grid[Pos::new(x, 0)].glyph(), ' ');
}
}
}