use rustc_hash::FxHashMap;
use serde::Serialize;
use ts_rs::TS;
use crate::options::density::default_gate_colors;
use crate::options::{DensityPlotOptions, PlotOptions};
use crate::plots::PlotType;
use crate::scatter_data::ScatterPlotData;
#[derive(Clone, Debug, Serialize, TS)]
#[ts(export)]
pub struct RawPixelData {
pub x: f32,
pub y: f32,
pub r: u8,
pub g: u8,
pub b: u8,
}
#[derive(Clone, Serialize, TS)]
#[ts(export)]
pub struct BinaryPixelChunk {
pub pixels: Vec<u8>,
pub width: u32,
pub height: u32,
pub offset_x: u32,
pub offset_y: u32,
pub total_width: u32,
pub total_height: u32,
}
pub fn create_binary_chunk(
pixels: &[RawPixelData],
plot_width: u32,
plot_height: u32,
_chunk_size: u32,
) -> Option<BinaryPixelChunk> {
if pixels.is_empty() {
return None;
}
let mut min_x = pixels[0].x;
let mut max_x = pixels[0].x;
let mut min_y = pixels[0].y;
let mut max_y = pixels[0].y;
for pixel in pixels {
min_x = min_x.min(pixel.x);
max_x = max_x.max(pixel.x);
min_y = min_y.min(pixel.y);
max_y = max_y.max(pixel.y);
}
let chunk_x = min_x.max(0.0).floor() as u32;
let chunk_y = min_y.max(0.0).floor() as u32;
let chunk_width = (max_x - min_x).max(0.0).floor() as u32 + 1;
let chunk_height = (max_y - min_y).max(0.0).floor() as u32 + 1;
let total_px: usize = (chunk_width as usize)
.saturating_mul(chunk_height as usize)
.min((plot_width as usize).saturating_mul(plot_height as usize));
let buf_len = total_px.saturating_mul(3);
let mut rgb_data = vec![0u8; buf_len];
for pixel in pixels {
let local_x = (pixel.x - min_x).round().max(0.0) as u32;
let local_y = (pixel.y - min_y).round().max(0.0) as u32;
if local_x < chunk_width && local_y < chunk_height {
let idx = ((local_y as usize)
.saturating_mul(chunk_width as usize)
.saturating_add(local_x as usize))
.saturating_mul(3);
if idx + 2 < rgb_data.len() {
rgb_data[idx] = pixel.r;
rgb_data[idx + 1] = pixel.g;
rgb_data[idx + 2] = pixel.b;
}
}
}
Some(BinaryPixelChunk {
pixels: rgb_data,
width: chunk_width,
height: chunk_height,
offset_x: chunk_x,
offset_y: chunk_y,
total_width: plot_width,
total_height: plot_height,
})
}
pub fn scatter_to_pixels_overlay(
data: &ScatterPlotData,
width: usize,
height: usize,
options: &DensityPlotOptions,
) -> Vec<RawPixelData> {
let gate_ids = match &data.gate_ids {
Some(ids) => ids,
None => return scatter_to_pixels(data.xy(), width, height, options),
};
let colors = if options.gate_colors.is_empty() {
default_gate_colors()
} else {
options.gate_colors.clone()
};
let point_size = options.point_size.max(0.1).min(4.0);
let radius_px = if point_size < 0.5 {
0
} else {
(point_size.ceil() as usize).max(1)
};
let scale_x = width as f32 / (*options.x_axis.range.end() - *options.x_axis.range.start());
let scale_y = height as f32 / (*options.y_axis.range.end() - *options.y_axis.range.start());
let mut pixels = Vec::new();
for (i, &(x, y)) in data.xy().iter().enumerate() {
let gate_id = gate_ids.get(i).copied().unwrap_or(0) as usize;
let (r, g, b) = colors
.get(gate_id)
.copied()
.unwrap_or((60, 60, 60));
let pixel_x = (((x - *options.x_axis.range.start()) * scale_x).floor() as isize)
.clamp(0, (width - 1) as isize) as usize;
let pixel_y = (((y - *options.y_axis.range.start()) * scale_y).floor() as isize)
.clamp(0, (height - 1) as isize) as usize;
for dy in -(radius_px as i32)..=(radius_px as i32) {
for dx in -(radius_px as i32)..=(radius_px as i32) {
let px = (pixel_x as i32 + dx).clamp(0, (width - 1) as i32) as usize;
let py = (pixel_y as i32 + dy).clamp(0, (height - 1) as i32) as usize;
let data_x = (px as f32 / scale_x) + *options.x_axis.range.start();
let data_y = (py as f32 / scale_y) + *options.y_axis.range.start();
pixels.push(RawPixelData {
x: data_x,
y: data_y,
r,
g,
b,
});
}
}
}
pixels
}
pub fn scatter_to_pixels_colored(
data: &ScatterPlotData,
width: usize,
height: usize,
options: &DensityPlotOptions,
) -> Vec<RawPixelData> {
let z_values = match &data.z_values {
Some(z) => z,
None => return scatter_to_pixels(data.xy(), width, height, options),
};
let (z_min, z_max) = match options.z_range {
Some((min, max)) => (min, max),
None => {
let min = z_values
.iter()
.copied()
.fold(f32::INFINITY, f32::min);
let max = z_values
.iter()
.copied()
.fold(f32::NEG_INFINITY, f32::max);
if min >= max {
(min, min + 1.0)
} else {
(min, max)
}
}
};
let z_range = z_max - z_min;
let point_size = options.point_size.max(0.1).min(4.0);
let radius_px = if point_size < 0.5 {
0
} else {
(point_size.ceil() as usize).max(1)
};
let scale_x = width as f32 / (*options.x_axis.range.end() - *options.x_axis.range.start());
let scale_y = height as f32 / (*options.y_axis.range.end() - *options.y_axis.range.start());
let mut pixels = Vec::new();
for (i, &(x, y)) in data.xy().iter().enumerate() {
let z = z_values.get(i).copied().unwrap_or(0.0);
let t = (z - z_min) / z_range;
let normalized = t.max(0.0).min(1.0);
let color = options.colormap.map(normalized);
let (r, g, b) = (color.0, color.1, color.2);
let pixel_x = (((x - *options.x_axis.range.start()) * scale_x).floor() as isize)
.clamp(0, (width - 1) as isize) as usize;
let pixel_y = (((y - *options.y_axis.range.start()) * scale_y).floor() as isize)
.clamp(0, (height - 1) as isize) as usize;
for dy in -(radius_px as i32)..=(radius_px as i32) {
for dx in -(radius_px as i32)..=(radius_px as i32) {
let px = (pixel_x as i32 + dx).clamp(0, (width - 1) as i32) as usize;
let py = (pixel_y as i32 + dy).clamp(0, (height - 1) as i32) as usize;
let data_x = (px as f32 / scale_x) + *options.x_axis.range.start();
let data_y = (py as f32 / scale_y) + *options.y_axis.range.start();
pixels.push(RawPixelData {
x: data_x,
y: data_y,
r,
g,
b,
});
}
}
}
pixels
}
pub fn scatter_to_pixels(
data: &[(f32, f32)],
width: usize,
height: usize,
options: &DensityPlotOptions,
) -> Vec<RawPixelData> {
let point_size = options.point_size.max(0.1).min(4.0);
let radius_px = if point_size < 0.5 {
0
} else {
(point_size.ceil() as usize).max(1)
};
let scale_x = width as f32 / (*options.x_axis.range.end() - *options.x_axis.range.start());
let scale_y = height as f32 / (*options.y_axis.range.end() - *options.y_axis.range.start());
let (r, g, b) = (60u8, 60u8, 60u8);
let mut pixels = Vec::with_capacity(data.len() * (2 * radius_px + 1).pow(2));
for &(x, y) in data {
let pixel_x = (((x - *options.x_axis.range.start()) * scale_x).floor() as isize)
.clamp(0, (width - 1) as isize) as usize;
let pixel_y = (((y - *options.y_axis.range.start()) * scale_y).floor() as isize)
.clamp(0, (height - 1) as isize) as usize;
for dy in -(radius_px as i32)..=(radius_px as i32) {
for dx in -(radius_px as i32)..=(radius_px as i32) {
let px = (pixel_x as i32 + dx).clamp(0, (width - 1) as i32) as usize;
let py = (pixel_y as i32 + dy).clamp(0, (height - 1) as i32) as usize;
let data_x = (px as f32 / scale_x) + *options.x_axis.range.start();
let data_y = (py as f32 / scale_y) + *options.y_axis.range.start();
pixels.push(RawPixelData {
x: data_x,
y: data_y,
r,
g,
b,
});
}
}
}
pixels
}
pub fn calculate_plot_pixels(
data: &ScatterPlotData,
width: usize,
height: usize,
options: &DensityPlotOptions,
) -> Vec<RawPixelData> {
let xy = data.xy();
match options.plot_type.canonical() {
PlotType::ScatterSolid | PlotType::Dot => {
scatter_to_pixels(xy, width, height, options)
}
PlotType::ScatterOverlay => {
if data.has_gates() {
scatter_to_pixels_overlay(data, width, height, options)
} else {
scatter_to_pixels(xy, width, height, options)
}
}
PlotType::ScatterColoredContinuous => {
if data.has_z() {
scatter_to_pixels_colored(data, width, height, options)
} else {
calculate_density_per_pixel(xy, width, height, options)
}
}
PlotType::Density
| PlotType::Contour
| PlotType::ContourOverlay
| PlotType::Zebra
| PlotType::Histogram => calculate_density_per_pixel(xy, width, height, options),
}
}
pub fn calculate_plot_pixels_cancelable(
data: &ScatterPlotData,
width: usize,
height: usize,
options: &DensityPlotOptions,
should_cancel: impl FnMut() -> bool,
) -> Option<Vec<RawPixelData>> {
let xy = data.xy();
match options.plot_type.canonical() {
PlotType::ScatterSolid | PlotType::Dot => {
Some(scatter_to_pixels(xy, width, height, options))
}
PlotType::ScatterOverlay => Some(if data.has_gates() {
scatter_to_pixels_overlay(data, width, height, options)
} else {
scatter_to_pixels(xy, width, height, options)
}),
PlotType::ScatterColoredContinuous => {
if data.has_z() {
Some(scatter_to_pixels_colored(data, width, height, options))
} else {
calculate_density_per_pixel_cancelable(xy, width, height, options, should_cancel)
}
}
PlotType::Density
| PlotType::Contour
| PlotType::ContourOverlay
| PlotType::Zebra
| PlotType::Histogram => {
calculate_density_per_pixel_cancelable(xy, width, height, options, should_cancel)
}
}
}
pub fn calculate_density_per_pixel(
data: &[(f32, f32)],
width: usize,
height: usize,
options: &DensityPlotOptions,
) -> Vec<RawPixelData> {
calculate_density_per_pixel_cancelable(data, width, height, options, || false).expect(
"calculate_density_per_pixel_cancelable returned None when cancellation is disabled",
)
}
pub fn calculate_density_per_pixel_cancelable(
data: &[(f32, f32)],
width: usize,
height: usize,
options: &DensityPlotOptions,
should_cancel: impl FnMut() -> bool,
) -> Option<Vec<RawPixelData>> {
calculate_density_per_pixel_cpu(data, width, height, options, should_cancel)
}
pub fn calculate_density_per_pixel_batch(
requests: &[(ScatterPlotData, DensityPlotOptions)],
) -> Vec<Vec<RawPixelData>> {
calculate_density_per_pixel_batch_cancelable(requests, || false)
.expect("calculate_density_per_pixel_batch_cancelable returned None when cancellation is disabled")
}
pub fn calculate_density_per_pixel_batch_cancelable(
requests: &[(ScatterPlotData, DensityPlotOptions)],
mut should_cancel: impl FnMut() -> bool,
) -> Option<Vec<Vec<RawPixelData>>> {
if should_cancel() {
return None;
}
Some(calculate_density_per_pixel_batch_cpu(requests))
}
fn calculate_density_per_pixel_batch_cpu(
requests: &[(ScatterPlotData, DensityPlotOptions)],
) -> Vec<Vec<RawPixelData>> {
requests
.iter()
.map(|(data, options)| {
let base = options.base();
calculate_plot_pixels(data, base.width as usize, base.height as usize, options)
})
.collect()
}
fn calculate_density_per_pixel_cpu(
data: &[(f32, f32)],
width: usize,
height: usize,
options: &DensityPlotOptions,
mut should_cancel: impl FnMut() -> bool,
) -> Option<Vec<RawPixelData>> {
let scale_x = width as f32 / (*options.x_axis.range.end() - *options.x_axis.range.start());
let scale_y = height as f32 / (*options.y_axis.range.end() - *options.y_axis.range.start());
let point_size = options.point_size.max(0.1).min(4.0);
let radius_px = if point_size < 0.5 {
0
} else {
(point_size.ceil() as usize).max(1)
};
let build_start = std::time::Instant::now();
let mut density = vec![0.0f32; width * height];
let mut last_progress = std::time::Instant::now();
for (i, &(x, y)) in data.iter().enumerate() {
if (i % 250_000) == 0 {
if should_cancel() {
eprintln!(
" ├─ Density build cancelled after {} / {} points",
i,
data.len()
);
return None;
}
if last_progress.elapsed().as_secs_f64() >= 2.0 {
eprintln!(
" ├─ Density build progress: {} / {} points",
i,
data.len()
);
last_progress = std::time::Instant::now();
}
}
let pixel_x = (((x - *options.x_axis.range.start()) * scale_x).floor() as isize)
.clamp(0, (width - 1) as isize) as usize;
let pixel_y = (((y - *options.y_axis.range.start()) * scale_y).floor() as isize)
.clamp(0, (height - 1) as isize) as usize;
for dy in -(radius_px as i32)..=(radius_px as i32) {
for dx in -(radius_px as i32)..=(radius_px as i32) {
let px = (pixel_x as i32 + dx).clamp(0, (width - 1) as i32) as usize;
let py = (pixel_y as i32 + dy).clamp(0, (height - 1) as i32) as usize;
let idx = py * width + px;
density[idx] += 1.0;
}
}
}
let mut density_map: FxHashMap<(usize, usize), f32> = FxHashMap::default();
density_map.reserve(width * height / 10);
for (idx, &count) in density.iter().enumerate() {
if count > 0.0 {
let px = idx % width;
let py = idx / width;
density_map.insert((px, py), count);
}
}
eprintln!(
" ├─ Density map building: {:?} ({} unique pixels from {} total)",
build_start.elapsed(),
density_map.len(),
width * height
);
let log_start = std::time::Instant::now();
for (_, count) in density_map.iter_mut() {
*count = (*count + 1.0).log10();
}
eprintln!(" ├─ Log transform: {:?}", log_start.elapsed());
let max_start = std::time::Instant::now();
let max_density_log = density_map
.values()
.fold(0.0f32, |max, &val| max.max(val))
.max(1.0); eprintln!(" ├─ Find max: {:?}", max_start.elapsed());
let color_start = std::time::Instant::now();
let colored_pixels: Vec<RawPixelData> = density_map
.iter()
.map(|(&(pixel_x, pixel_y), &dens)| {
let normalized_density = dens / max_density_log;
let color = options.colormap.map(normalized_density);
let r = color.0;
let g = color.1;
let b = color.2;
let x = (pixel_x as f32 / scale_x) + *options.x_axis.range.start();
let y = (pixel_y as f32 / scale_y) + *options.y_axis.range.start();
RawPixelData { x, y, r, g, b }
})
.collect();
eprintln!(" └─ Pixel coloring: {:?}", color_start.elapsed());
Some(colored_pixels)
}