const LEFT_BITS: [u32; 4] = [
0x40, 0x04, 0x02, 0x01, ];
const RIGHT_BITS: [u32; 4] = [
0x80, 0x20, 0x10, 0x08, ];
#[must_use]
pub fn sparkline_braille(data: &[f64], width: usize, range: Option<(f64, f64)>) -> String {
let mut rows = sparkline_braille_rows(data, width, 1, range);
rows.pop().unwrap_or_default()
}
#[must_use]
pub fn sparkline_braille_rows(
data: &[f64],
width: usize,
rows: usize,
range: Option<(f64, f64)>,
) -> Vec<String> {
if rows == 0 {
return Vec::new();
}
if data.is_empty() {
return vec![" ".repeat(width); rows];
}
if width == 0 {
return vec![String::new(); rows];
}
let n_sub = width * 2;
let len = data.len();
let window = &data[len.saturating_sub(n_sub)..];
let blank_subs = n_sub - window.len();
let (min, max) = match range {
Some((lo, hi)) if !lo.is_finite() || !hi.is_finite() => {
(0.0_f64, 0.0_f64)
}
Some((lo, hi)) => (lo, hi),
None => {
let mut lo = f64::INFINITY;
let mut hi = f64::NEG_INFINITY;
for &v in window {
if v.is_finite() {
if v < lo {
lo = v;
}
if v > hi {
hi = v;
}
}
}
if !lo.is_finite() {
lo = 0.0;
}
if !hi.is_finite() {
hi = lo;
}
(lo, hi)
}
};
let total_levels = rows * 4;
let level_of = |v: f64| -> usize {
if max <= min {
return 0;
}
let clamped = if v.is_finite() {
v.clamp(min, max)
} else {
min
};
let norm = (clamped - min) / (max - min);
((norm * total_levels as f64).floor() as usize).min(total_levels - 1)
};
let dots_filled: Vec<Option<usize>> = (0..n_sub)
.map(|i| i.checked_sub(blank_subs).map(|w| level_of(window[w]) + 1))
.collect();
let mut out_rows: Vec<String> = Vec::with_capacity(rows);
for r in 0..rows {
let row_from_bottom = rows - 1 - r;
let row_base = row_from_bottom * 4;
let mut row = String::with_capacity(width * 3); for cell in 0..width {
let left = dots_filled[cell * 2];
let right = dots_filled[cell * 2 + 1];
if left.is_none() && right.is_none() {
row.push(' ');
continue;
}
let left_dots = left.map_or(0, |d| d.saturating_sub(row_base).min(4));
let right_dots = right.map_or(0, |d| d.saturating_sub(row_base).min(4));
let mut bits: u32 = 0;
for &b in LEFT_BITS.iter().take(left_dots) {
bits |= b;
}
for &b in RIGHT_BITS.iter().take(right_dots) {
bits |= b;
}
let ch = char::from_u32(0x2800 + bits).unwrap_or('⠀');
row.push(ch);
}
out_rows.push(row);
}
out_rows
}
#[cfg(test)]
mod tests {
use super::*;
fn char_count(s: &str) -> usize {
s.chars().count()
}
fn all_braille(s: &str) -> bool {
s.chars().all(|c| ('\u{2800}'..='\u{28FF}').contains(&c))
}
#[test]
fn empty_input_returns_spaces() {
let result = sparkline_braille(&[], 8, None);
assert_eq!(result.len(), 8, "should be 8 ASCII space bytes");
assert_eq!(char_count(&result), 8);
assert!(result.chars().all(|c| c == ' '));
}
#[test]
fn zero_width_returns_empty() {
let result = sparkline_braille(&[1.0, 2.0, 3.0], 0, None);
assert!(result.is_empty());
}
#[test]
fn single_point_no_panic() {
let result = sparkline_braille(&[42.0], 5, None);
assert_eq!(char_count(&result), 5);
}
#[test]
fn constant_input_renders_bottom_row() {
let data = vec![7.0; 10];
let result = sparkline_braille(&data, 4, None);
assert_eq!(char_count(&result), 4);
for ch in result.chars() {
assert_eq!(
ch, '\u{28C0}',
"expected bottom-row-filled cell ⣀, got {ch:?}"
);
}
}
#[test]
fn monotonic_ramp_valid_braille() {
let data = [0.0, 1.0, 2.0, 3.0];
let result = sparkline_braille(&data, 2, None);
assert_eq!(char_count(&result), 2);
assert!(
all_braille(&result),
"all chars should be braille codepoints"
);
}
#[test]
fn explicit_range_different_outputs() {
let data = [5.0, 10.0, 15.0];
let wide = sparkline_braille(&data, 3, Some((0.0, 20.0)));
let tight = sparkline_braille(&data, 3, Some((5.0, 15.0)));
assert_eq!(char_count(&wide), 3);
assert_eq!(char_count(&tight), 3);
assert_ne!(
wide, tight,
"different ranges should produce different sparklines"
);
}
#[test]
fn degenerate_range_no_panic() {
let result = sparkline_braille(&[5.0, 5.0, 5.0], 4, Some((5.0, 5.0)));
assert_eq!(char_count(&result), 4);
}
#[test]
fn nan_and_infinity_no_panic() {
let data = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, 1.0, 2.0];
let result = sparkline_braille(&data, 5, None);
assert_eq!(char_count(&result), 5);
}
#[test]
fn non_finite_range_bounds_no_panic() {
let result = sparkline_braille(&[1.0], 4, Some((f64::NAN, 1.0)));
assert_eq!(
char_count(&result),
4,
"should return 4 chars even with NaN range bound"
);
let result2 = sparkline_braille(&[1.0], 4, Some((0.0, f64::INFINITY)));
assert_eq!(
char_count(&result2),
4,
"should return 4 chars even with infinite range bound"
);
}
#[test]
fn multirow_dimensions() {
let data: Vec<f64> = (0..50).map(|i| (i as f64).sin() * 10.0 + 20.0).collect();
let rows = sparkline_braille_rows(&data, 6, 3, None);
assert_eq!(rows.len(), 3, "should return one string per row");
for row in &rows {
assert_eq!(char_count(row), 6, "each row should be `width` chars");
assert!(all_braille(row), "all chars should be braille codepoints");
}
}
#[test]
fn level_continuity_fills_bottom_row_at_half_range() {
let data = vec![50.0; 8];
let rows = sparkline_braille_rows(&data, 4, 2, Some((0.0, 100.0)));
assert_eq!(rows.len(), 2);
let bottom_row = &rows[1]; for ch in bottom_row.chars() {
assert_eq!(
ch, '\u{28FF}',
"bottom terminal row should be fully filled at exactly 50% of range, got {ch:?}"
);
}
}
#[test]
fn spike_preservation_width_8() {
let mut data = vec![0.0; 99];
data.push(100.0);
let result = sparkline_braille(&data, 8, None);
assert_eq!(char_count(&result), 8);
let has_top_dot = result.chars().any(|c| {
let bits = c as u32 - 0x2800;
bits & (0x01 | 0x08) != 0
});
assert!(
has_top_dot,
"a single-sample spike should render at least one top-level dot, got {result:?}"
);
}
#[test]
fn rows_one_matches_wrapper() {
type Case = (Vec<f64>, usize, Option<(f64, f64)>);
let cases: Vec<Case> = vec![
(vec![], 8, None),
(vec![1.0, 2.0, 3.0], 0, None),
(vec![42.0], 5, None),
(vec![7.0; 10], 4, None),
((0..30).map(|i| i as f64).collect(), 8, None),
(vec![5.0, 10.0, 15.0], 3, Some((0.0, 20.0))),
(vec![f64::NAN, f64::INFINITY, 1.0, 2.0], 5, None),
];
for (data, width, range) in cases {
let direct = sparkline_braille(&data, width, range);
let via_rows = sparkline_braille_rows(&data, width, 1, range);
assert_eq!(via_rows.len(), 1);
assert_eq!(
direct, via_rows[0],
"sparkline_braille should match sparkline_braille_rows(..., 1, ...)[0] for {data:?}"
);
}
}
#[test]
fn window_keeps_the_most_recent_samples() {
let data = [0.0, 0.0, 5.0, 0.0];
let result = sparkline_braille(&data, 1, Some((0.0, 5.0)));
assert_eq!(char_count(&result), 1);
let ch = result.chars().next().expect("single char");
let bits = ch as u32 - 0x2800;
assert_eq!(
bits & (0x40 | 0x04 | 0x02 | 0x01),
0x40 | 0x04 | 0x02 | 0x01,
"left sub-column should be fully filled by data[2] = 5.0"
);
assert_eq!(bits & 0x80, 0x80, "right bottom dot should be set");
assert_eq!(
bits & 0x08,
0,
"right top dot should be clear (data[3] = 0.0 is the newest sample)"
);
}
#[test]
fn multirow_empty_data() {
let rows = sparkline_braille_rows(&[], 6, 3, None);
assert_eq!(rows.len(), 3);
for row in &rows {
assert_eq!(char_count(row), 6);
assert!(row.chars().all(|c| c == ' '));
}
}
#[test]
fn multirow_zero_width() {
let rows = sparkline_braille_rows(&[1.0, 2.0, 3.0], 0, 3, None);
assert_eq!(rows.len(), 3);
for row in &rows {
assert!(row.is_empty());
}
}
#[test]
fn multirow_zero_rows() {
let rows = sparkline_braille_rows(&[1.0, 2.0, 3.0], 8, 0, None);
assert!(rows.is_empty());
assert!(sparkline_braille_rows(&[], 8, 0, None).is_empty());
assert!(sparkline_braille_rows(&[1.0], 0, 0, None).is_empty());
}
#[test]
fn short_history_is_right_anchored() {
let data = [0.0, 10.0];
let result = sparkline_braille(&data, 2, Some((0.0, 10.0)));
assert_eq!(
result, " \u{28F8}",
"a 2-sample series at width 2 should leave the first cell blank and \
render both samples right-anchored in the second cell"
);
}
#[test]
fn window_scrolls_left_without_rescaling() {
const WIDTH: usize = 8; let burst_at = |offset: usize| -> Vec<f64> {
let mut v = vec![0.0; 40];
let end = v.len() - offset;
for x in v[end - 4..end].iter_mut() {
*x = 100.0;
}
v
};
let burst_width = |s: &str| -> usize {
s.chars()
.map(|c| {
let bits = c as u32 - 0x2800;
usize::from(bits & 0x01 != 0) + usize::from(bits & 0x08 != 0)
})
.sum()
};
let burst_start = |s: &str| -> Option<usize> {
s.chars().enumerate().find_map(|(cell, c)| {
let bits = c as u32 - 0x2800;
if bits & 0x01 != 0 {
Some(cell * 2)
} else if bits & 0x08 != 0 {
Some(cell * 2 + 1)
} else {
None
}
})
};
let mut prev_start = None;
for offset in 0..8 {
let s = sparkline_braille(&burst_at(offset), WIDTH, Some((0.0, 100.0)));
assert_eq!(
burst_width(&s),
4,
"the burst must keep its width as it scrolls (offset={offset}): {s:?}"
);
let start = burst_start(&s).expect("burst must be visible");
if let Some(prev) = prev_start {
assert_eq!(
start + 1,
prev,
"the burst must shift left by exactly one sub-column per sample \
(offset={offset}): {s:?}"
);
}
prev_start = Some(start);
}
}
#[test]
fn auto_range_uses_only_the_visible_window() {
let mut data = vec![1000.0];
data.extend(std::iter::repeat_n(5.0, 10));
let result = sparkline_braille(&data, 2, None);
assert_eq!(
result, "\u{28C0}\u{28C0}",
"the out-of-window spike must not affect the auto-range: {result:?}"
);
}
}