const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
const NON_FINITE_GLYPH: char = '·';
pub fn render_sparkline(values: &[f64]) -> String {
if values.is_empty() {
return String::new();
}
let mut min = f64::INFINITY;
let mut max = f64::NEG_INFINITY;
for &v in values {
if v.is_finite() {
if v < min {
min = v;
}
if v > max {
max = v;
}
}
}
if !min.is_finite() || !max.is_finite() {
return values.iter().map(|_| NON_FINITE_GLYPH).collect();
}
let span = max - min;
let last = BLOCKS.len() - 1;
values
.iter()
.map(|&v| {
if !v.is_finite() {
return NON_FINITE_GLYPH;
}
if span <= 0.0 {
return BLOCKS[last / 2];
}
let frac = (v - min) / span;
let idx = (frac * last as f64).round() as usize;
BLOCKS[idx.min(last)]
})
.collect()
}
pub fn render_smooth_line(name: &str, xs: &[f64], ys: &[f64]) -> String {
let n = xs.len().min(ys.len());
let xs = &xs[..n];
let ys = &ys[..n];
let spark = render_sparkline(ys);
let (xlo, xhi) = finite_range(xs);
let (ylo, yhi) = finite_range(ys);
format!(
" {name}: {spark} x∈[{xlo}, {xhi}] y∈[{ylo}, {yhi}]",
xlo = fmt(xlo),
xhi = fmt(xhi),
ylo = fmt(ylo),
yhi = fmt(yhi),
)
}
fn finite_range(values: &[f64]) -> (f64, f64) {
let mut min = f64::INFINITY;
let mut max = f64::NEG_INFINITY;
for &v in values {
if v.is_finite() {
if v < min {
min = v;
}
if v > max {
max = v;
}
}
}
if min.is_finite() && max.is_finite() {
(min, max)
} else {
(f64::NAN, f64::NAN)
}
}
fn fmt(v: f64) -> String {
if v.is_finite() {
format!("{v:.3}")
} else {
"n/a".to_string()
}
}
#[cfg(test)]
mod tests {
use super::{render_smooth_line, render_sparkline};
#[test]
fn ramp_maps_min_to_bottom_and_max_to_top() {
let ys: Vec<f64> = (0..8).map(|i| i as f64).collect();
assert_eq!(render_sparkline(&ys), "▁▂▃▄▅▆▇█");
}
#[test]
fn symmetric_bump_is_faithful() {
let ys = [0.0, 1.0, 2.0, 3.0, 2.0, 1.0, 0.0];
assert_eq!(render_sparkline(&ys), "▁▃▆█▆▃▁");
}
#[test]
fn constant_series_is_a_flat_midline() {
assert_eq!(render_sparkline(&[2.5, 2.5, 2.5, 2.5]), "▄▄▄▄");
assert_eq!(render_sparkline(&[42.0]), "▄");
}
#[test]
fn empty_series_renders_empty() {
assert_eq!(render_sparkline(&[]), "");
}
#[test]
fn non_finite_samples_are_marked_and_do_not_rescale() {
assert_eq!(render_sparkline(&[0.0, f64::NAN, 1.0]), "▁·█");
assert_eq!(render_sparkline(&[f64::NAN, f64::INFINITY]), "··");
}
#[test]
fn labelled_line_reports_true_ranges() {
let xs = [18.0, 50.0, 84.0];
let ys = [-0.42, 0.30, 1.07];
let line = render_smooth_line("s(age)", &xs, &ys);
assert_eq!(
line,
" s(age): ▁▄█ x∈[18.000, 84.000] y∈[-0.420, 1.070]"
);
}
#[test]
fn labelled_line_handles_no_finite_y() {
let xs = [0.0, 1.0];
let ys = [f64::NAN, f64::NAN];
let line = render_smooth_line("s(x)", &xs, &ys);
assert_eq!(line, " s(x): ·· x∈[0.000, 1.000] y∈[n/a, n/a]");
}
}