const BLOCKS: [char; 9] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
pub fn spark(values: &[f32], max: f32) -> String {
if max <= 0.0 {
return " ".repeat(values.len());
}
values
.iter()
.map(|&v| {
let frac = (v / max).clamp(0.0, 1.0);
let level = (frac * 8.0).round() as usize;
BLOCKS[level.min(8)]
})
.collect()
}
pub fn graph_rows(
values: &[f64],
width_cells: usize,
height_cells: usize,
max: f64,
) -> Vec<String> {
let w_dots = width_cells.max(1) * 2;
let h_dots = height_cells.max(1) * 4;
let mut cv = Canvas::new(w_dots, h_dots);
let n = values.len().min(w_dots);
let start = values.len() - n;
let col_off = w_dots - n;
for (i, &v) in values[start..].iter().enumerate() {
let frac = if max > 0.0 {
(v / max).clamp(0.0, 1.0)
} else {
0.0
};
let filled = (frac * h_dots as f64).round() as usize;
for up in 0..filled {
let y = h_dots - 1 - up; cv.set(col_off + i, y);
}
}
cv.rows()
}
pub struct Canvas {
w: usize,
h: usize,
cells: Vec<u8>,
}
impl Canvas {
pub fn new(w_dots: usize, h_dots: usize) -> Self {
let w = w_dots.div_ceil(2).max(1);
let h = h_dots.div_ceil(4).max(1);
Canvas {
w,
h,
cells: vec![0u8; w * h],
}
}
pub fn set(&mut self, x: usize, y: usize) {
let cx = x / 2;
let cy = y / 4;
if cx >= self.w || cy >= self.h {
return;
}
const MAP: [[u8; 2]; 4] = [[0x01, 0x08], [0x02, 0x10], [0x04, 0x20], [0x40, 0x80]];
self.cells[cy * self.w + cx] |= MAP[y % 4][x % 2];
}
pub fn rows(&self) -> Vec<String> {
(0..self.h)
.map(|cy| {
(0..self.w)
.map(|cx| {
let bits = self.cells[cy * self.w + cx] as u32;
char::from_u32(0x2800 + bits).unwrap_or('?')
})
.collect()
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spark_scales_full_range() {
let s = spark(&[0.0, 50.0, 100.0], 100.0);
let chars: Vec<char> = s.chars().collect();
assert_eq!(chars[0], ' ');
assert_eq!(chars[2], '█');
assert_eq!(chars.len(), 3);
}
#[test]
fn spark_no_data_is_blank() {
assert_eq!(spark(&[1.0, 2.0], 0.0), " ");
}
#[test]
fn canvas_top_left_dot_is_2801() {
let mut c = Canvas::new(2, 4);
c.set(0, 0);
assert_eq!(c.rows()[0].chars().next().unwrap(), '\u{2801}');
}
#[test]
fn canvas_bottom_right_dot() {
let mut c = Canvas::new(2, 4);
c.set(1, 3); assert_eq!(c.rows()[0].chars().next().unwrap(), '\u{2880}');
}
#[test]
fn graph_rows_right_aligns_newest_when_underfilled() {
let rows = graph_rows(&[100.0, 100.0], 4, 1, 100.0);
assert_eq!(rows.len(), 1);
let cells: Vec<char> = rows[0].chars().collect();
assert_eq!(cells.len(), 4);
assert_eq!(cells[0], '\u{2800}'); assert_eq!(cells[1], '\u{2800}');
assert_ne!(cells[3], '\u{2800}'); }
#[test]
fn graph_rows_zero_max_is_blank() {
let rows = graph_rows(&[50.0, 90.0], 3, 1, 0.0);
assert!(rows[0].chars().all(|c| c == '\u{2800}'));
}
#[test]
fn canvas_out_of_bounds_is_ignored() {
let mut c = Canvas::new(2, 4);
c.set(99, 99);
assert_eq!(c.rows()[0], "\u{2800}");
}
}