1use crate::ansi::Buffer;
12use crate::render::{BarStyle, Rendered};
13use crate::scene::{BarDirection, Scene, SceneMark};
14
15const EIGHTHS: [char; 8] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇'];
18
19const LEFT_EIGHTHS: [char; 8] = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉'];
23
24pub struct RasterOptions {
27 pub marker: Marker,
28 pub bar_style: BarStyle,
29 pub color: bool,
30}
31
32pub fn rasterize(scene: &Scene, opts: &RasterOptions) -> Rendered {
36 let mut buf = Buffer::new(scene.size.columns, scene.size.rows);
37 let gutter = scene.plot.x.saturating_sub(1);
39 let top = scene.plot.y;
40 let plot_w = scene.plot.w;
41 let plot_h = scene.plot.h;
42 let axis = Some(scene.chrome.axis);
43
44 if let Some(t) = &scene.title {
45 buf.text(t.col, t.row, &t.text, Some(scene.chrome.title));
46 }
47 for entry in &scene.legend {
48 buf.text(entry.col, entry.row, "──", Some(entry.color));
49 buf.text(entry.col + 3, entry.row, &entry.name, axis);
50 }
51
52 for r in 0..plot_h {
54 buf.set(gutter, top + r, '│', axis);
55 }
56 for tick in &scene.y_axis.ticks {
57 buf.set(gutter, tick.row, '┤', axis);
58 let len = tick.label.chars().count();
59 buf.text(gutter.saturating_sub(len), tick.row, &tick.label, axis);
60 }
61
62 match scene.marks.first() {
66 Some(SceneMark::Bars { .. }) => {
67 for mark in &scene.marks {
68 if let SceneMark::Bars { bars, direction } = mark {
69 rasterize_bars(
70 &mut buf, bars, *direction, opts, gutter, top, plot_w, plot_h,
71 );
72 }
73 }
74 }
75 _ => rasterize_xy(&mut buf, &scene.marks, opts, gutter, top, plot_w, plot_h),
76 }
77
78 let axis_row = top + plot_h;
80 buf.set(gutter, axis_row, '└', axis);
81 for c in 0..plot_w {
82 buf.set(gutter + 1 + c, axis_row, '─', axis);
83 }
84 for &c in &scene.x_axis.tick_cols {
85 if c < plot_w {
86 buf.set(gutter + 1 + c, axis_row, '┴', axis);
87 }
88 }
89 for label in &scene.x_axis.labels {
90 buf.text(label.col, label.row, &label.text, axis);
91 }
92
93 Rendered {
94 text: buf.to_ansi(opts.color),
95 meta: scene.meta(),
96 }
97}
98
99#[allow(clippy::too_many_arguments)]
100fn rasterize_bars(
101 buf: &mut Buffer,
102 bars: &[crate::scene::Bar],
103 direction: BarDirection,
104 opts: &RasterOptions,
105 gutter: usize,
106 top: usize,
107 plot_w: usize,
108 plot_h: usize,
109) {
110 match opts.bar_style {
111 BarStyle::Dots => {
112 let mut canvas = PixelCanvas::new(plot_w, plot_h, opts.marker);
113 match direction {
114 BarDirection::Vertical => {
115 let ph = (plot_h * 4) as i64;
119 for bar in bars {
120 let x0 = (bar.x0 * plot_w as f64).round() as usize;
121 let bar_w = (bar.w * plot_w as f64).round() as usize;
122 let level = (bar.h * ph as f64).round() as i64;
123 for px in (x0 * 2) as i64..((x0 + bar_w) * 2) as i64 {
124 for py in (ph - level)..ph {
125 canvas.set(px, py, bar.color);
126 }
127 }
128 }
129 }
130 BarDirection::Horizontal => {
131 for bar in bars {
134 let px_lo = (bar.x0 * plot_w as f64).round() as i64 * 2;
135 let px_hi = ((bar.x0 + bar.w) * plot_w as f64).round() as i64 * 2;
136 let py_lo = (bar.y0 * plot_h as f64).round() as i64 * 4;
137 let py_hi = ((bar.y0 + bar.h) * plot_h as f64).round() as i64 * 4;
138 for px in px_lo..px_hi {
139 for py in py_lo..py_hi {
140 canvas.set(px, py, bar.color);
141 }
142 }
143 }
144 }
145 }
146 for cy in 0..plot_h {
147 for cx in 0..plot_w {
148 if let Some((ch, color)) = canvas.cell(cx, cy) {
149 buf.set(gutter + 1 + cx, top + cy, ch, Some(color));
150 }
151 }
152 }
153 }
154 BarStyle::Blocks => match direction {
155 BarDirection::Vertical => {
156 for bar in bars {
158 let x0 = (bar.x0 * plot_w as f64).round() as usize;
159 let bar_w = (bar.w * plot_w as f64).round() as usize;
160 let level = (bar.h * (plot_h * 8) as f64).round() as i64;
161 for r in 0..plot_h {
162 let fill = level - ((plot_h - 1 - r) * 8) as i64;
163 if fill <= 0 {
164 continue;
165 }
166 let ch = if fill >= 8 {
167 '█'
168 } else {
169 EIGHTHS[fill as usize]
170 };
171 for c in 0..bar_w {
172 buf.set(gutter + 1 + x0 + c, top + r, ch, Some(bar.color));
173 }
174 }
175 }
176 }
177 BarDirection::Horizontal => {
178 for bar in bars {
182 let x0 = (bar.x0 * plot_w as f64).round() as usize;
183 let r0 = (bar.y0 * plot_h as f64).round() as usize;
184 let r1 = ((bar.y0 + bar.h) * plot_h as f64).round() as usize;
185 let level = (bar.w * (plot_w * 8) as f64).round() as i64;
186 for c in 0..plot_w {
187 let fill = level - (c * 8) as i64;
188 if fill <= 0 {
189 continue;
190 }
191 let ch = if fill >= 8 {
192 '█'
193 } else {
194 LEFT_EIGHTHS[fill as usize]
195 };
196 for r in r0..r1 {
197 buf.set(gutter + 1 + x0 + c, top + r, ch, Some(bar.color));
198 }
199 }
200 }
201 }
202 },
203 }
204}
205
206fn rasterize_xy(
212 buf: &mut Buffer,
213 marks: &[SceneMark],
214 opts: &RasterOptions,
215 gutter: usize,
216 top: usize,
217 plot_w: usize,
218 plot_h: usize,
219) {
220 let mut canvas = PixelCanvas::new(plot_w, plot_h, opts.marker);
221 let (pw, ph) = (canvas.pixel_width() as i64, canvas.pixel_height() as i64);
222 let px = |fx: f64| (fx * (pw - 1) as f64).round() as i64;
223 let py = |fy: f64| (fy * (ph - 1) as f64).round() as i64;
224
225 for mark in marks {
226 let (series, points, fill, points_mark) = match mark {
227 SceneMark::Points { series, points } => (series, points, false, true),
228 SceneMark::Path { series, points } => (series, points, false, false),
229 SceneMark::Fill { series, points } => (series, points, true, false),
230 SceneMark::Bars { .. } => continue,
231 };
232 let color = series.color;
233
234 if points_mark {
235 for p in points {
237 let (cx, cy) = (px(p[0]), py(p[1]));
238 for dx in 0..2 {
239 for dy in 0..2 {
240 canvas.set(cx + dx, cy + dy, color);
241 }
242 }
243 }
244 continue;
245 }
246
247 if fill {
250 for w in points.windows(2) {
251 let (x0, y0, x1, y1) = (px(w[0][0]), py(w[0][1]), px(w[1][0]), py(w[1][1]));
252 for x in x0..=x1 {
253 let t = if x1 == x0 {
254 0.0
255 } else {
256 (x - x0) as f64 / (x1 - x0) as f64
257 };
258 let ytop = (y0 as f64 + t * (y1 - y0) as f64).round() as i64;
259 for yy in ytop..ph {
260 canvas.set(x, yy, color);
261 }
262 }
263 }
264 }
265 if points.len() == 1 {
267 let (cx, cy) = (px(points[0][0]), py(points[0][1]));
268 canvas.set(cx, cy, color);
269 canvas.set(cx + 1, cy, color);
270 }
271 for w in points.windows(2) {
272 canvas.line(px(w[0][0]), py(w[0][1]), px(w[1][0]), py(w[1][1]), color);
273 }
274 }
275
276 for cy in 0..plot_h {
277 for cx in 0..plot_w {
278 if let Some((ch, color)) = canvas.cell(cx, cy) {
279 buf.set(gutter + 1 + cx, top + cy, ch, Some(color));
280 }
281 }
282 }
283}
284
285#[derive(Debug, Clone, Copy, PartialEq, Eq)]
286pub struct Rgb(pub u8, pub u8, pub u8);
287
288impl Rgb {
289 pub fn hex(&self) -> String {
290 format!("#{:02x}{:02x}{:02x}", self.0, self.1, self.2)
291 }
292}
293
294impl serde::Serialize for Rgb {
295 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
296 s.serialize_str(&self.hex())
297 }
298}
299
300#[derive(Debug, Clone, Copy, PartialEq, Eq)]
301pub enum Marker {
302 Braille,
303 Octant,
304}
305
306pub struct PixelCanvas {
307 width_cells: usize,
308 height_cells: usize,
309 bits: Vec<u8>,
310 colors: Vec<Option<Rgb>>,
311 marker: Marker,
312}
313
314impl PixelCanvas {
315 pub fn new(width_cells: usize, height_cells: usize, marker: Marker) -> Self {
316 PixelCanvas {
317 width_cells,
318 height_cells,
319 bits: vec![0; width_cells * height_cells],
320 colors: vec![None; width_cells * height_cells],
321 marker,
322 }
323 }
324
325 pub fn pixel_width(&self) -> usize {
326 self.width_cells * 2
327 }
328
329 pub fn pixel_height(&self) -> usize {
330 self.height_cells * 4
331 }
332
333 pub fn set(&mut self, x: i64, y: i64, color: Rgb) {
334 if x < 0 || y < 0 {
335 return;
336 }
337 let (x, y) = (x as usize, y as usize);
338 if x >= self.pixel_width() || y >= self.pixel_height() {
339 return;
340 }
341 let idx = (y / 4) * self.width_cells + x / 2;
342 self.bits[idx] |= 1 << ((y % 4) * 2 + (x % 2));
343 self.colors[idx] = Some(color);
344 }
345
346 pub fn line(&mut self, x0: i64, y0: i64, x1: i64, y1: i64, color: Rgb) {
348 let dx = (x1 - x0).abs();
349 let dy = -(y1 - y0).abs();
350 let sx = if x0 < x1 { 1 } else { -1 };
351 let sy = if y0 < y1 { 1 } else { -1 };
352 let mut err = dx + dy;
353 let (mut x, mut y) = (x0, y0);
354 loop {
355 self.set(x, y, color);
356 if x == x1 && y == y1 {
357 break;
358 }
359 let e2 = 2 * err;
360 if e2 >= dy {
361 err += dy;
362 x += sx;
363 }
364 if e2 <= dx {
365 err += dx;
366 y += sy;
367 }
368 }
369 }
370
371 pub fn cell(&self, cx: usize, cy: usize) -> Option<(char, Rgb)> {
373 let idx = cy * self.width_cells + cx;
374 let bits = self.bits[idx];
375 if bits == 0 {
376 return None;
377 }
378 let ch = match self.marker {
379 Marker::Braille => braille_char(bits),
380 Marker::Octant => OCTANTS[bits as usize],
381 };
382 Some((ch, self.colors[idx].unwrap_or(Rgb(255, 255, 255))))
383 }
384}
385
386const BRAILLE_DOT: [[u16; 2]; 4] = [[0x01, 0x08], [0x02, 0x10], [0x04, 0x20], [0x40, 0x80]];
389
390fn braille_char(bits: u8) -> char {
391 let mut v: u16 = 0;
392 for (row, cols) in BRAILLE_DOT.iter().enumerate() {
393 for (col, dot) in cols.iter().enumerate() {
394 if bits & (1 << (row * 2 + col)) != 0 {
395 v |= dot;
396 }
397 }
398 }
399 char::from_u32(0x2800 + u32::from(v)).expect("U+2800..=U+28FF are valid chars")
400}
401
402#[rustfmt::skip]
405pub const OCTANTS: [char; 256] = [
406 ' ', '', '', '🮂', '', '▘', '', '', '', '', '▝', '', '', '', '', '▀', '', '', '',
407 '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
408 '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
409 '', '', '', '', '', '', '🮅', '', '', '', '', '', '', '', '', '', '', '', '',
410 '', '', '', '', '▖', '', '', '', '', '▌', '', '', '', '', '▞', '', '', '', '',
411 '▛', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
412 '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
413 '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
414 '', '', '', '', '', '', '', '', '▗', '', '', '', '', '▚', '', '', '', '', '▐',
415 '', '', '', '', '▜', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
416 '', '', '▂', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
417 '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
418 '', '', '', '', '', '', '', '', '', '', '', '', '▄', '', '', '', '', '▙', '',
419 '', '', '', '▟', '', '▆', '', '', '█',
420];
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425
426 #[test]
427 fn braille_bit_mapping() {
428 assert_eq!(braille_char(0b0000_0001), '⠁'); assert_eq!(braille_char(0b1111_1111), '⣿'); }
431
432 #[test]
433 fn octant_table_landmarks() {
434 assert_eq!(OCTANTS[0b0000_1111], '▀'); assert_eq!(OCTANTS[255], '█');
436 }
437
438 #[test]
439 fn canvas_maps_pixels_to_cells() {
440 let mut c = PixelCanvas::new(2, 1, Marker::Braille);
441 c.set(0, 0, Rgb(255, 0, 0));
442 assert_eq!(c.cell(0, 0), Some(('⠁', Rgb(255, 0, 0))));
443 assert_eq!(c.cell(1, 0), None);
444 }
445
446 fn bar_rows(
451 direction: BarDirection,
452 bar_style: BarStyle,
453 plot_w: usize,
454 plot_h: usize,
455 bar: crate::scene::Bar,
456 ) -> Vec<String> {
457 let mut buf = Buffer::new(plot_w + 1, plot_h);
458 let opts = RasterOptions {
459 marker: Marker::Octant,
460 bar_style,
461 color: false,
462 };
463 rasterize_bars(&mut buf, &[bar], direction, &opts, 0, 0, plot_w, plot_h);
464 let out = buf.to_ansi(false);
465 let mut rows: Vec<String> = out.split('\n').map(str::to_string).collect();
466 rows.pop(); rows
468 }
469
470 #[test]
471 fn rasterize_bars_glyph_rows() {
472 use crate::scene::Bar;
473 use BarDirection::{Horizontal, Vertical};
474
475 struct Case {
476 name: &'static str,
477 direction: BarDirection,
478 style: BarStyle,
479 plot_w: usize,
480 plot_h: usize,
481 bar: Bar,
482 expected: &'static [&'static str],
483 }
484
485 let color = Rgb(1, 2, 3);
486 let cases = [
487 Case {
490 name: "vertical blocks",
491 direction: Vertical,
492 style: BarStyle::Blocks,
493 plot_w: 1,
494 plot_h: 2,
495 bar: Bar {
496 x0: 0.0,
497 y0: 0.25,
498 w: 1.0,
499 h: 0.75,
500 color,
501 },
502 expected: &[" ▄", " █"],
503 },
504 Case {
507 name: "horizontal blocks",
508 direction: Horizontal,
509 style: BarStyle::Blocks,
510 plot_w: 2,
511 plot_h: 1,
512 bar: Bar {
513 x0: 0.0,
514 y0: 0.0,
515 w: 0.75,
516 h: 1.0,
517 color,
518 },
519 expected: &[" █▌"],
520 },
521 Case {
524 name: "vertical dots",
525 direction: Vertical,
526 style: BarStyle::Dots,
527 plot_w: 1,
528 plot_h: 2,
529 bar: Bar {
530 x0: 0.0,
531 y0: 0.5,
532 w: 1.0,
533 h: 0.5,
534 color,
535 },
536 expected: &["", " █"],
537 },
538 Case {
540 name: "horizontal dots",
541 direction: Horizontal,
542 style: BarStyle::Dots,
543 plot_w: 2,
544 plot_h: 1,
545 bar: Bar {
546 x0: 0.0,
547 y0: 0.0,
548 w: 1.0,
549 h: 1.0,
550 color,
551 },
552 expected: &[" ██"],
553 },
554 ];
555
556 for case in cases {
557 let rows = bar_rows(
558 case.direction,
559 case.style,
560 case.plot_w,
561 case.plot_h,
562 case.bar,
563 );
564 assert_eq!(rows, case.expected, "case: {}", case.name);
565 }
566 }
567}