use std::sync::{Arc, Mutex, OnceLock};
use crate::style::{Length, Style};
use crate::utils::gradient::{ColorGradient, GradientRange};
use crate::widgets::Overflow;
mod layout;
mod node;
mod reconcile;
pub use layout::measure_sparkline;
pub use node::SparklineNode;
pub use reconcile::reconcile_sparkline;
pub(crate) use node::SparklineCacheKey;
static SPARKLINE_CACHE: OnceLock<Mutex<SparklineVisualCache>> = OnceLock::new();
#[derive(Clone, Debug)]
pub(crate) struct SparklineVisualCache {
entries: Vec<(SparklineCacheKey, Arc<node::SparklineRenderOutput>)>,
}
impl SparklineVisualCache {
fn new() -> Self {
Self {
entries: Vec::new(),
}
}
fn get(&self, key: &SparklineCacheKey) -> Option<Arc<node::SparklineRenderOutput>> {
self.entries
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| Arc::clone(v))
}
fn insert(&mut self, key: SparklineCacheKey, value: Arc<node::SparklineRenderOutput>) {
if let Some(idx) = self.entries.iter().position(|(k, _)| k == &key) {
self.entries.remove(idx);
}
self.entries.push((key, value));
if self.entries.len() > 100 {
self.entries.remove(0);
}
}
}
pub(crate) fn get_cached_output(
key: &SparklineCacheKey,
) -> Option<Arc<node::SparklineRenderOutput>> {
let cache_mutex = SPARKLINE_CACHE.get_or_init(|| Mutex::new(SparklineVisualCache::new()));
if let Ok(cache) = cache_mutex.lock() {
return cache.get(key);
}
None
}
pub(crate) fn insert_cached_output(
key: SparklineCacheKey,
output: Arc<node::SparklineRenderOutput>,
) {
let cache_mutex = SPARKLINE_CACHE.get_or_init(|| Mutex::new(SparklineVisualCache::new()));
if let Ok(mut cache) = cache_mutex.lock() {
cache.insert(key, output);
}
}
pub(crate) const DEFAULT_BARS: [char; 8] = [' ', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
pub(crate) const SHADE_BARS: [char; 5] = [' ', '░', '▒', '▓', '█'];
pub(crate) const LINE_NORTH: u8 = 0b0001;
pub(crate) const LINE_EAST: u8 = 0b0010;
pub(crate) const LINE_SOUTH: u8 = 0b0100;
pub(crate) const LINE_WEST: u8 = 0b1000;
pub(crate) const LINE_POINT: u8 = 0b1_0000;
use crate::core::element::Element;
impl From<Sparkline> for Element {
fn from(val: Sparkline) -> Self {
Element::new(crate::core::element::ElementKind::Sparkline(val))
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum SparklineVariant {
#[default]
Bars,
Braille,
Line,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum SparklineBarsPreset {
#[default]
Blocks,
Shades,
}
impl SparklineBarsPreset {
pub(crate) fn glyphs(self) -> &'static [char] {
match self {
Self::Blocks => &DEFAULT_BARS,
Self::Shades => &SHADE_BARS,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum SparklineAggregation {
#[default]
Average,
Min,
Max,
First,
Last,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum SparklineZeroPolicy {
#[default]
Empty,
MinGlyph,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum SparklineLinePreset {
#[default]
Unicode,
Ascii,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct SparklineLineGlyphs {
pub rising: char,
pub falling: char,
pub flat: char,
pub peak: char,
pub valley: char,
}
impl SparklineLineGlyphs {
pub const UNICODE: Self = Self {
rising: '╱',
falling: '╲',
flat: '─',
peak: '╮',
valley: '╰',
};
pub const ASCII: Self = Self {
rising: '/',
falling: '\\',
flat: '-',
peak: '^',
valley: 'v',
};
}
impl Default for SparklineLineGlyphs {
fn default() -> Self {
Self::UNICODE
}
}
impl SparklineLinePreset {
pub(crate) fn glyphs(self) -> SparklineLineGlyphs {
match self {
Self::Unicode => SparklineLineGlyphs::UNICODE,
Self::Ascii => SparklineLineGlyphs::ASCII,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum PointTrend {
Rising,
Falling,
Flat,
Turn,
}
#[derive(Clone)]
pub struct Sparkline {
pub data: Vec<u64>,
pub min: Option<u64>,
pub max: Option<u64>,
pub bars: Vec<char>,
pub variant: SparklineVariant,
pub max_points: Option<usize>,
pub aggregation: SparklineAggregation,
pub zero_policy: SparklineZeroPolicy,
pub line_glyphs: SparklineLineGlyphs,
pub chart_height: u16,
pub mirror_x: bool,
pub mirror_y: bool,
pub style: Style,
pub rising_style: Style,
pub falling_style: Style,
pub flat_style: Style,
pub turn_style: Style,
pub gradient: Option<ColorGradient>,
pub height_gradient: Option<ColorGradient>,
pub gradient_range: Option<GradientRange>,
pub width: Length,
pub height: Length,
pub overflow: Overflow,
}
impl Sparkline {
pub fn new(data: impl IntoIterator<Item = u64>) -> Self {
Self {
data: data.into_iter().collect(),
min: None,
max: None,
bars: DEFAULT_BARS.to_vec(),
variant: SparklineVariant::Bars,
max_points: None,
aggregation: SparklineAggregation::Average,
zero_policy: SparklineZeroPolicy::default(),
line_glyphs: SparklineLineGlyphs::default(),
chart_height: 1,
mirror_x: false,
mirror_y: false,
style: Style::default(),
rising_style: Style::default(),
falling_style: Style::default(),
flat_style: Style::default(),
turn_style: Style::default(),
gradient: None,
height_gradient: None,
gradient_range: None,
width: Length::Auto,
height: Length::Auto,
overflow: Overflow::Auto,
}
}
pub fn data(mut self, data: impl IntoIterator<Item = u64>) -> Self {
self.data = data.into_iter().collect();
self
}
pub fn min(mut self, min: u64) -> Self {
self.min = Some(min);
self
}
pub fn max(mut self, max: u64) -> Self {
self.max = Some(max);
self
}
pub fn variant(mut self, variant: SparklineVariant) -> Self {
self.variant = variant;
self
}
pub fn line(mut self) -> Self {
self.variant = SparklineVariant::Line;
self
}
pub fn braille(mut self) -> Self {
self.variant = SparklineVariant::Braille;
self
}
pub fn bars(mut self, bars: impl IntoIterator<Item = char>) -> Self {
self.bars = bars.into_iter().collect();
self
}
pub fn bars_preset(mut self, preset: SparklineBarsPreset) -> Self {
self.bars = preset.glyphs().to_vec();
self
}
pub fn line_preset(mut self, preset: SparklineLinePreset) -> Self {
self.line_glyphs = preset.glyphs();
self
}
pub fn line_glyphs(mut self, glyphs: SparklineLineGlyphs) -> Self {
self.line_glyphs = glyphs;
self
}
pub fn chart_height(mut self, rows: u16) -> Self {
self.chart_height = rows.max(1);
self
}
pub fn mirror_x(mut self, mirror: bool) -> Self {
self.mirror_x = mirror;
self
}
pub fn mirror_y(mut self, mirror: bool) -> Self {
self.mirror_y = mirror;
self
}
pub fn max_points(mut self, max_points: usize) -> Self {
self.max_points = Some(max_points.max(1));
self
}
pub fn aggregation(mut self, aggregation: SparklineAggregation) -> Self {
self.aggregation = aggregation;
self
}
pub fn zero_policy(mut self, policy: SparklineZeroPolicy) -> Self {
self.zero_policy = policy;
self
}
pub fn gradient(mut self, gradient: ColorGradient) -> Self {
self.gradient = Some(gradient);
self
}
pub fn height_gradient(mut self, gradient: ColorGradient) -> Self {
self.height_gradient = Some(gradient);
self
}
pub fn gradient_range(mut self, min: u64, max: u64) -> Self {
self.gradient_range = Some(GradientRange::new(min, max));
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn rising_style(mut self, style: Style) -> Self {
self.rising_style = style;
self
}
pub fn falling_style(mut self, style: Style) -> Self {
self.falling_style = style;
self
}
pub fn flat_style(mut self, style: Style) -> Self {
self.flat_style = style;
self
}
pub fn turn_style(mut self, style: Style) -> Self {
self.turn_style = style;
self
}
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
pub fn overflow(mut self, overflow: Overflow) -> Self {
self.overflow = overflow;
self
}
}
#[cfg(test)]
mod tests {
use super::{Sparkline, SparklineLinePreset, SparklineVariant};
use crate::core::element::{Element, ElementKind};
use crate::style::Length;
fn into_node(el: Element) -> super::node::SparklineNode {
match el.kind {
ElementKind::Sparkline(spark) => spark.into(),
_ => panic!("expected sparkline element"),
}
}
#[test]
fn default_bars_render_expected_ramp() {
let node = into_node(
Sparkline::new([0, 1, 2, 3, 4, 5, 6, 7])
.min(0)
.max(7)
.into(),
);
let content: String = node.output.rows[0]
.iter()
.map(|s| s.content.as_ref())
.collect();
assert_eq!(content, " ▂▃▄▅▆▇█");
}
#[test]
fn chart_height_renders_multi_row_bars() {
let node = into_node(
Sparkline::new([0, 25, 50, 75, 100])
.min(0)
.max(100)
.chart_height(3)
.into(),
);
assert_eq!(node.output.rows.len(), 3);
assert_eq!(node.height, Length::Auto);
}
#[test]
fn line_variant_renders_turn_glyphs() {
let node = into_node(Sparkline::new([1, 3, 2, 4, 4, 1]).line().into());
let content: String = node.output.rows[0]
.iter()
.map(|s| s.content.as_ref())
.collect();
assert_eq!(content, "╱╮╰╱╲╲");
}
#[test]
fn braille_variant_packs_two_samples_per_cell() {
let node = into_node(
Sparkline::new([0, 1, 2, 3, 4])
.variant(SparklineVariant::Braille)
.min(0)
.max(4)
.into(),
);
let content: String = node.output.rows[0]
.iter()
.map(|s| s.content.as_ref())
.collect();
assert_eq!(content.chars().count(), 3);
}
#[test]
fn line_ascii_preset_is_available() {
let node = into_node(
Sparkline::new([1, 2, 1])
.variant(SparklineVariant::Line)
.line_preset(SparklineLinePreset::Ascii)
.into(),
);
let content: String = node.output.rows[0]
.iter()
.map(|s| s.content.as_ref())
.collect();
assert_eq!(content, "/^\\");
}
#[test]
fn mirror_y_braille_flips_fill_direction() {
let normal_node = into_node(
Sparkline::new([1])
.variant(SparklineVariant::Braille)
.min(0)
.max(4)
.into(),
);
let mirrored_node = into_node(
Sparkline::new([1])
.variant(SparklineVariant::Braille)
.min(0)
.max(4)
.mirror_y(true)
.into(),
);
let normal: String = normal_node.output.rows[0]
.iter()
.map(|s| s.content.as_ref())
.collect();
let mirrored: String = mirrored_node.output.rows[0]
.iter()
.map(|s| s.content.as_ref())
.collect();
assert_eq!(normal, "⡀");
assert_eq!(mirrored, "⠁");
}
}