use crate::contour::ContourData;
use crate::PlotBytes;
use crate::create_axis_specs;
use crate::density_calc::RawPixelData;
use crate::options::DensityPlotOptions;
use crate::render::{ProgressInfo, RenderConfig};
use flow_fcs::{TransformType, Transformable};
use plotters::prelude::*;
fn format_transform_value(transform: &TransformType, value: &f32) -> String {
match transform {
TransformType::Linear => format!("{:.1e}", value),
TransformType::Arcsinh { cofactor: _ } => {
let original_value = transform.inverse_transform(value);
format!("{:.1e}", original_value)
}
TransformType::Biexponential { .. } => {
let original_value = transform.inverse_transform(value);
format!("{:.1e}", original_value)
}
}
}
use anyhow::Result;
use image::RgbImage;
use plotters::{
backend::BitMapBackend, chart::ChartBuilder, prelude::IntoDrawingArea, style::WHITE,
};
pub fn render_pixels(
pixels: Vec<RawPixelData>,
options: &DensityPlotOptions,
render_config: &mut RenderConfig,
) -> Result<PlotBytes> {
use crate::options::PlotOptions;
let base = options.base();
let width = base.width;
let height = base.height;
let margin = base.margin;
let x_label_area_size = base.x_label_area_size;
let y_label_area_size = base.y_label_area_size;
let setup_start = std::time::Instant::now();
let mut pixel_buffer = vec![255; (width * height * 3) as usize];
let (plot_x_range, plot_y_range, x_spec, y_spec) = {
let backend = BitMapBackend::with_buffer(&mut pixel_buffer, (width, height));
let root = backend.into_drawing_area();
root.fill(&WHITE)
.map_err(|e| anyhow::anyhow!("failed to fill plot background: {e}"))?;
let (x_spec, y_spec) = create_axis_specs(
&options.x_axis.range,
&options.y_axis.range,
&options.x_axis.transform,
&options.y_axis.transform,
)?;
let mut chart = ChartBuilder::on(&root)
.margin(margin)
.x_label_area_size(x_label_area_size)
.y_label_area_size(y_label_area_size)
.build_cartesian_2d(x_spec.start..x_spec.end, y_spec.start..y_spec.end)?;
let x_transform_clone = options.x_axis.transform.clone();
let y_transform_clone = options.y_axis.transform.clone();
let x_formatter =
move |x: &f32| -> String { format_transform_value(&x_transform_clone, x) };
let y_formatter =
move |y: &f32| -> String { format_transform_value(&y_transform_clone, y) };
let mut mesh = chart.configure_mesh();
mesh.x_max_light_lines(4)
.y_max_light_lines(4)
.x_labels(10)
.y_labels(10)
.x_label_formatter(&x_formatter)
.y_label_formatter(&y_formatter);
if let Some(ref x_label) = options.x_axis.label {
mesh.x_desc(x_label);
}
if let Some(ref y_label) = options.y_axis.label {
mesh.y_desc(y_label);
}
let mesh_start = std::time::Instant::now();
mesh.draw()
.map_err(|e| anyhow::anyhow!("failed to draw plot mesh: {e}"))?;
eprintln!(" ├─ Mesh drawing: {:?}", mesh_start.elapsed());
let plotting_area = chart.plotting_area();
let (plot_x_range, plot_y_range) = plotting_area.get_pixel_range();
root.present()
.map_err(|e| anyhow::anyhow!("failed to present plotters buffer: {e}"))?;
(plot_x_range, plot_y_range, x_spec, y_spec)
};
let series_start = std::time::Instant::now();
let plot_x_start = plot_x_range.start as f32;
let plot_y_start = plot_y_range.start as f32;
let plot_width = (plot_x_range.end - plot_x_range.start) as f32;
let plot_height = (plot_y_range.end - plot_y_range.start) as f32;
let data_width = x_spec.end - x_spec.start;
let data_height = y_spec.end - y_spec.start;
let mut pixel_count = 0;
let total_pixels = pixels.len();
let chunk_size = 1000;
for pixel in &pixels {
let data_x = pixel.x;
let data_y = pixel.y;
let rel_x = (data_x - x_spec.start) / data_width;
let rel_y = (y_spec.end - data_y) / data_height;
let screen_x = (plot_x_start + rel_x * plot_width) as i32;
let screen_y = (plot_y_start + rel_y * plot_height) as i32;
if screen_x >= plot_x_range.start
&& screen_x < plot_x_range.end
&& screen_y >= plot_y_range.start
&& screen_y < plot_y_range.end
{
let px = screen_x as u32;
let py = screen_y as u32;
let idx = ((py * width + px) * 3) as usize;
if idx + 2 < pixel_buffer.len() {
pixel_buffer[idx] = pixel.r;
pixel_buffer[idx + 1] = pixel.g;
pixel_buffer[idx + 2] = pixel.b;
}
}
pixel_count += 1;
if pixel_count % chunk_size == 0 || pixel_count == total_pixels {
let percent = (pixel_count as f32 / total_pixels as f32) * 100.0;
let chunk_start = (pixel_count - chunk_size.min(pixel_count)).max(0);
let chunk_end = pixel_count;
let chunk_pixels: Vec<RawPixelData> = pixels
.iter()
.skip(chunk_start)
.take(chunk_end - chunk_start)
.map(|p| RawPixelData {
x: p.x,
y: p.y,
r: p.r,
g: p.g,
b: p.b,
})
.collect();
render_config.report_progress(ProgressInfo {
pixels: chunk_pixels,
percent,
});
}
}
eprintln!(
" ├─ Direct pixel writing: {:?} ({} pixels)",
series_start.elapsed(),
pixels.len()
);
eprintln!(" ├─ Total plotting: {:?}", setup_start.elapsed());
let img_start = std::time::Instant::now();
let img: RgbImage = image::ImageBuffer::from_vec(width, height, pixel_buffer)
.ok_or_else(|| anyhow::anyhow!("plot image buffer had unexpected size"))?;
eprintln!(" ├─ Image buffer conversion: {:?}", img_start.elapsed());
let encode_start = std::time::Instant::now();
let raw_size = (width * height * 3) as usize;
let estimated_jpeg_size = raw_size / 8; let mut encoded_data = Vec::with_capacity(estimated_jpeg_size);
let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut encoded_data, 85);
encoder
.encode(img.as_raw(), width, height, image::ExtendedColorType::Rgb8)
.map_err(|e| anyhow::anyhow!("failed to JPEG encode plot: {e}"))?;
eprintln!(" └─ JPEG encoding: {:?}", encode_start.elapsed());
Ok(encoded_data)
}
pub fn render_contour(
contour_data: ContourData,
options: &DensityPlotOptions,
_render_config: &mut RenderConfig,
) -> Result<PlotBytes> {
use crate::options::PlotOptions;
let base = options.base();
let width = base.width;
let height = base.height;
let margin = base.margin;
let x_label_area_size = base.x_label_area_size;
let y_label_area_size = base.y_label_area_size;
let (x_spec, y_spec) = create_axis_specs(
&options.x_axis.range,
&options.y_axis.range,
&options.x_axis.transform,
&options.y_axis.transform,
)?;
let mut pixel_buffer = vec![255; (width * height * 3) as usize];
{
let backend = BitMapBackend::with_buffer(&mut pixel_buffer, (width, height));
let root = backend.into_drawing_area();
root.fill(&WHITE)
.map_err(|e| anyhow::anyhow!("failed to fill plot background: {e}"))?;
let x_transform_clone = options.x_axis.transform.clone();
let y_transform_clone = options.y_axis.transform.clone();
let x_formatter = move |x: &f64| -> String {
format_transform_value(&x_transform_clone, &(*x as f32))
};
let y_formatter = move |y: &f64| -> String {
format_transform_value(&y_transform_clone, &(*y as f32))
};
let mut chart = ChartBuilder::on(&root)
.margin(margin)
.x_label_area_size(x_label_area_size)
.y_label_area_size(y_label_area_size)
.build_cartesian_2d(
x_spec.start as f64..x_spec.end as f64,
y_spec.start as f64..y_spec.end as f64,
)?;
let mut mesh = chart.configure_mesh();
mesh.x_max_light_lines(4)
.y_max_light_lines(4)
.x_labels(10)
.y_labels(10)
.x_label_formatter(&x_formatter)
.y_label_formatter(&y_formatter);
if let Some(ref x_label) = options.x_axis.label {
mesh.x_desc(x_label);
}
if let Some(ref y_label) = options.y_axis.label {
mesh.y_desc(y_label);
}
mesh.draw()
.map_err(|e| anyhow::anyhow!("failed to draw plot mesh: {e}"))?;
let stroke_width = options.contour_line_thickness.max(0.5).min(5.0) as u32;
let contour_color = RGBColor(60, 60, 60);
for path in &contour_data.contours {
if path.len() < 2 {
continue;
}
let points: Vec<(f64, f64)> = path.iter().copied().collect();
chart
.draw_series(LineSeries::new(
points,
contour_color.stroke_width(stroke_width),
))
.map_err(|e| anyhow::anyhow!("failed to draw contour: {e}"))?;
}
if !contour_data.outliers.is_empty() {
let outlier_color = RGBColor(150, 150, 150);
chart
.draw_series(
contour_data
.outliers
.iter()
.map(|&(x, y)| Circle::new((x, y), 2, outlier_color.filled())),
)
.map_err(|e| anyhow::anyhow!("failed to draw outliers: {e}"))?;
}
root.present()
.map_err(|e| anyhow::anyhow!("failed to present plotters buffer: {e}"))?;
}
let img: RgbImage =
image::ImageBuffer::from_vec(width, height, pixel_buffer)
.ok_or_else(|| anyhow::anyhow!("plot image buffer had unexpected size"))?;
let mut encoded_data = Vec::new();
let mut encoder =
image::codecs::jpeg::JpegEncoder::new_with_quality(&mut encoded_data, 85);
encoder
.encode(img.as_raw(), width, height, image::ExtendedColorType::Rgb8)
.map_err(|e| anyhow::anyhow!("failed to JPEG encode plot: {e}"))?;
Ok(encoded_data)
}
pub fn render_spectral_signature(
data: (Vec<(usize, f64)>, Vec<String>),
options: &crate::options::spectral::SpectralSignaturePlotOptions,
_render_config: &mut RenderConfig,
) -> Result<PlotBytes> {
use crate::options::PlotOptions;
use plotters::prelude::*;
let (spectrum_data, channel_names) = data;
let base = options.base();
let width = base.width;
let height = base.height;
let margin = base.margin;
let x_label_area_size = base.x_label_area_size;
let y_label_area_size = base.y_label_area_size;
let mut pixel_buffer = vec![255; (width * height * 3) as usize];
let x_min = 0.0f32;
let x_max = spectrum_data
.iter()
.map(|(idx, _)| *idx as f32)
.fold(0.0f32, f32::max)
.max(1.0);
let y_min = 0.0f32;
let y_max = 1.0f32;
let channel_names_clone = channel_names.clone();
{
let backend = BitMapBackend::with_buffer(&mut pixel_buffer, (width, height));
let root = backend.into_drawing_area();
root.fill(&WHITE)
.map_err(|e| anyhow::anyhow!("failed to fill plot background: {e}"))?;
let mut chart = ChartBuilder::on(&root)
.margin(margin)
.x_label_area_size(x_label_area_size)
.y_label_area_size(y_label_area_size)
.build_cartesian_2d(x_min..x_max, y_min..y_max)
.map_err(|e| anyhow::anyhow!("failed to build chart: {e}"))?;
let x_formatter: Option<Box<dyn Fn(&f32) -> String>> = if !channel_names_clone.is_empty()
&& channel_names_clone.len() == spectrum_data.len()
{
let channel_names_for_formatter = channel_names_clone.clone();
Some(Box::new(move |x: &f32| -> String {
let idx = x.round() as usize;
if idx < channel_names_for_formatter.len() {
channel_names_for_formatter[idx].clone()
} else {
format!("{:.0}", x)
}
}))
} else {
None
};
let mut mesh = chart.configure_mesh();
if options.show_grid {
mesh.x_max_light_lines(4).y_max_light_lines(4);
}
if let Some(ref x_axis) = options.x_axis {
if let Some(ref label) = x_axis.label {
mesh.x_desc(label);
}
} else {
mesh.x_desc("Channel");
}
if let Some(ref y_axis) = options.y_axis {
if let Some(ref label) = y_axis.label {
mesh.y_desc(label);
}
} else {
mesh.y_desc("Normalized Intensity");
}
if let Some(ref formatter) = x_formatter {
mesh.x_label_formatter(formatter);
}
let x_label_count = if !channel_names_clone.is_empty() {
channel_names_clone.len().min(20) } else {
10
};
mesh.x_labels(x_label_count)
.y_labels(10)
.draw()
.map_err(|e| anyhow::anyhow!("failed to draw mesh: {e}"))?;
if !spectrum_data.is_empty() {
let line_color = if options.line_color.starts_with('#') {
let hex = &options.line_color[1..];
if hex.len() == 6 {
let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(31);
let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(119);
let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(180);
RGBColor(r, g, b)
} else {
RGBColor(31, 119, 180) }
} else {
RGBColor(31, 119, 180) };
chart
.draw_series(LineSeries::new(
spectrum_data
.iter()
.map(|(idx, val)| (*idx as f32, *val as f32)),
line_color.stroke_width(options.line_width as u32),
))
.map_err(|e| anyhow::anyhow!("failed to draw line series: {e}"))?;
}
}
let img: RgbImage = image::ImageBuffer::from_vec(width, height, pixel_buffer)
.ok_or_else(|| anyhow::anyhow!("plot image buffer had unexpected size"))?;
let mut encoded_data = Vec::new();
let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut encoded_data, 85);
encoder
.encode(img.as_raw(), width, height, image::ExtendedColorType::Rgb8)
.map_err(|e| anyhow::anyhow!("failed to JPEG encode plot: {e}"))?;
Ok(encoded_data)
}
pub fn render_histogram(
data: crate::histogram_data::HistogramData,
options: &crate::options::HistogramPlotOptions,
_render_config: &mut RenderConfig,
) -> Result<PlotBytes> {
use crate::histogram_data::{bin_values, BinnedHistogram, HistogramData, HistogramSeries};
use crate::options::PlotOptions;
use plotters::prelude::*;
let base = options.base();
let width = base.width;
let height = base.height;
let margin = base.margin;
let x_label_area_size = base.x_label_area_size;
let y_label_area_size = base.y_label_area_size;
let x_min = *options.x_axis.range.start() as f64;
let x_max = *options.x_axis.range.end() as f64;
let series: Vec<(BinnedHistogram, u32)> = match data {
HistogramData::RawValues(values) => {
let binned = bin_values(
&values,
options.num_bins,
*options.x_axis.range.start(),
*options.x_axis.range.end(),
);
match binned {
Some(b) => vec![(b, 0)],
None => vec![],
}
}
HistogramData::PreBinned { bin_edges, counts } => {
let bin_centers: Vec<f64> = bin_edges
.windows(2)
.map(|w| (w[0] as f64 + w[1] as f64) / 2.0)
.collect();
let counts_f64: Vec<f64> = counts.iter().map(|&c| c as f64).collect();
vec![(
BinnedHistogram {
bin_centers,
counts: counts_f64,
},
0,
)]
}
HistogramData::Overlaid(overlaid) => {
let mut result = Vec::with_capacity(overlaid.len());
for HistogramSeries { values, gate_id } in overlaid {
if let Some(binned) = bin_values(
&values,
options.num_bins,
*options.x_axis.range.start(),
*options.x_axis.range.end(),
) {
result.push((binned, gate_id));
}
}
result
}
};
if series.is_empty() {
return render_empty_histogram(
options, width, height, margin, x_label_area_size, y_label_area_size, x_min, x_max,
);
}
let series: Vec<(BinnedHistogram, u32)> = if options.scale_to_peak && series.len() > 1 {
series
.into_iter()
.map(|(mut binned, gate_id)| {
let max_count = binned.counts.iter().cloned().fold(0.0f64, f64::max);
if max_count > 0.0 {
binned.counts.iter_mut().for_each(|c| *c /= max_count);
}
(binned, gate_id)
})
.collect()
} else {
series
};
let (y_min, y_max) = if options.baseline_separation > 0.0 && series.len() > 1 {
let mut max_y = 0.0f64;
let mut cumulative_offset = 0.0f64;
for (binned, _) in &series {
let peak = binned.counts.iter().cloned().fold(0.0f64, f64::max);
max_y = max_y.max(cumulative_offset + peak);
cumulative_offset += options.baseline_separation as f64;
}
(0.0, (max_y * 1.05).max(0.1))
} else {
let global_max = series
.iter()
.flat_map(|(b, _)| b.counts.iter())
.cloned()
.fold(0.0f64, f64::max);
(0.0, (global_max * 1.05).max(0.1))
};
let mut pixel_buffer = vec![255; (width * height * 3) as usize];
{
let backend = BitMapBackend::with_buffer(&mut pixel_buffer, (width, height));
let root = backend.into_drawing_area();
root.fill(&WHITE)
.map_err(|e| anyhow::anyhow!("failed to fill plot background: {e}"))?;
let mut chart = ChartBuilder::on(&root)
.margin(margin)
.x_label_area_size(x_label_area_size)
.y_label_area_size(y_label_area_size)
.build_cartesian_2d(x_min..x_max, y_min..y_max)
.map_err(|e| anyhow::anyhow!("failed to build histogram chart: {e}"))?;
let mut mesh = chart.configure_mesh();
mesh.x_max_light_lines(4).y_max_light_lines(4)
.x_labels(10)
.y_labels(10);
if let Some(ref label) = options.x_axis.label {
mesh.x_desc(label);
} else {
mesh.x_desc("Value");
}
mesh.y_desc("Count");
mesh.draw()
.map_err(|e| anyhow::anyhow!("failed to draw mesh: {e}"))?;
let baseline_sep = options.baseline_separation as f64;
let mut y_offset = 0.0f64;
for (binned, gate_id) in &series {
let (r, g, b) = options.gate_color(*gate_id);
let color = RGBColor(r, g, b);
let fill_color = RGBColor(r, g, b).mix(0.3);
let points: Vec<(f64, f64)> = binned
.bin_centers
.iter()
.zip(binned.counts.iter())
.map(|(x, c)| (*x, y_offset + *c))
.collect();
if points.is_empty() {
y_offset += baseline_sep;
continue;
}
if options.histogram_filled {
chart
.draw_series(AreaSeries::new(
points.iter().copied(),
y_offset,
fill_color,
))
.map_err(|e| anyhow::anyhow!("failed to draw area series: {e}"))?;
chart
.draw_series(LineSeries::new(
points.iter().copied(),
color.stroke_width(options.line_width as u32),
))
.map_err(|e| anyhow::anyhow!("failed to draw histogram line: {e}"))?;
} else {
chart
.draw_series(LineSeries::new(
points.iter().copied(),
color.stroke_width(options.line_width as u32),
))
.map_err(|e| anyhow::anyhow!("failed to draw histogram line: {e}"))?;
}
y_offset += baseline_sep;
}
}
let img: RgbImage = image::ImageBuffer::from_vec(width, height, pixel_buffer)
.ok_or_else(|| anyhow::anyhow!("plot image buffer had unexpected size"))?;
let mut encoded_data = Vec::new();
let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut encoded_data, 85);
encoder
.encode(img.as_raw(), width, height, image::ExtendedColorType::Rgb8)
.map_err(|e| anyhow::anyhow!("failed to JPEG encode plot: {e}"))?;
Ok(encoded_data)
}
fn render_empty_histogram(
options: &crate::options::HistogramPlotOptions,
width: u32,
height: u32,
margin: u32,
x_label_area_size: u32,
y_label_area_size: u32,
x_min: f64,
x_max: f64,
) -> Result<PlotBytes> {
use plotters::prelude::*;
let mut pixel_buffer = vec![255; (width * height * 3) as usize];
{
let backend = BitMapBackend::with_buffer(&mut pixel_buffer, (width, height));
let root = backend.into_drawing_area();
root.fill(&WHITE)
.map_err(|e| anyhow::anyhow!("failed to fill plot background: {e}"))?;
let mut chart = ChartBuilder::on(&root)
.margin(margin)
.x_label_area_size(x_label_area_size)
.y_label_area_size(y_label_area_size)
.build_cartesian_2d(x_min..x_max, 0.0f64..1.0f64)
.map_err(|e| anyhow::anyhow!("failed to build histogram chart: {e}"))?;
let mut mesh = chart.configure_mesh();
mesh.x_max_light_lines(4).y_max_light_lines(4);
if let Some(ref label) = options.x_axis.label {
mesh.x_desc(label);
}
mesh.y_desc("Count")
.draw()
.map_err(|e| anyhow::anyhow!("failed to draw mesh: {e}"))?;
}
let img: RgbImage = image::ImageBuffer::from_vec(width, height, pixel_buffer)
.ok_or_else(|| anyhow::anyhow!("plot image buffer had unexpected size"))?;
let mut encoded_data = Vec::new();
let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut encoded_data, 85);
encoder
.encode(img.as_raw(), width, height, image::ExtendedColorType::Rgb8)
.map_err(|e| anyhow::anyhow!("failed to JPEG encode plot: {e}"))?;
Ok(encoded_data)
}