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