Skip to main content

flow_plots/render/
plotters_backend.rs

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
10/// Format a value using the transform type
11///
12/// This replicates the Formattable::format logic since the trait is not exported.
13fn format_transform_value(transform: &TransformType, value: &f32) -> String {
14    match transform {
15        TransformType::Linear => format!("{:.1e}", value),
16        TransformType::Arcsinh { cofactor: _ } => {
17            // Convert from transformed space back to original space
18            let original_value = transform.inverse_transform(value);
19            // Make nice rounded labels in original space
20            format!("{:.1e}", original_value)
21        }
22        TransformType::Biexponential { .. } => {
23            // Convert from transformed space back to original space using inverse transform
24            let original_value = transform.inverse_transform(value);
25            // Make nice rounded labels in original space
26            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
36/// Render pixels to a JPEG image using the Plotters backend
37///
38/// This function handles the complete rendering pipeline:
39/// 1. Sets up Plotters chart with axes and mesh
40/// 2. Writes pixels directly to the buffer
41/// 3. Encodes to JPEG format
42///
43/// Progress reporting is handled via the RenderConfig if provided.
44pub 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    // Use RGB buffer (3 bytes per pixel) since we'll encode to JPEG which doesn't support alpha
60    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        // Create appropriate ranges based on transform types
69        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        // Clone transforms to avoid lifetime issues with closures
83        let x_transform_clone = options.x_axis.transform.clone();
84        let y_transform_clone = options.y_axis.transform.clone();
85
86        // Create owned closures for formatters
87        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        // Add axis labels if provided
101        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        // Get the plotting area bounds (we'll use these after Plotters releases the buffer)
114        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    }; // End Plotters scope - pixel_buffer is now released and we can write to it
122
123    // DIRECT PIXEL BUFFER WRITING - 10-50x faster than Plotters series rendering
124    // Now that Plotters has released pixel_buffer, we can write directly
125    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    // Calculate scale factors from data coordinates to screen pixels
133    let data_width = x_spec.end - x_spec.start;
134    let data_height = y_spec.end - y_spec.start;
135
136    // Stream pixel chunks during rendering using configurable chunk size
137    let mut pixel_count = 0;
138    let total_pixels = pixels.len();
139    let chunk_size = 1000; // Default chunk size for progress reporting
140
141    // Write each pixel directly to the buffer
142    for pixel in &pixels {
143        let data_x = pixel.x;
144        let data_y = pixel.y;
145
146        // Transform data coordinates to screen pixel coordinates
147        let rel_x = (data_x - x_spec.start) / data_width;
148        let rel_y = (y_spec.end - data_y) / data_height; // Flip Y (screen coords go down)
149
150        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        // Bounds check
154        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            // Write to pixel buffer (RGB format - 3 bytes per pixel)
163            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        // Emit progress every chunk_size pixels
175        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            // Create a small sample of pixels for this chunk (for visualization)
179            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    // Pre-allocate Vec with estimated JPEG size
216    // RGB buffer is (width * height * 3) bytes
217    // JPEG at quality 85 typically compresses to ~10-15% of raw size for density plots
218    let raw_size = (width * height * 3) as usize;
219    let estimated_jpeg_size = raw_size / 8; // Conservative estimate (~12.5% of raw)
220    let mut encoded_data = Vec::with_capacity(estimated_jpeg_size);
221
222    // JPEG encoding is faster and produces smaller files for density plots
223    // Quality 85 provides good visual quality with ~2x smaller file size vs PNG
224    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    // Return the JPEG-encoded bytes directly
231    Ok(encoded_data)
232}
233
234/// Render contour plot to JPEG using Plotters LineSeries
235///
236/// Draws contour lines from KDE density estimation plus optional outlier scatter points.
237pub 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        // Draw contour lines
304        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        // Draw outlier points if present
318        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
348/// Render spectral signature plot to JPEG
349///
350/// Creates a line plot showing normalized spectral signatures (0.0 to 1.0) across detector channels.
351pub 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    // Create RGB buffer
368    let mut pixel_buffer = vec![255; (width * height * 3) as usize];
369
370    // Determine x and y ranges (use f32 to match plotters expectations)
371    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    // Clone channel_names for the closure
381    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        // Create formatter for x-axis labels if channel names are provided
397        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                // Find closest channel index
403                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        // Configure mesh
415        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        // Set axis labels
421        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        // Apply x-axis formatter if provided
438        if let Some(ref formatter) = x_formatter {
439            mesh.x_label_formatter(formatter);
440        }
441
442        // Set number of x labels to match number of channels for better readability
443        let x_label_count = if !channel_names_clone.is_empty() {
444            channel_names_clone.len().min(20) // Show all channels if <= 20, otherwise show 20
445        } 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        // Draw the spectral signature line
455        if !spectrum_data.is_empty() {
456            // Parse hex color (e.g., "#1f77b4" or "1f77b4")
457            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) // Default blue
466                }
467            } else {
468                RGBColor(31, 119, 180) // Default blue
469            };
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    } // Backend is dropped here, pixel_buffer is now available
481
482    // Convert to image and encode
483    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
495/// Render histogram plot to JPEG
496///
497/// Creates a 1D histogram (x = values, y = count/frequency) with optional fill
498/// and support for overlaid multiple series.
499pub 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    // Convert all data to binned series (list of (BinnedHistogram, gate_id))
519    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        // Empty plot - still render axes
564        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    // Optional: scale each series to its peak (max = 1.0)
570    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    // Compute y range
586    let (y_min, y_max) = if options.baseline_separation > 0.0 && series.len() > 1 {
587        // Stacked: each series gets baseline_separation offset
588        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                // Draw border line on top
664                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}