1const BRAILLE_DOTS: [[u8; 4]; 2] = [
16 [0x01, 0x02, 0x04, 0x40], [0x08, 0x10, 0x20, 0x80], ];
19
20pub struct BrailleCanvas {
22 width: usize,
24 height: usize,
26 cells: Vec<Vec<u8>>,
28}
29
30impl BrailleCanvas {
31 pub fn new(width: usize, height: usize) -> Self {
32 Self {
33 width,
34 height,
35 cells: vec![vec![0u8; width]; height],
36 }
37 }
38
39 pub fn pixel_width(&self) -> usize {
41 self.width * 2
42 }
43
44 pub fn pixel_height(&self) -> usize {
45 self.height * 4
46 }
47
48 pub fn set_pixel(&mut self, x: usize, y: usize) {
50 let cell_x = x / 2;
51 let cell_y = y / 4;
52 let dot_x = x % 2;
53 let dot_y = y % 4;
54
55 if cell_x < self.width && cell_y < self.height {
56 self.cells[cell_y][cell_x] |= BRAILLE_DOTS[dot_x][dot_y];
57 }
58 }
59
60 pub fn draw_line(&mut self, x0: usize, y0: usize, x1: usize, y1: usize) {
62 let (mut x0, mut y0) = (x0 as isize, y0 as isize);
63 let (x1, y1) = (x1 as isize, y1 as isize);
64
65 let dx = (x1 - x0).abs();
66 let dy = -(y1 - y0).abs();
67 let sx: isize = if x0 < x1 { 1 } else { -1 };
68 let sy: isize = if y0 < y1 { 1 } else { -1 };
69 let mut err = dx + dy;
70
71 loop {
72 if x0 >= 0 && y0 >= 0 {
73 self.set_pixel(x0 as usize, y0 as usize);
74 }
75
76 if x0 == x1 && y0 == y1 {
77 break;
78 }
79
80 let e2 = 2 * err;
81 if e2 >= dy {
82 err += dy;
83 x0 += sx;
84 }
85 if e2 <= dx {
86 err += dx;
87 y0 += sy;
88 }
89 }
90 }
91
92 pub fn render(&self) -> Vec<String> {
94 self.cells
95 .iter()
96 .map(|row| {
97 row.iter()
98 .map(|&bits| char::from_u32(0x2800 + bits as u32).unwrap_or(' '))
99 .collect()
100 })
101 .collect()
102 }
103}
104
105pub fn plot_series(data: &[f64], width: usize, height: usize, label: &str) -> Vec<String> {
109 if data.is_empty() || width == 0 || height == 0 {
110 return vec![format!("{label}: (no data)")];
111 }
112
113 let label_width = 8;
115 let chart_width = width.saturating_sub(label_width);
116 if chart_width == 0 {
117 return vec![format!("{label}: (too narrow)")];
118 }
119
120 let mut canvas = BrailleCanvas::new(chart_width, height);
121 let pw = canvas.pixel_width();
122 let ph = canvas.pixel_height();
123
124 let min_val = data.iter().copied().fold(f64::INFINITY, f64::min);
126 let max_val = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
127 let range = if (max_val - min_val).abs() < f64::EPSILON {
128 1.0
129 } else {
130 max_val - min_val
131 };
132
133 let points: Vec<(usize, usize)> = data
135 .iter()
136 .enumerate()
137 .map(|(i, &v)| {
138 let x = if data.len() > 1 {
139 i * (pw - 1) / (data.len() - 1)
140 } else {
141 pw / 2
142 };
143 let y = ((max_val - v) / range * (ph - 1) as f64) as usize;
145 (x, y.min(ph - 1))
146 })
147 .collect();
148
149 for window in points.windows(2) {
151 canvas.draw_line(window[0].0, window[0].1, window[1].0, window[1].1);
152 }
153
154 if points.len() == 1 {
156 canvas.set_pixel(points[0].0, points[0].1);
157 }
158
159 let rendered = canvas.render();
161 let mut result = Vec::with_capacity(height + 1);
162
163 result.push(format!("─ {label} "));
165
166 for (i, line) in rendered.iter().enumerate() {
167 let y_val = if i == 0 {
168 max_val
169 } else if i == height - 1 {
170 min_val
171 } else {
172 max_val - (i as f64 / (height - 1) as f64) * range
173 };
174
175 let y_label = format_number(y_val);
176 result.push(format!("{y_label:>label_width$}┤{line}"));
177 }
178
179 result
180}
181
182pub fn format_number(v: f64) -> String {
184 let abs = v.abs();
185 if abs == 0.0 {
186 "0".into()
187 } else if abs >= 1_000_000.0 {
188 format!("{:.1}M", v / 1_000_000.0)
189 } else if abs >= 1_000.0 {
190 format!("{:.1}k", v / 1_000.0)
191 } else if abs >= 1.0 {
192 format!("{v:.2}")
193 } else if abs >= 0.001 {
194 format!("{v:.4}")
195 } else {
196 format!("{v:.2e}")
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn test_braille_canvas_new() {
206 let canvas = BrailleCanvas::new(10, 5);
207 assert_eq!(canvas.pixel_width(), 20);
208 assert_eq!(canvas.pixel_height(), 20);
209 }
210
211 #[test]
212 fn test_set_pixel_renders_braille() {
213 let mut canvas = BrailleCanvas::new(1, 1);
214 canvas.set_pixel(0, 0); let rendered = canvas.render();
216 assert_eq!(rendered.len(), 1);
217 assert_eq!(rendered[0], "\u{2801}");
219 }
220
221 #[test]
222 fn test_set_multiple_pixels() {
223 let mut canvas = BrailleCanvas::new(1, 1);
224 canvas.set_pixel(0, 0); canvas.set_pixel(1, 0); let rendered = canvas.render();
227 assert_eq!(rendered[0], "\u{2809}");
229 }
230
231 #[test]
232 fn test_empty_canvas_renders_blank_braille() {
233 let canvas = BrailleCanvas::new(3, 2);
234 let rendered = canvas.render();
235 assert_eq!(rendered.len(), 2);
236 for line in &rendered {
238 assert_eq!(line.chars().count(), 3);
239 assert!(line.chars().all(|c| c == '\u{2800}'));
240 }
241 }
242
243 #[test]
244 fn test_draw_line_horizontal() {
245 let mut canvas = BrailleCanvas::new(5, 1);
246 canvas.draw_line(0, 0, 9, 0);
247 let rendered = canvas.render();
248 for c in rendered[0].chars() {
250 assert_ne!(c, '\u{2800}', "expected dots in horizontal line");
251 }
252 }
253
254 #[test]
255 fn test_draw_line_vertical() {
256 let mut canvas = BrailleCanvas::new(1, 3);
257 canvas.draw_line(0, 0, 0, 11);
258 let rendered = canvas.render();
259 for line in &rendered {
261 let c = line.chars().next().unwrap();
262 assert_ne!(c, '\u{2800}', "expected dots in vertical line");
263 }
264 }
265
266 #[test]
267 fn test_out_of_bounds_pixel_ignored() {
268 let mut canvas = BrailleCanvas::new(2, 2);
269 canvas.set_pixel(100, 100); let rendered = canvas.render();
271 assert!(rendered.iter().all(|l| l.chars().all(|c| c == '\u{2800}')));
272 }
273
274 #[test]
275 fn test_plot_series_empty() {
276 let result = plot_series(&[], 40, 10, "test");
277 assert_eq!(result.len(), 1);
278 assert!(result[0].contains("no data"));
279 }
280
281 #[test]
282 fn test_plot_series_single_point() {
283 let result = plot_series(&[42.0], 40, 5, "value");
284 assert!(result.len() > 1);
285 }
286
287 #[test]
288 fn test_plot_series_monotonic() {
289 let data: Vec<f64> = (0..50).map(|i| i as f64).collect();
290 let result = plot_series(&data, 40, 8, "linear");
291 assert!(result.len() > 1);
292 }
293
294 #[test]
295 fn test_plot_series_constant() {
296 let data = vec![5.0; 20];
297 let result = plot_series(&data, 40, 5, "constant");
298 assert!(result.len() > 1);
299 }
300
301 #[test]
302 fn test_format_number() {
303 assert_eq!(format_number(0.0), "0");
304 assert_eq!(format_number(1500000.0), "1.5M");
305 assert_eq!(format_number(2500.0), "2.5k");
306 assert_eq!(format_number(3.14), "3.14");
307 assert_eq!(format_number(0.0523), "0.0523");
308 }
309}