1use rustc_hash::FxHashMap;
2use serde::Serialize;
3use ts_rs::TS;
4
5use crate::options::density::default_gate_colors;
6use crate::options::{DensityPlotOptions, PlotOptions};
7use crate::plots::PlotType;
8use crate::scatter_data::ScatterPlotData;
9
10#[derive(Clone, Debug, Serialize, TS)]
21#[ts(export)]
22pub struct RawPixelData {
23 pub x: f32,
24 pub y: f32,
25 pub r: u8,
26 pub g: u8,
27 pub b: u8,
28}
29
30#[derive(Clone, Serialize, TS)]
33#[ts(export)]
34pub struct BinaryPixelChunk {
35 pub pixels: Vec<u8>,
37 pub width: u32,
39 pub height: u32,
41 pub offset_x: u32,
43 pub offset_y: u32,
45 pub total_width: u32,
47 pub total_height: u32,
49}
50
51pub fn create_binary_chunk(
54 pixels: &[RawPixelData],
55 plot_width: u32,
56 plot_height: u32,
57 _chunk_size: u32,
58) -> Option<BinaryPixelChunk> {
59 if pixels.is_empty() {
60 return None;
61 }
62
63 let mut min_x = pixels[0].x;
65 let mut max_x = pixels[0].x;
66 let mut min_y = pixels[0].y;
67 let mut max_y = pixels[0].y;
68
69 for pixel in pixels {
70 min_x = min_x.min(pixel.x);
71 max_x = max_x.max(pixel.x);
72 min_y = min_y.min(pixel.y);
73 max_y = max_y.max(pixel.y);
74 }
75
76 let chunk_x = min_x.max(0.0).floor() as u32;
78 let chunk_y = min_y.max(0.0).floor() as u32;
79 let chunk_width = (max_x - min_x).max(0.0).floor() as u32 + 1;
80 let chunk_height = (max_y - min_y).max(0.0).floor() as u32 + 1;
81
82 let total_px: usize = (chunk_width as usize)
84 .saturating_mul(chunk_height as usize)
85 .min((plot_width as usize).saturating_mul(plot_height as usize));
86 let buf_len = total_px.saturating_mul(3);
87 let mut rgb_data = vec![0u8; buf_len];
88
89 for pixel in pixels {
91 let local_x = (pixel.x - min_x).round().max(0.0) as u32;
92 let local_y = (pixel.y - min_y).round().max(0.0) as u32;
93
94 if local_x < chunk_width && local_y < chunk_height {
95 let idx = ((local_y as usize)
96 .saturating_mul(chunk_width as usize)
97 .saturating_add(local_x as usize))
98 .saturating_mul(3);
99 if idx + 2 < rgb_data.len() {
100 rgb_data[idx] = pixel.r;
101 rgb_data[idx + 1] = pixel.g;
102 rgb_data[idx + 2] = pixel.b;
103 }
104 }
105 }
106
107 Some(BinaryPixelChunk {
108 pixels: rgb_data,
109 width: chunk_width,
110 height: chunk_height,
111 offset_x: chunk_x,
112 offset_y: chunk_y,
113 total_width: plot_width,
114 total_height: plot_height,
115 })
116}
117
118pub fn scatter_to_pixels_overlay(
120 data: &ScatterPlotData,
121 width: usize,
122 height: usize,
123 options: &DensityPlotOptions,
124) -> Vec<RawPixelData> {
125 let gate_ids = match &data.gate_ids {
126 Some(ids) => ids,
127 None => return scatter_to_pixels(data.xy(), width, height, options),
128 };
129 let colors = if options.gate_colors.is_empty() {
130 default_gate_colors()
131 } else {
132 options.gate_colors.clone()
133 };
134
135 let point_size = options.point_size.max(0.1).min(4.0);
137 let radius_px = if point_size < 0.5 {
138 0
139 } else {
140 (point_size.ceil() as usize).max(1)
141 };
142
143 let scale_x = width as f32 / (*options.x_axis.range.end() - *options.x_axis.range.start());
144 let scale_y = height as f32 / (*options.y_axis.range.end() - *options.y_axis.range.start());
145
146 let mut pixels = Vec::new();
147 for (i, &(x, y)) in data.xy().iter().enumerate() {
148 let gate_id = gate_ids.get(i).copied().unwrap_or(0) as usize;
149 let (r, g, b) = colors
150 .get(gate_id)
151 .copied()
152 .unwrap_or((60, 60, 60));
153
154 let pixel_x = (((x - *options.x_axis.range.start()) * scale_x).floor() as isize)
155 .clamp(0, (width - 1) as isize) as usize;
156 let pixel_y = (((y - *options.y_axis.range.start()) * scale_y).floor() as isize)
157 .clamp(0, (height - 1) as isize) as usize;
158
159 for dy in -(radius_px as i32)..=(radius_px as i32) {
160 for dx in -(radius_px as i32)..=(radius_px as i32) {
161 let px = (pixel_x as i32 + dx).clamp(0, (width - 1) as i32) as usize;
162 let py = (pixel_y as i32 + dy).clamp(0, (height - 1) as i32) as usize;
163
164 let data_x = (px as f32 / scale_x) + *options.x_axis.range.start();
165 let data_y = (py as f32 / scale_y) + *options.y_axis.range.start();
166
167 pixels.push(RawPixelData {
168 x: data_x,
169 y: data_y,
170 r,
171 g,
172 b,
173 });
174 }
175 }
176 }
177 pixels
178}
179
180pub fn scatter_to_pixels_colored(
182 data: &ScatterPlotData,
183 width: usize,
184 height: usize,
185 options: &DensityPlotOptions,
186) -> Vec<RawPixelData> {
187 let z_values = match &data.z_values {
188 Some(z) => z,
189 None => return scatter_to_pixels(data.xy(), width, height, options),
190 };
191
192 let (z_min, z_max) = match options.z_range {
193 Some((min, max)) => (min, max),
194 None => {
195 let min = z_values
196 .iter()
197 .copied()
198 .fold(f32::INFINITY, f32::min);
199 let max = z_values
200 .iter()
201 .copied()
202 .fold(f32::NEG_INFINITY, f32::max);
203 if min >= max {
204 (min, min + 1.0)
205 } else {
206 (min, max)
207 }
208 }
209 };
210 let z_range = z_max - z_min;
211
212 let point_size = options.point_size.max(0.1).min(4.0);
214 let radius_px = if point_size < 0.5 {
215 0
216 } else {
217 (point_size.ceil() as usize).max(1)
218 };
219
220 let scale_x = width as f32 / (*options.x_axis.range.end() - *options.x_axis.range.start());
221 let scale_y = height as f32 / (*options.y_axis.range.end() - *options.y_axis.range.start());
222
223 let mut pixels = Vec::new();
224 for (i, &(x, y)) in data.xy().iter().enumerate() {
225 let z = z_values.get(i).copied().unwrap_or(0.0);
226 let t = (z - z_min) / z_range;
227 let normalized = t.max(0.0).min(1.0);
228 let color = options.colormap.map(normalized);
229 let (r, g, b) = (color.0, color.1, color.2);
230
231 let pixel_x = (((x - *options.x_axis.range.start()) * scale_x).floor() as isize)
232 .clamp(0, (width - 1) as isize) as usize;
233 let pixel_y = (((y - *options.y_axis.range.start()) * scale_y).floor() as isize)
234 .clamp(0, (height - 1) as isize) as usize;
235
236 for dy in -(radius_px as i32)..=(radius_px as i32) {
237 for dx in -(radius_px as i32)..=(radius_px as i32) {
238 let px = (pixel_x as i32 + dx).clamp(0, (width - 1) as i32) as usize;
239 let py = (pixel_y as i32 + dy).clamp(0, (height - 1) as i32) as usize;
240
241 let data_x = (px as f32 / scale_x) + *options.x_axis.range.start();
242 let data_y = (py as f32 / scale_y) + *options.y_axis.range.start();
243
244 pixels.push(RawPixelData {
245 x: data_x,
246 y: data_y,
247 r,
248 g,
249 b,
250 });
251 }
252 }
253 }
254 pixels
255}
256
257pub fn scatter_to_pixels(
262 data: &[(f32, f32)],
263 width: usize,
264 height: usize,
265 options: &DensityPlotOptions,
266) -> Vec<RawPixelData> {
267 let point_size = options.point_size.max(0.1).min(4.0);
269 let radius_px = if point_size < 0.5 {
270 0
271 } else {
272 (point_size.ceil() as usize).max(1)
273 };
274
275 let scale_x = width as f32 / (*options.x_axis.range.end() - *options.x_axis.range.start());
276 let scale_y = height as f32 / (*options.y_axis.range.end() - *options.y_axis.range.start());
277
278 let (r, g, b) = (60u8, 60u8, 60u8);
280
281 let mut pixels = Vec::with_capacity(data.len() * (2 * radius_px + 1).pow(2));
282
283 for &(x, y) in data {
284 let pixel_x = (((x - *options.x_axis.range.start()) * scale_x).floor() as isize)
285 .clamp(0, (width - 1) as isize) as usize;
286 let pixel_y = (((y - *options.y_axis.range.start()) * scale_y).floor() as isize)
287 .clamp(0, (height - 1) as isize) as usize;
288
289 for dy in -(radius_px as i32)..=(radius_px as i32) {
290 for dx in -(radius_px as i32)..=(radius_px as i32) {
291 let px = (pixel_x as i32 + dx).clamp(0, (width - 1) as i32) as usize;
292 let py = (pixel_y as i32 + dy).clamp(0, (height - 1) as i32) as usize;
293
294 let data_x = (px as f32 / scale_x) + *options.x_axis.range.start();
295 let data_y = (py as f32 / scale_y) + *options.y_axis.range.start();
296
297 pixels.push(RawPixelData {
298 x: data_x,
299 y: data_y,
300 r,
301 g,
302 b,
303 });
304 }
305 }
306 }
307
308 pixels
309}
310
311pub fn calculate_plot_pixels(
319 data: &ScatterPlotData,
320 width: usize,
321 height: usize,
322 options: &DensityPlotOptions,
323) -> Vec<RawPixelData> {
324 let xy = data.xy();
325 match options.plot_type.canonical() {
326 PlotType::ScatterSolid | PlotType::Dot => {
327 scatter_to_pixels(xy, width, height, options)
328 }
329 PlotType::ScatterOverlay => {
330 if data.has_gates() {
331 scatter_to_pixels_overlay(data, width, height, options)
332 } else {
333 scatter_to_pixels(xy, width, height, options)
334 }
335 }
336 PlotType::ScatterColoredContinuous => {
337 if data.has_z() {
338 scatter_to_pixels_colored(data, width, height, options)
339 } else {
340 calculate_density_per_pixel(xy, width, height, options)
341 }
342 }
343 PlotType::Density
344 | PlotType::Contour
345 | PlotType::ContourOverlay
346 | PlotType::Zebra
347 | PlotType::Histogram => calculate_density_per_pixel(xy, width, height, options),
348 }
349}
350
351pub fn calculate_plot_pixels_cancelable(
357 data: &ScatterPlotData,
358 width: usize,
359 height: usize,
360 options: &DensityPlotOptions,
361 should_cancel: impl FnMut() -> bool,
362) -> Option<Vec<RawPixelData>> {
363 let xy = data.xy();
364 match options.plot_type.canonical() {
365 PlotType::ScatterSolid | PlotType::Dot => {
366 Some(scatter_to_pixels(xy, width, height, options))
367 }
368 PlotType::ScatterOverlay => Some(if data.has_gates() {
369 scatter_to_pixels_overlay(data, width, height, options)
370 } else {
371 scatter_to_pixels(xy, width, height, options)
372 }),
373 PlotType::ScatterColoredContinuous => {
374 if data.has_z() {
375 Some(scatter_to_pixels_colored(data, width, height, options))
376 } else {
377 calculate_density_per_pixel_cancelable(xy, width, height, options, should_cancel)
378 }
379 }
380 PlotType::Density
381 | PlotType::Contour
382 | PlotType::ContourOverlay
383 | PlotType::Zebra
384 | PlotType::Histogram => {
385 calculate_density_per_pixel_cancelable(xy, width, height, options, should_cancel)
386 }
387 }
388}
389
390pub fn calculate_density_per_pixel(
391 data: &[(f32, f32)],
392 width: usize,
393 height: usize,
394 options: &DensityPlotOptions,
395) -> Vec<RawPixelData> {
396 calculate_density_per_pixel_cancelable(data, width, height, options, || false).expect(
397 "calculate_density_per_pixel_cancelable returned None when cancellation is disabled",
398 )
399}
400
401pub fn calculate_density_per_pixel_cancelable(
402 data: &[(f32, f32)],
403 width: usize,
404 height: usize,
405 options: &DensityPlotOptions,
406 should_cancel: impl FnMut() -> bool,
407) -> Option<Vec<RawPixelData>> {
408 calculate_density_per_pixel_cpu(data, width, height, options, should_cancel)
409}
410
411pub fn calculate_density_per_pixel_batch(
424 requests: &[(ScatterPlotData, DensityPlotOptions)],
425) -> Vec<Vec<RawPixelData>> {
426 calculate_density_per_pixel_batch_cancelable(requests, || false)
427 .expect("calculate_density_per_pixel_batch_cancelable returned None when cancellation is disabled")
428}
429
430pub fn calculate_density_per_pixel_batch_cancelable(
442 requests: &[(ScatterPlotData, DensityPlotOptions)],
443 mut should_cancel: impl FnMut() -> bool,
444) -> Option<Vec<Vec<RawPixelData>>> {
445 if should_cancel() {
446 return None;
447 }
448 Some(calculate_density_per_pixel_batch_cpu(requests))
449}
450
451fn calculate_density_per_pixel_batch_cpu(
453 requests: &[(ScatterPlotData, DensityPlotOptions)],
454) -> Vec<Vec<RawPixelData>> {
455 requests
456 .iter()
457 .map(|(data, options)| {
458 let base = options.base();
459 calculate_plot_pixels(data, base.width as usize, base.height as usize, options)
460 })
461 .collect()
462}
463
464fn calculate_density_per_pixel_cpu(
466 data: &[(f32, f32)],
467 width: usize,
468 height: usize,
469 options: &DensityPlotOptions,
470 mut should_cancel: impl FnMut() -> bool,
471) -> Option<Vec<RawPixelData>> {
472 let scale_x = width as f32 / (*options.x_axis.range.end() - *options.x_axis.range.start());
475 let scale_y = height as f32 / (*options.y_axis.range.end() - *options.y_axis.range.start());
476
477 let point_size = options.point_size.max(0.1).min(4.0);
485 let radius_px = if point_size < 0.5 {
486 0
487 } else {
488 (point_size.ceil() as usize).max(1)
489 };
490
491 let build_start = std::time::Instant::now();
492 let mut density = vec![0.0f32; width * height];
493
494 let mut last_progress = std::time::Instant::now();
498 for (i, &(x, y)) in data.iter().enumerate() {
499 if (i % 250_000) == 0 {
500 if should_cancel() {
501 eprintln!(
502 " ├─ Density build cancelled after {} / {} points",
503 i,
504 data.len()
505 );
506 return None;
507 }
508
509 if last_progress.elapsed().as_secs_f64() >= 2.0 {
511 eprintln!(
512 " ├─ Density build progress: {} / {} points",
513 i,
514 data.len()
515 );
516 last_progress = std::time::Instant::now();
517 }
518 }
519
520 let pixel_x = (((x - *options.x_axis.range.start()) * scale_x).floor() as isize)
521 .clamp(0, (width - 1) as isize) as usize;
522 let pixel_y = (((y - *options.y_axis.range.start()) * scale_y).floor() as isize)
523 .clamp(0, (height - 1) as isize) as usize;
524
525 for dy in -(radius_px as i32)..=(radius_px as i32) {
526 for dx in -(radius_px as i32)..=(radius_px as i32) {
527 let px = (pixel_x as i32 + dx).clamp(0, (width - 1) as i32) as usize;
528 let py = (pixel_y as i32 + dy).clamp(0, (height - 1) as i32) as usize;
529 let idx = py * width + px;
530 density[idx] += 1.0;
531 }
532 }
533 }
534
535 let mut density_map: FxHashMap<(usize, usize), f32> = FxHashMap::default();
537 density_map.reserve(width * height / 10); for (idx, &count) in density.iter().enumerate() {
540 if count > 0.0 {
541 let px = idx % width;
542 let py = idx / width;
543 density_map.insert((px, py), count);
544 }
545 }
546
547 eprintln!(
548 " ├─ Density map building: {:?} ({} unique pixels from {} total)",
549 build_start.elapsed(),
550 density_map.len(),
551 width * height
552 );
553
554 let log_start = std::time::Instant::now();
560 for (_, count) in density_map.iter_mut() {
561 *count = (*count + 1.0).log10();
562 }
563 eprintln!(" ├─ Log transform: {:?}", log_start.elapsed());
564
565 let max_start = std::time::Instant::now();
567 let max_density_log = density_map
568 .values()
569 .fold(0.0f32, |max, &val| max.max(val))
570 .max(1.0); eprintln!(" ├─ Find max: {:?}", max_start.elapsed());
572
573 let color_start = std::time::Instant::now();
577 let colored_pixels: Vec<RawPixelData> = density_map
578 .iter()
579 .map(|(&(pixel_x, pixel_y), &dens)| {
580 let normalized_density = dens / max_density_log;
582
583 let color = options.colormap.map(normalized_density);
585 let r = color.0;
586 let g = color.1;
587 let b = color.2;
588
589 let x = (pixel_x as f32 / scale_x) + *options.x_axis.range.start();
591 let y = (pixel_y as f32 / scale_y) + *options.y_axis.range.start();
592
593 RawPixelData { x, y, r, g, b }
594 })
595 .collect();
596 eprintln!(" └─ Pixel coloring: {:?}", color_start.elapsed());
597
598 Some(colored_pixels)
599}
600