flow_plots/render/
plotters_backend.rs

1use crate::PlotBytes;
2use crate::create_axis_specs;
3use crate::density_calc::RawPixelData;
4use crate::options::DensityPlotOptions;
5use crate::render::{ProgressInfo, RenderConfig};
6use flow_fcs::TransformType;
7
8/// Format a value using the transform type
9///
10/// This replicates the Formattable::format logic since the trait is not exported.
11fn format_transform_value(transform: &TransformType, value: &f32) -> String {
12    match transform {
13        TransformType::Linear => format!("{:.1e}", value),
14        TransformType::Arcsinh { cofactor } => {
15            // Convert from transformed space back to original space
16            let original_value = (value / cofactor).sinh() * cofactor;
17            // Make nice rounded labels in original space
18            format!("{:.1e}", original_value)
19        }
20    }
21}
22use anyhow::Result;
23use image::RgbImage;
24use plotters::{
25    backend::BitMapBackend, chart::ChartBuilder, prelude::IntoDrawingArea, style::WHITE,
26};
27
28/// Render pixels to a JPEG image using the Plotters backend
29///
30/// This function handles the complete rendering pipeline:
31/// 1. Sets up Plotters chart with axes and mesh
32/// 2. Writes pixels directly to the buffer
33/// 3. Encodes to JPEG format
34///
35/// Progress reporting is handled via the RenderConfig if provided.
36pub fn render_pixels(
37    pixels: Vec<RawPixelData>,
38    options: &DensityPlotOptions,
39    render_config: &mut RenderConfig,
40) -> Result<PlotBytes> {
41    use crate::options::PlotOptions;
42
43    let base = options.base();
44    let width = base.width;
45    let height = base.height;
46    let margin = base.margin;
47    let x_label_area_size = base.x_label_area_size;
48    let y_label_area_size = base.y_label_area_size;
49
50    let setup_start = std::time::Instant::now();
51    // Use RGB buffer (3 bytes per pixel) since we'll encode to JPEG which doesn't support alpha
52    let mut pixel_buffer = vec![255; (width * height * 3) as usize];
53
54    let (plot_x_range, plot_y_range, x_spec, y_spec) = {
55        let backend = BitMapBackend::with_buffer(&mut pixel_buffer, (width, height));
56        let root = backend.into_drawing_area();
57        root.fill(&WHITE)
58            .map_err(|e| anyhow::anyhow!("failed to fill plot background: {e}"))?;
59
60        // Create appropriate ranges based on transform types
61        let (x_spec, y_spec) = create_axis_specs(
62            &options.x_axis.range,
63            &options.y_axis.range,
64            &options.x_axis.transform,
65            &options.y_axis.transform,
66        )?;
67
68        let mut chart = ChartBuilder::on(&root)
69            .margin(margin)
70            .x_label_area_size(x_label_area_size)
71            .y_label_area_size(y_label_area_size)
72            .build_cartesian_2d(x_spec.start..x_spec.end, y_spec.start..y_spec.end)?;
73
74        // Clone transforms to avoid lifetime issues with closures
75        let x_transform_clone = options.x_axis.transform.clone();
76        let y_transform_clone = options.y_axis.transform.clone();
77
78        // Create owned closures for formatters
79        let x_formatter =
80            move |x: &f32| -> String { format_transform_value(&x_transform_clone, x) };
81        let y_formatter =
82            move |y: &f32| -> String { format_transform_value(&y_transform_clone, y) };
83
84        let mut mesh = chart.configure_mesh();
85        mesh.x_max_light_lines(4)
86            .y_max_light_lines(4)
87            .x_labels(10)
88            .y_labels(10)
89            .x_label_formatter(&x_formatter)
90            .y_label_formatter(&y_formatter);
91
92        // Add axis labels if provided
93        if let Some(ref x_label) = options.x_axis.label {
94            mesh.x_desc(x_label);
95        }
96        if let Some(ref y_label) = options.y_axis.label {
97            mesh.y_desc(y_label);
98        }
99
100        let mesh_start = std::time::Instant::now();
101        mesh.draw()
102            .map_err(|e| anyhow::anyhow!("failed to draw plot mesh: {e}"))?;
103        eprintln!("    ├─ Mesh drawing: {:?}", mesh_start.elapsed());
104
105        // Get the plotting area bounds (we'll use these after Plotters releases the buffer)
106        let plotting_area = chart.plotting_area();
107        let (plot_x_range, plot_y_range) = plotting_area.get_pixel_range();
108
109        root.present()
110            .map_err(|e| anyhow::anyhow!("failed to present plotters buffer: {e}"))?;
111
112        (plot_x_range, plot_y_range, x_spec, y_spec)
113    }; // End Plotters scope - pixel_buffer is now released and we can write to it
114
115    // DIRECT PIXEL BUFFER WRITING - 10-50x faster than Plotters series rendering
116    // Now that Plotters has released pixel_buffer, we can write directly
117    let series_start = std::time::Instant::now();
118
119    let plot_x_start = plot_x_range.start as f32;
120    let plot_y_start = plot_y_range.start as f32;
121    let plot_width = (plot_x_range.end - plot_x_range.start) as f32;
122    let plot_height = (plot_y_range.end - plot_y_range.start) as f32;
123
124    // Calculate scale factors from data coordinates to screen pixels
125    let data_width = x_spec.end - x_spec.start;
126    let data_height = y_spec.end - y_spec.start;
127
128    // Stream pixel chunks during rendering using configurable chunk size
129    let mut pixel_count = 0;
130    let total_pixels = pixels.len();
131    let chunk_size = 1000; // Default chunk size for progress reporting
132
133    // Write each pixel directly to the buffer
134    for pixel in &pixels {
135        let data_x = pixel.x;
136        let data_y = pixel.y;
137
138        // Transform data coordinates to screen pixel coordinates
139        let rel_x = (data_x - x_spec.start) / data_width;
140        let rel_y = (y_spec.end - data_y) / data_height; // Flip Y (screen coords go down)
141
142        let screen_x = (plot_x_start + rel_x * plot_width) as i32;
143        let screen_y = (plot_y_start + rel_y * plot_height) as i32;
144
145        // Bounds check
146        if screen_x >= plot_x_range.start
147            && screen_x < plot_x_range.end
148            && screen_y >= plot_y_range.start
149            && screen_y < plot_y_range.end
150        {
151            let px = screen_x as u32;
152            let py = screen_y as u32;
153
154            // Write to pixel buffer (RGB format - 3 bytes per pixel)
155            let idx = ((py * width + px) * 3) as usize;
156
157            if idx + 2 < pixel_buffer.len() {
158                pixel_buffer[idx] = pixel.r;
159                pixel_buffer[idx + 1] = pixel.g;
160                pixel_buffer[idx + 2] = pixel.b;
161            }
162        }
163
164        pixel_count += 1;
165
166        // Emit progress every chunk_size pixels
167        if pixel_count % chunk_size == 0 || pixel_count == total_pixels {
168            let percent = (pixel_count as f32 / total_pixels as f32) * 100.0;
169
170            // Create a small sample of pixels for this chunk (for visualization)
171            let chunk_start = (pixel_count - chunk_size.min(pixel_count)).max(0);
172            let chunk_end = pixel_count;
173            let chunk_pixels: Vec<RawPixelData> = pixels
174                .iter()
175                .skip(chunk_start)
176                .take(chunk_end - chunk_start)
177                .map(|p| RawPixelData {
178                    x: p.x,
179                    y: p.y,
180                    r: p.r,
181                    g: p.g,
182                    b: p.b,
183                })
184                .collect();
185
186            render_config.report_progress(ProgressInfo {
187                pixels: chunk_pixels,
188                percent,
189            });
190        }
191    }
192
193    eprintln!(
194        "    ├─ Direct pixel writing: {:?} ({} pixels)",
195        series_start.elapsed(),
196        pixels.len()
197    );
198    eprintln!("    ├─ Total plotting: {:?}", setup_start.elapsed());
199
200    let img_start = std::time::Instant::now();
201    let img: RgbImage = image::ImageBuffer::from_vec(width, height, pixel_buffer)
202        .ok_or_else(|| anyhow::anyhow!("plot image buffer had unexpected size"))?;
203    eprintln!("    ├─ Image buffer conversion: {:?}", img_start.elapsed());
204
205    let encode_start = std::time::Instant::now();
206
207    // Pre-allocate Vec with estimated JPEG size
208    // RGB buffer is (width * height * 3) bytes
209    // JPEG at quality 85 typically compresses to ~10-15% of raw size for density plots
210    let raw_size = (width * height * 3) as usize;
211    let estimated_jpeg_size = raw_size / 8; // Conservative estimate (~12.5% of raw)
212    let mut encoded_data = Vec::with_capacity(estimated_jpeg_size);
213
214    // JPEG encoding is faster and produces smaller files for density plots
215    // Quality 85 provides good visual quality with ~2x smaller file size vs PNG
216    let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut encoded_data, 85);
217    encoder
218        .encode(img.as_raw(), width, height, image::ExtendedColorType::Rgb8)
219        .map_err(|e| anyhow::anyhow!("failed to JPEG encode plot: {e}"))?;
220    eprintln!("    └─ JPEG encoding: {:?}", encode_start.elapsed());
221
222    // Return the JPEG-encoded bytes directly
223    Ok(encoded_data)
224}