1use crate::contour::ContourData;
2use crate::PlotBytes;
3use crate::create_axis_specs;
4use crate::density_calc::RawPixelData;
5use crate::options::DensityPlotOptions;
6use crate::render::{ProgressInfo, RenderConfig};
7use flow_fcs::{TransformType, Transformable};
8use plotters::prelude::*;
9
10fn format_transform_value(transform: &TransformType, value: &f32) -> String {
14 match transform {
15 TransformType::Linear => format!("{:.1e}", value),
16 TransformType::Arcsinh { cofactor: _ } => {
17 let original_value = transform.inverse_transform(value);
19 format!("{:.1e}", original_value)
21 }
22 TransformType::Biexponential { .. } => {
23 let original_value = transform.inverse_transform(value);
25 format!("{:.1e}", original_value)
27 }
28 }
29}
30use anyhow::Result;
31use image::RgbImage;
32use plotters::{
33 backend::BitMapBackend, chart::ChartBuilder, prelude::IntoDrawingArea, style::WHITE,
34};
35
36pub fn render_pixels(
45 pixels: Vec<RawPixelData>,
46 options: &DensityPlotOptions,
47 render_config: &mut RenderConfig,
48) -> Result<PlotBytes> {
49 use crate::options::PlotOptions;
50
51 let base = options.base();
52 let width = base.width;
53 let height = base.height;
54 let margin = base.margin;
55 let x_label_area_size = base.x_label_area_size;
56 let y_label_area_size = base.y_label_area_size;
57
58 let setup_start = std::time::Instant::now();
59 let mut pixel_buffer = vec![255; (width * height * 3) as usize];
61
62 let (plot_x_range, plot_y_range, x_spec, y_spec) = {
63 let backend = BitMapBackend::with_buffer(&mut pixel_buffer, (width, height));
64 let root = backend.into_drawing_area();
65 root.fill(&WHITE)
66 .map_err(|e| anyhow::anyhow!("failed to fill plot background: {e}"))?;
67
68 let (x_spec, y_spec) = create_axis_specs(
70 &options.x_axis.range,
71 &options.y_axis.range,
72 &options.x_axis.transform,
73 &options.y_axis.transform,
74 )?;
75
76 let mut chart = ChartBuilder::on(&root)
77 .margin(margin)
78 .x_label_area_size(x_label_area_size)
79 .y_label_area_size(y_label_area_size)
80 .build_cartesian_2d(x_spec.start..x_spec.end, y_spec.start..y_spec.end)?;
81
82 let x_transform_clone = options.x_axis.transform.clone();
84 let y_transform_clone = options.y_axis.transform.clone();
85
86 let x_formatter =
88 move |x: &f32| -> String { format_transform_value(&x_transform_clone, x) };
89 let y_formatter =
90 move |y: &f32| -> String { format_transform_value(&y_transform_clone, y) };
91
92 let mut mesh = chart.configure_mesh();
93 mesh.x_max_light_lines(4)
94 .y_max_light_lines(4)
95 .x_labels(10)
96 .y_labels(10)
97 .x_label_formatter(&x_formatter)
98 .y_label_formatter(&y_formatter);
99
100 if let Some(ref x_label) = options.x_axis.label {
102 mesh.x_desc(x_label);
103 }
104 if let Some(ref y_label) = options.y_axis.label {
105 mesh.y_desc(y_label);
106 }
107
108 let mesh_start = std::time::Instant::now();
109 mesh.draw()
110 .map_err(|e| anyhow::anyhow!("failed to draw plot mesh: {e}"))?;
111 eprintln!(" ├─ Mesh drawing: {:?}", mesh_start.elapsed());
112
113 let plotting_area = chart.plotting_area();
115 let (plot_x_range, plot_y_range) = plotting_area.get_pixel_range();
116
117 root.present()
118 .map_err(|e| anyhow::anyhow!("failed to present plotters buffer: {e}"))?;
119
120 (plot_x_range, plot_y_range, x_spec, y_spec)
121 }; let series_start = std::time::Instant::now();
126
127 let plot_x_start = plot_x_range.start as f32;
128 let plot_y_start = plot_y_range.start as f32;
129 let plot_width = (plot_x_range.end - plot_x_range.start) as f32;
130 let plot_height = (plot_y_range.end - plot_y_range.start) as f32;
131
132 let data_width = x_spec.end - x_spec.start;
134 let data_height = y_spec.end - y_spec.start;
135
136 let mut pixel_count = 0;
138 let total_pixels = pixels.len();
139 let chunk_size = 1000; for pixel in &pixels {
143 let data_x = pixel.x;
144 let data_y = pixel.y;
145
146 let rel_x = (data_x - x_spec.start) / data_width;
148 let rel_y = (y_spec.end - data_y) / data_height; let screen_x = (plot_x_start + rel_x * plot_width) as i32;
151 let screen_y = (plot_y_start + rel_y * plot_height) as i32;
152
153 if screen_x >= plot_x_range.start
155 && screen_x < plot_x_range.end
156 && screen_y >= plot_y_range.start
157 && screen_y < plot_y_range.end
158 {
159 let px = screen_x as u32;
160 let py = screen_y as u32;
161
162 let idx = ((py * width + px) * 3) as usize;
164
165 if idx + 2 < pixel_buffer.len() {
166 pixel_buffer[idx] = pixel.r;
167 pixel_buffer[idx + 1] = pixel.g;
168 pixel_buffer[idx + 2] = pixel.b;
169 }
170 }
171
172 pixel_count += 1;
173
174 if pixel_count % chunk_size == 0 || pixel_count == total_pixels {
176 let percent = (pixel_count as f32 / total_pixels as f32) * 100.0;
177
178 let chunk_start = (pixel_count - chunk_size.min(pixel_count)).max(0);
180 let chunk_end = pixel_count;
181 let chunk_pixels: Vec<RawPixelData> = pixels
182 .iter()
183 .skip(chunk_start)
184 .take(chunk_end - chunk_start)
185 .map(|p| RawPixelData {
186 x: p.x,
187 y: p.y,
188 r: p.r,
189 g: p.g,
190 b: p.b,
191 })
192 .collect();
193
194 render_config.report_progress(ProgressInfo {
195 pixels: chunk_pixels,
196 percent,
197 });
198 }
199 }
200
201 eprintln!(
202 " ├─ Direct pixel writing: {:?} ({} pixels)",
203 series_start.elapsed(),
204 pixels.len()
205 );
206 eprintln!(" ├─ Total plotting: {:?}", setup_start.elapsed());
207
208 let img_start = std::time::Instant::now();
209 let img: RgbImage = image::ImageBuffer::from_vec(width, height, pixel_buffer)
210 .ok_or_else(|| anyhow::anyhow!("plot image buffer had unexpected size"))?;
211 eprintln!(" ├─ Image buffer conversion: {:?}", img_start.elapsed());
212
213 let encode_start = std::time::Instant::now();
214
215 let raw_size = (width * height * 3) as usize;
219 let estimated_jpeg_size = raw_size / 8; let mut encoded_data = Vec::with_capacity(estimated_jpeg_size);
221
222 let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut encoded_data, 85);
225 encoder
226 .encode(img.as_raw(), width, height, image::ExtendedColorType::Rgb8)
227 .map_err(|e| anyhow::anyhow!("failed to JPEG encode plot: {e}"))?;
228 eprintln!(" └─ JPEG encoding: {:?}", encode_start.elapsed());
229
230 Ok(encoded_data)
232}
233
234pub fn render_contour(
238 contour_data: ContourData,
239 options: &DensityPlotOptions,
240 _render_config: &mut RenderConfig,
241) -> Result<PlotBytes> {
242 use crate::options::PlotOptions;
243
244 let base = options.base();
245 let width = base.width;
246 let height = base.height;
247 let margin = base.margin;
248 let x_label_area_size = base.x_label_area_size;
249 let y_label_area_size = base.y_label_area_size;
250
251 let (x_spec, y_spec) = create_axis_specs(
252 &options.x_axis.range,
253 &options.y_axis.range,
254 &options.x_axis.transform,
255 &options.y_axis.transform,
256 )?;
257
258 let mut pixel_buffer = vec![255; (width * height * 3) as usize];
259
260 {
261 let backend = BitMapBackend::with_buffer(&mut pixel_buffer, (width, height));
262 let root = backend.into_drawing_area();
263 root.fill(&WHITE)
264 .map_err(|e| anyhow::anyhow!("failed to fill plot background: {e}"))?;
265
266 let x_transform_clone = options.x_axis.transform.clone();
267 let y_transform_clone = options.y_axis.transform.clone();
268 let x_formatter = move |x: &f64| -> String {
269 format_transform_value(&x_transform_clone, &(*x as f32))
270 };
271 let y_formatter = move |y: &f64| -> String {
272 format_transform_value(&y_transform_clone, &(*y as f32))
273 };
274
275 let mut chart = ChartBuilder::on(&root)
276 .margin(margin)
277 .x_label_area_size(x_label_area_size)
278 .y_label_area_size(y_label_area_size)
279 .build_cartesian_2d(
280 x_spec.start as f64..x_spec.end as f64,
281 y_spec.start as f64..y_spec.end as f64,
282 )?;
283
284 let mut mesh = chart.configure_mesh();
285 mesh.x_max_light_lines(4)
286 .y_max_light_lines(4)
287 .x_labels(10)
288 .y_labels(10)
289 .x_label_formatter(&x_formatter)
290 .y_label_formatter(&y_formatter);
291 if let Some(ref x_label) = options.x_axis.label {
292 mesh.x_desc(x_label);
293 }
294 if let Some(ref y_label) = options.y_axis.label {
295 mesh.y_desc(y_label);
296 }
297 mesh.draw()
298 .map_err(|e| anyhow::anyhow!("failed to draw plot mesh: {e}"))?;
299
300 let stroke_width = options.contour_line_thickness.max(0.5).min(5.0) as u32;
301 let contour_color = RGBColor(60, 60, 60);
302
303 for path in &contour_data.contours {
305 if path.len() < 2 {
306 continue;
307 }
308 let points: Vec<(f64, f64)> = path.iter().copied().collect();
309 chart
310 .draw_series(LineSeries::new(
311 points,
312 contour_color.stroke_width(stroke_width),
313 ))
314 .map_err(|e| anyhow::anyhow!("failed to draw contour: {e}"))?;
315 }
316
317 if !contour_data.outliers.is_empty() {
319 let outlier_color = RGBColor(150, 150, 150);
320 chart
321 .draw_series(
322 contour_data
323 .outliers
324 .iter()
325 .map(|&(x, y)| Circle::new((x, y), 2, outlier_color.filled())),
326 )
327 .map_err(|e| anyhow::anyhow!("failed to draw outliers: {e}"))?;
328 }
329
330 root.present()
331 .map_err(|e| anyhow::anyhow!("failed to present plotters buffer: {e}"))?;
332 }
333
334 let img: RgbImage =
335 image::ImageBuffer::from_vec(width, height, pixel_buffer)
336 .ok_or_else(|| anyhow::anyhow!("plot image buffer had unexpected size"))?;
337
338 let mut encoded_data = Vec::new();
339 let mut encoder =
340 image::codecs::jpeg::JpegEncoder::new_with_quality(&mut encoded_data, 85);
341 encoder
342 .encode(img.as_raw(), width, height, image::ExtendedColorType::Rgb8)
343 .map_err(|e| anyhow::anyhow!("failed to JPEG encode plot: {e}"))?;
344
345 Ok(encoded_data)
346}
347
348pub fn render_spectral_signature(
352 data: (Vec<(usize, f64)>, Vec<String>),
353 options: &crate::options::spectral::SpectralSignaturePlotOptions,
354 _render_config: &mut RenderConfig,
355) -> Result<PlotBytes> {
356 use crate::options::PlotOptions;
357 use plotters::prelude::*;
358
359 let (spectrum_data, channel_names) = data;
360 let base = options.base();
361 let width = base.width;
362 let height = base.height;
363 let margin = base.margin;
364 let x_label_area_size = base.x_label_area_size;
365 let y_label_area_size = base.y_label_area_size;
366
367 let mut pixel_buffer = vec![255; (width * height * 3) as usize];
369
370 let x_min = 0.0f32;
372 let x_max = spectrum_data
373 .iter()
374 .map(|(idx, _)| *idx as f32)
375 .fold(0.0f32, f32::max)
376 .max(1.0);
377 let y_min = 0.0f32;
378 let y_max = 1.0f32;
379
380 let channel_names_clone = channel_names.clone();
382
383 {
384 let backend = BitMapBackend::with_buffer(&mut pixel_buffer, (width, height));
385 let root = backend.into_drawing_area();
386 root.fill(&WHITE)
387 .map_err(|e| anyhow::anyhow!("failed to fill plot background: {e}"))?;
388
389 let mut chart = ChartBuilder::on(&root)
390 .margin(margin)
391 .x_label_area_size(x_label_area_size)
392 .y_label_area_size(y_label_area_size)
393 .build_cartesian_2d(x_min..x_max, y_min..y_max)
394 .map_err(|e| anyhow::anyhow!("failed to build chart: {e}"))?;
395
396 let x_formatter: Option<Box<dyn Fn(&f32) -> String>> = if !channel_names_clone.is_empty()
398 && channel_names_clone.len() == spectrum_data.len()
399 {
400 let channel_names_for_formatter = channel_names_clone.clone();
401 Some(Box::new(move |x: &f32| -> String {
402 let idx = x.round() as usize;
404 if idx < channel_names_for_formatter.len() {
405 channel_names_for_formatter[idx].clone()
406 } else {
407 format!("{:.0}", x)
408 }
409 }))
410 } else {
411 None
412 };
413
414 let mut mesh = chart.configure_mesh();
416 if options.show_grid {
417 mesh.x_max_light_lines(4).y_max_light_lines(4);
418 }
419
420 if let Some(ref x_axis) = options.x_axis {
422 if let Some(ref label) = x_axis.label {
423 mesh.x_desc(label);
424 }
425 } else {
426 mesh.x_desc("Channel");
427 }
428
429 if let Some(ref y_axis) = options.y_axis {
430 if let Some(ref label) = y_axis.label {
431 mesh.y_desc(label);
432 }
433 } else {
434 mesh.y_desc("Normalized Intensity");
435 }
436
437 if let Some(ref formatter) = x_formatter {
439 mesh.x_label_formatter(formatter);
440 }
441
442 let x_label_count = if !channel_names_clone.is_empty() {
444 channel_names_clone.len().min(20) } else {
446 10
447 };
448
449 mesh.x_labels(x_label_count)
450 .y_labels(10)
451 .draw()
452 .map_err(|e| anyhow::anyhow!("failed to draw mesh: {e}"))?;
453
454 if !spectrum_data.is_empty() {
456 let line_color = if options.line_color.starts_with('#') {
458 let hex = &options.line_color[1..];
459 if hex.len() == 6 {
460 let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(31);
461 let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(119);
462 let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(180);
463 RGBColor(r, g, b)
464 } else {
465 RGBColor(31, 119, 180) }
467 } else {
468 RGBColor(31, 119, 180) };
470
471 chart
472 .draw_series(LineSeries::new(
473 spectrum_data
474 .iter()
475 .map(|(idx, val)| (*idx as f32, *val as f32)),
476 line_color.stroke_width(options.line_width as u32),
477 ))
478 .map_err(|e| anyhow::anyhow!("failed to draw line series: {e}"))?;
479 }
480 } let img: RgbImage = image::ImageBuffer::from_vec(width, height, pixel_buffer)
484 .ok_or_else(|| anyhow::anyhow!("plot image buffer had unexpected size"))?;
485
486 let mut encoded_data = Vec::new();
487 let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut encoded_data, 85);
488 encoder
489 .encode(img.as_raw(), width, height, image::ExtendedColorType::Rgb8)
490 .map_err(|e| anyhow::anyhow!("failed to JPEG encode plot: {e}"))?;
491
492 Ok(encoded_data)
493}
494
495pub fn render_histogram(
500 data: crate::histogram_data::HistogramData,
501 options: &crate::options::HistogramPlotOptions,
502 _render_config: &mut RenderConfig,
503) -> Result<PlotBytes> {
504 use crate::histogram_data::{bin_values, BinnedHistogram, HistogramData, HistogramSeries};
505 use crate::options::PlotOptions;
506 use plotters::prelude::*;
507
508 let base = options.base();
509 let width = base.width;
510 let height = base.height;
511 let margin = base.margin;
512 let x_label_area_size = base.x_label_area_size;
513 let y_label_area_size = base.y_label_area_size;
514
515 let x_min = *options.x_axis.range.start() as f64;
516 let x_max = *options.x_axis.range.end() as f64;
517
518 let series: Vec<(BinnedHistogram, u32)> = match data {
520 HistogramData::RawValues(values) => {
521 let binned = bin_values(
522 &values,
523 options.num_bins,
524 *options.x_axis.range.start(),
525 *options.x_axis.range.end(),
526 );
527 match binned {
528 Some(b) => vec![(b, 0)],
529 None => vec![],
530 }
531 }
532 HistogramData::PreBinned { bin_edges, counts } => {
533 let bin_centers: Vec<f64> = bin_edges
534 .windows(2)
535 .map(|w| (w[0] as f64 + w[1] as f64) / 2.0)
536 .collect();
537 let counts_f64: Vec<f64> = counts.iter().map(|&c| c as f64).collect();
538 vec![(
539 BinnedHistogram {
540 bin_centers,
541 counts: counts_f64,
542 },
543 0,
544 )]
545 }
546 HistogramData::Overlaid(overlaid) => {
547 let mut result = Vec::with_capacity(overlaid.len());
548 for HistogramSeries { values, gate_id } in overlaid {
549 if let Some(binned) = bin_values(
550 &values,
551 options.num_bins,
552 *options.x_axis.range.start(),
553 *options.x_axis.range.end(),
554 ) {
555 result.push((binned, gate_id));
556 }
557 }
558 result
559 }
560 };
561
562 if series.is_empty() {
563 return render_empty_histogram(
565 options, width, height, margin, x_label_area_size, y_label_area_size, x_min, x_max,
566 );
567 }
568
569 let series: Vec<(BinnedHistogram, u32)> = if options.scale_to_peak && series.len() > 1 {
571 series
572 .into_iter()
573 .map(|(mut binned, gate_id)| {
574 let max_count = binned.counts.iter().cloned().fold(0.0f64, f64::max);
575 if max_count > 0.0 {
576 binned.counts.iter_mut().for_each(|c| *c /= max_count);
577 }
578 (binned, gate_id)
579 })
580 .collect()
581 } else {
582 series
583 };
584
585 let (y_min, y_max) = if options.baseline_separation > 0.0 && series.len() > 1 {
587 let mut max_y = 0.0f64;
589 let mut cumulative_offset = 0.0f64;
590 for (binned, _) in &series {
591 let peak = binned.counts.iter().cloned().fold(0.0f64, f64::max);
592 max_y = max_y.max(cumulative_offset + peak);
593 cumulative_offset += options.baseline_separation as f64;
594 }
595 (0.0, (max_y * 1.05).max(0.1))
596 } else {
597 let global_max = series
598 .iter()
599 .flat_map(|(b, _)| b.counts.iter())
600 .cloned()
601 .fold(0.0f64, f64::max);
602 (0.0, (global_max * 1.05).max(0.1))
603 };
604
605 let mut pixel_buffer = vec![255; (width * height * 3) as usize];
606
607 {
608 let backend = BitMapBackend::with_buffer(&mut pixel_buffer, (width, height));
609 let root = backend.into_drawing_area();
610 root.fill(&WHITE)
611 .map_err(|e| anyhow::anyhow!("failed to fill plot background: {e}"))?;
612
613 let mut chart = ChartBuilder::on(&root)
614 .margin(margin)
615 .x_label_area_size(x_label_area_size)
616 .y_label_area_size(y_label_area_size)
617 .build_cartesian_2d(x_min..x_max, y_min..y_max)
618 .map_err(|e| anyhow::anyhow!("failed to build histogram chart: {e}"))?;
619
620 let mut mesh = chart.configure_mesh();
621 mesh.x_max_light_lines(4).y_max_light_lines(4)
622 .x_labels(10)
623 .y_labels(10);
624
625 if let Some(ref label) = options.x_axis.label {
626 mesh.x_desc(label);
627 } else {
628 mesh.x_desc("Value");
629 }
630 mesh.y_desc("Count");
631
632 mesh.draw()
633 .map_err(|e| anyhow::anyhow!("failed to draw mesh: {e}"))?;
634
635 let baseline_sep = options.baseline_separation as f64;
636 let mut y_offset = 0.0f64;
637
638 for (binned, gate_id) in &series {
639 let (r, g, b) = options.gate_color(*gate_id);
640 let color = RGBColor(r, g, b);
641 let fill_color = RGBColor(r, g, b).mix(0.3);
642
643 let points: Vec<(f64, f64)> = binned
644 .bin_centers
645 .iter()
646 .zip(binned.counts.iter())
647 .map(|(x, c)| (*x, y_offset + *c))
648 .collect();
649
650 if points.is_empty() {
651 y_offset += baseline_sep;
652 continue;
653 }
654
655 if options.histogram_filled {
656 chart
657 .draw_series(AreaSeries::new(
658 points.iter().copied(),
659 y_offset,
660 fill_color,
661 ))
662 .map_err(|e| anyhow::anyhow!("failed to draw area series: {e}"))?;
663 chart
665 .draw_series(LineSeries::new(
666 points.iter().copied(),
667 color.stroke_width(options.line_width as u32),
668 ))
669 .map_err(|e| anyhow::anyhow!("failed to draw histogram line: {e}"))?;
670 } else {
671 chart
672 .draw_series(LineSeries::new(
673 points.iter().copied(),
674 color.stroke_width(options.line_width as u32),
675 ))
676 .map_err(|e| anyhow::anyhow!("failed to draw histogram line: {e}"))?;
677 }
678
679 y_offset += baseline_sep;
680 }
681 }
682
683 let img: RgbImage = image::ImageBuffer::from_vec(width, height, pixel_buffer)
684 .ok_or_else(|| anyhow::anyhow!("plot image buffer had unexpected size"))?;
685
686 let mut encoded_data = Vec::new();
687 let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut encoded_data, 85);
688 encoder
689 .encode(img.as_raw(), width, height, image::ExtendedColorType::Rgb8)
690 .map_err(|e| anyhow::anyhow!("failed to JPEG encode plot: {e}"))?;
691
692 Ok(encoded_data)
693}
694
695fn render_empty_histogram(
696 options: &crate::options::HistogramPlotOptions,
697 width: u32,
698 height: u32,
699 margin: u32,
700 x_label_area_size: u32,
701 y_label_area_size: u32,
702 x_min: f64,
703 x_max: f64,
704) -> Result<PlotBytes> {
705 use plotters::prelude::*;
706
707 let mut pixel_buffer = vec![255; (width * height * 3) as usize];
708
709 {
710 let backend = BitMapBackend::with_buffer(&mut pixel_buffer, (width, height));
711 let root = backend.into_drawing_area();
712 root.fill(&WHITE)
713 .map_err(|e| anyhow::anyhow!("failed to fill plot background: {e}"))?;
714
715 let mut chart = ChartBuilder::on(&root)
716 .margin(margin)
717 .x_label_area_size(x_label_area_size)
718 .y_label_area_size(y_label_area_size)
719 .build_cartesian_2d(x_min..x_max, 0.0f64..1.0f64)
720 .map_err(|e| anyhow::anyhow!("failed to build histogram chart: {e}"))?;
721
722 let mut mesh = chart.configure_mesh();
723 mesh.x_max_light_lines(4).y_max_light_lines(4);
724 if let Some(ref label) = options.x_axis.label {
725 mesh.x_desc(label);
726 }
727 mesh.y_desc("Count")
728 .draw()
729 .map_err(|e| anyhow::anyhow!("failed to draw mesh: {e}"))?;
730 }
731
732 let img: RgbImage = image::ImageBuffer::from_vec(width, height, pixel_buffer)
733 .ok_or_else(|| anyhow::anyhow!("plot image buffer had unexpected size"))?;
734
735 let mut encoded_data = Vec::new();
736 let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut encoded_data, 85);
737 encoder
738 .encode(img.as_raw(), width, height, image::ExtendedColorType::Rgb8)
739 .map_err(|e| anyhow::anyhow!("failed to JPEG encode plot: {e}"))?;
740
741 Ok(encoded_data)
742}