use crate::visualize::theme::graph;
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
Frame,
};
const BRAILLE_OFFSETS: [[u8; 4]; 2] = [
[0, 1, 2, 6], [3, 4, 5, 7], ];
const BRAILLE_BASE: char = '\u{2800}';
pub fn render(f: &mut Frame, points: &[(f64, f64, u8)], cluster_colors: &[Color], area: Rect) {
let buf = f.buffer_mut();
render_to_buffer(buf, points, cluster_colors, area);
}
fn build_dot_matrix(
points: &[(f64, f64, u8)],
dot_width: usize,
dot_height: usize,
) -> Vec<Vec<Option<u8>>> {
let mut dots: Vec<Vec<Option<u8>>> = vec![vec![None; dot_height]; dot_width];
for (x, y, cluster_id) in points {
let x = x.clamp(0.0, 1.0);
let y = y.clamp(0.0, 1.0);
let dot_x = ((x * (dot_width - 1) as f64).round() as usize).min(dot_width - 1);
let dot_y = (((1.0 - y) * (dot_height - 1) as f64).round() as usize).min(dot_height - 1);
dots[dot_x][dot_y] = Some(*cluster_id);
}
dots
}
fn resolve_cluster_color(cluster_id: u8, cluster_colors: &[Color]) -> Color {
if cluster_id == 255 {
graph::OUTLIER
} else {
cluster_colors.get(cluster_id as usize).copied().unwrap_or(Color::White)
}
}
fn build_cell_braille(
dots: &[Vec<Option<u8>>],
dot_x_start: usize,
dot_y_start: usize,
dot_width: usize,
dot_height: usize,
cluster_colors: &[Color],
) -> (u8, Option<Color>) {
let mut braille_bits: u8 = 0;
let mut cell_color: Option<Color> = None;
#[allow(clippy::needless_range_loop)]
for dx in 0..2 {
#[allow(clippy::needless_range_loop)]
for dy in 0..4 {
let dot_x = dot_x_start + dx;
let dot_y = dot_y_start + dy;
if dot_x >= dot_width || dot_y >= dot_height {
continue;
}
let Some(cluster_id) = dots[dot_x][dot_y] else {
continue;
};
braille_bits |= 1 << BRAILLE_OFFSETS[dx][dy];
cell_color = Some(resolve_cluster_color(cluster_id, cluster_colors));
}
}
(braille_bits, cell_color)
}
pub fn render_to_buffer(
buf: &mut Buffer,
points: &[(f64, f64, u8)],
cluster_colors: &[Color],
area: Rect,
) {
if area.width < 2 || area.height < 2 {
return;
}
let dot_width = (area.width as usize) * 2;
let dot_height = (area.height as usize) * 4;
let dots = build_dot_matrix(points, dot_width, dot_height);
for cell_y in 0..area.height {
for cell_x in 0..area.width {
let dot_x_start = (cell_x as usize) * 2;
let dot_y_start = (cell_y as usize) * 4;
let (braille_bits, cell_color) = build_cell_braille(
&dots,
dot_x_start,
dot_y_start,
dot_width,
dot_height,
cluster_colors,
);
if braille_bits != 0 {
let ch = char::from_u32(BRAILLE_BASE as u32 + braille_bits as u32)
.unwrap_or(BRAILLE_BASE);
let style = Style::default().fg(cell_color.unwrap_or(Color::White));
let buf_x = area.x + cell_x;
let buf_y = area.y + cell_y;
buf[(buf_x, buf_y)].set_char(ch).set_style(style);
}
}
}
}
pub fn render_outlier_markers(f: &mut Frame, points: &[(f64, f64, u8)], area: Rect) {
let buf = f.buffer_mut();
for (x, y, cluster_id) in points {
if *cluster_id != 255 {
continue; }
let cell_x =
((x.clamp(0.0, 1.0) * (area.width - 1) as f64).round() as u16).min(area.width - 1);
let cell_y = (((1.0 - y.clamp(0.0, 1.0)) * (area.height - 1) as f64).round() as u16)
.min(area.height - 1);
let buf_x = area.x + cell_x;
let buf_y = area.y + cell_y;
buf[(buf_x, buf_y)].set_char('×').set_style(Style::default().fg(graph::OUTLIER));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_braille_base() {
assert_eq!(BRAILLE_BASE, '⠀');
}
#[test]
fn test_braille_encoding() {
let ch = char::from_u32(BRAILLE_BASE as u32 + 1).unwrap();
assert_eq!(ch, '⠁');
let ch = char::from_u32(BRAILLE_BASE as u32 + 8).unwrap();
assert_eq!(ch, '⠈');
let ch = char::from_u32(BRAILLE_BASE as u32 + 255).unwrap();
assert_eq!(ch, '⣿');
}
#[test]
fn test_render_empty() {
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 5));
let points: Vec<(f64, f64, u8)> = vec![];
let colors = vec![Color::Cyan];
render_to_buffer(&mut buf, &points, &colors, Rect::new(0, 0, 10, 5));
}
#[test]
fn test_render_single_point() {
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 5));
let points = vec![(0.5, 0.5, 0u8)];
let colors = vec![Color::Cyan];
render_to_buffer(&mut buf, &points, &colors, Rect::new(0, 0, 10, 5));
let has_content = (0..10)
.flat_map(|x| (0..5).map(move |y| (x, y)))
.any(|(x, y)| buf[(x, y)].symbol() != " ");
assert!(has_content);
}
#[test]
fn test_render_corners() {
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 10));
let points = vec![
(0.0, 0.0, 0u8), (1.0, 1.0, 1u8), (0.0, 1.0, 2u8), (1.0, 0.0, 3u8), ];
let colors = vec![Color::Red, Color::Green, Color::Blue, Color::Yellow];
render_to_buffer(&mut buf, &points, &colors, Rect::new(0, 0, 20, 10));
}
#[test]
fn test_render_outlier_color() {
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 5));
let points = vec![(0.5, 0.5, 255u8)]; let colors = vec![Color::Cyan];
render_to_buffer(&mut buf, &points, &colors, Rect::new(0, 0, 10, 5));
}
#[test]
fn test_render_small_area() {
let mut buf = Buffer::empty(Rect::new(0, 0, 1, 1));
let points = vec![(0.5, 0.5, 0u8)];
let colors = vec![Color::Cyan];
render_to_buffer(&mut buf, &points, &colors, Rect::new(0, 0, 1, 1));
}
#[test]
fn test_render_overlapping_points() {
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 5));
let points = vec![(0.5, 0.5, 0u8), (0.5, 0.5, 1u8), (0.5, 0.5, 2u8)];
let colors = vec![Color::Red, Color::Green, Color::Blue];
render_to_buffer(&mut buf, &points, &colors, Rect::new(0, 0, 10, 5));
}
#[test]
fn test_render_with_offset_area() {
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 10));
let points = vec![(0.5, 0.5, 0u8)];
let colors = vec![Color::Cyan];
render_to_buffer(&mut buf, &points, &colors, Rect::new(5, 2, 10, 5));
}
#[test]
fn test_render_missing_color() {
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 5));
let points = vec![(0.5, 0.5, 10u8)]; let colors = vec![Color::Cyan];
render_to_buffer(&mut buf, &points, &colors, Rect::new(0, 0, 10, 5));
}
#[test]
fn test_render_outlier_markers() {
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 10));
let points = vec![
(0.2, 0.3, 0u8), (0.5, 0.5, 255u8), (0.8, 0.7, 255u8), (0.1, 0.9, 1u8), ];
render_outlier_markers_to_buffer(&mut buf, &points, Rect::new(0, 0, 20, 10));
}
#[test]
fn test_render_outlier_markers_empty() {
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 5));
let points: Vec<(f64, f64, u8)> = vec![];
render_outlier_markers_to_buffer(&mut buf, &points, Rect::new(0, 0, 10, 5));
}
#[test]
fn test_render_outlier_markers_no_outliers() {
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 5));
let points = vec![(0.5, 0.5, 0u8), (0.3, 0.7, 1u8)];
render_outlier_markers_to_buffer(&mut buf, &points, Rect::new(0, 0, 10, 5));
}
#[test]
fn test_render_points_at_boundaries() {
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 5));
let points = vec![
(-0.1, -0.1, 0u8), (1.1, 1.1, 1u8), ];
let colors = vec![Color::Red, Color::Green];
render_to_buffer(&mut buf, &points, &colors, Rect::new(0, 0, 10, 5));
}
#[test]
fn test_braille_offsets() {
assert_eq!(BRAILLE_OFFSETS[0][0], 0);
assert_eq!(BRAILLE_OFFSETS[0][1], 1);
assert_eq!(BRAILLE_OFFSETS[0][2], 2);
assert_eq!(BRAILLE_OFFSETS[0][3], 6);
assert_eq!(BRAILLE_OFFSETS[1][0], 3);
assert_eq!(BRAILLE_OFFSETS[1][1], 4);
assert_eq!(BRAILLE_OFFSETS[1][2], 5);
assert_eq!(BRAILLE_OFFSETS[1][3], 7);
}
fn render_outlier_markers_to_buffer(buf: &mut Buffer, points: &[(f64, f64, u8)], area: Rect) {
for (x, y, cluster_id) in points {
if *cluster_id != 255 {
continue;
}
let cell_x =
((x.clamp(0.0, 1.0) * (area.width - 1) as f64).round() as u16).min(area.width - 1);
let cell_y = (((1.0 - y.clamp(0.0, 1.0)) * (area.height - 1) as f64).round() as u16)
.min(area.height - 1);
let buf_x = area.x + cell_x;
let buf_y = area.y + cell_y;
buf[(buf_x, buf_y)].set_char('×').set_style(Style::default().fg(graph::OUTLIER));
}
}
}