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 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 canvas_out_of_bounds_is_ignored() {
let mut c = Canvas::new(2, 4);
c.set(99, 99);
assert_eq!(c.rows()[0], "\u{2800}");
}
}