flow-plots

Crates.ioflow-plots
lib.rsflow-plots
version0.2.0
created_at2026-01-08 17:57:28.973176+00
updated_at2026-01-21 19:12:46.664063+00
descriptionPackage for drawing and interacting with plots in flow cytometry data
homepage
repositoryhttps://github.com/jrmoynihan/flow/flow-plots
max_upload_size
id2030801
size267,531
James Moynihan (jrmoynihan)

documentation

README

flow-plots

A library for creating visualizations of flow cytometry data.

Overview

This library provides a flexible, extensible API for creating different types of plots from flow cytometry data. The architecture is designed to be easily extended with new plot types while maintaining clean separation of concerns.

Features

  • Extensible Architecture: Easy to add new plot types by implementing the Plot trait
  • Builder Pattern: Type-safe configuration using the builder pattern
  • Progress Reporting: Optional progress callbacks for streaming/progressive rendering
  • Flexible Rendering: Applications can inject their own execution and progress logic

Basic Usage

Simple Density Plot

use flow_plots::{DensityPlot, DensityPlotOptions};
use flow_plots::render::RenderConfig;

let plot = DensityPlot::new();
let options = DensityPlotOptions::new()
    .width(800)
    .height(600)
    .title("My Density Plot")
    .build()?;

let data: Vec<(f32, f32)> = vec![(100.0, 200.0), (150.0, 250.0)];
let mut render_config = RenderConfig::default();
let bytes = plot.render(data, &options, &mut render_config)?;

With FCS File Initialization

This example shows the complete workflow from opening an FCS file to generating a plot:

use flow_plots::{DensityPlot, helpers};
use flow_plots::render::RenderConfig;
use flow_fcs::Fcs;

// Step 1: Open the FCS file from a file path
let fcs = Fcs::open("path/to/your/file.fcs")?;

// Step 2: Select parameters for the x and y axes
// You can find parameters by their channel name (e.g., "FSC-A", "SSC-A", "FL1-A")
let x_parameter = fcs.find_parameter("FSC-A")?;
let y_parameter = fcs.find_parameter("SSC-A")?;

// Step 3: Use the helper function to create plot options with sensible defaults
// This analyzes the FCS file and parameters to determine appropriate ranges and transforms
let mut builder = helpers::density_options_from_fcs(
    &fcs,
    x_parameter,
    y_parameter,
)?;

// Step 4: Customize the options further if needed
let options = builder
    .width(800)
    .height(600)
    .build()?;

// Step 5: Extract the data for plotting
// The helper function uses the parameter's transform to calculate appropriate ranges,
// so we should use the same data (raw or transformed) for plotting
let data: Vec<(f32, f32)> = fcs.get_xy_pairs("FSC-A", "SSC-A")?;

// Step 6: Create and render the plot
let plot = DensityPlot::new();
let mut render_config = RenderConfig::default();
let bytes = plot.render(data, &options, &mut render_config)?;

// Step 7: Use the bytes (JPEG-encoded image) as needed
// e.g., save to file, send over network, display in UI, etc.

Key Points:

  • Opening FCS files: Fcs::open(path) opens and parses an FCS file from a file path. The path must have a .fcs extension. This function:

    • Memory-maps the file for efficient access
    • Parses the header, text segment, and data segment
    • Loads event data into a Polars DataFrame
    • Returns a fully parsed Fcs struct ready for use
  • Finding parameters: fcs.find_parameter(channel_name) finds a parameter by its channel name (e.g., "FSC-A", "SSC-A", "FL1-A"). Returns a Result<&Parameter> - an error if the parameter doesn't exist. To list all available parameters, use fcs.get_parameter_names_from_dataframe() which returns a Vec<String> of all channel names.

  • Automatic option configuration: helpers::density_options_from_fcs() analyzes the FCS file and parameters to automatically:

    • Determine plot ranges based on parameter type:
      • FSC/SSC: Uses default range (0 to 200,000)
      • Time: Uses the actual maximum time value from the data
      • Fluorescence: Calculates percentile bounds (1st to 99th percentile) after applying the parameter's transform to the raw values
    • Apply transformations: For fluorescence parameters, it transforms the raw values using the parameter's TransformType (typically Arcsinh) before calculating percentile bounds, ensuring the plot range reflects the transformed data scale
    • Set axis transforms: Automatically sets Linear transform for FSC/SSC, and the parameter's default transform (usually Arcsinh) for fluorescence
    • Extract metadata: Gets the file name from the $FIL keyword for the plot title
  • Extracting data: fcs.get_xy_pairs(x_param, y_param) extracts (x, y) coordinate pairs for plotting. This returns raw (untransformed) values, which is appropriate since the plot options handle transformation during rendering and axis labeling.

Error Handling:

All operations return Result types. Here's a more complete example with error handling:

use anyhow::Result;

fn create_plot_from_file(path: &str) -> Result<Vec<u8>> {
    // Open the file - returns error if file doesn't exist or is invalid
    let fcs = Fcs::open(path)?;

    // Find parameters - returns error if parameter name doesn't exist
    let x_parameter = fcs.find_parameter("FSC-A")
        .map_err(|e| anyhow::anyhow!("Parameter 'FSC-A' not found: {}", e))?;
    let y_parameter = fcs.find_parameter("SSC-A")
        .map_err(|e| anyhow::anyhow!("Parameter 'SSC-A' not found: {}", e))?;

    // Create options with automatic configuration
    let options = helpers::density_options_from_fcs(&fcs, x_parameter, y_parameter)?
        .width(800)
        .height(600)
        .build()?;

    // Extract data - returns error if parameters don't exist
    let data = fcs.get_xy_pairs("FSC-A", "SSC-A")?;

    // Render the plot
    let plot = DensityPlot::new();
    let mut render_config = RenderConfig::default();
    let bytes = plot.render(data, &options, &mut render_config)?;

    Ok(bytes)
}

With Application Executor and Progress

use flow_plots::{DensityPlot, DensityPlotOptions, RenderConfig, ProgressInfo};
use crate::plot_executor::with_render_lock;
use crate::commands::PlotProgressEvent;

let options = DensityPlotOptions::new()
    .width(800)
    .height(600)
    // ... configure options
    .build()?;

// Configure rendering with app-specific concerns
let mut render_config = RenderConfig {
    progress: Some(Box::new(move |info: ProgressInfo| {
        channel.send(PlotProgressEvent::Progress {
            pixels: info.pixels,
            percent: info.percent,
        })?;
        Ok(())
    })),
};

// Wrap the render call with your executor's render lock
let bytes = with_render_lock(|| {
    let plot = DensityPlot::new();
    plot.render(data, &options, &mut render_config)
})?;

Batch Rendering

For processing multiple plots together, use the batch API:

use flow_plots::{DensityPlot, DensityPlotOptions};
use flow_plots::render::RenderConfig;

let plot = DensityPlot::new();
let mut render_config = RenderConfig::default();

// Prepare multiple plot requests
let requests: Vec<(Vec<(f32, f32)>, DensityPlotOptions)> = vec![
    (
        vec![(100.0, 200.0), (150.0, 250.0)], // Data for plot 1
        DensityPlotOptions::new()
            .width(800)
            .height(600)
            .title("Plot 1")
            .build()?,
    ),
    (
        vec![(200.0, 300.0), (250.0, 350.0)], // Data for plot 2
        DensityPlotOptions::new()
            .width(800)
            .height(600)
            .title("Plot 2")
            .build()?,
    ),
];

// Render all plots in batch
let plot_bytes: Vec<Vec<u8>> = plot.render_batch(&requests, &mut render_config)?;

// plot_bytes[0] contains the JPEG bytes for the first plot
// plot_bytes[1] contains the JPEG bytes for the second plot

Custom Batch Orchestration

For applications that want to orchestrate rendering themselves (e.g., custom progress reporting, parallel rendering, etc.), use the low-level batch density calculation:

use flow_plots::density_calc::calculate_density_per_pixel_batch;
use flow_plots::{DensityPlotOptions};
use flow_plots::render::{RenderConfig, plotters_backend::render_pixels};
use anyhow::Result;

// Calculate density for multiple plots
let requests: Vec<(Vec<(f32, f32)>, DensityPlotOptions)> = vec![
    (data1, options1),
    (data2, options2),
    (data3, options3),
];

// Get raw pixel data for all plots (density calculation only)
let raw_pixels_batch = calculate_density_per_pixel_batch(&requests);

// Now you can orchestrate rendering yourself
let mut render_config = RenderConfig::default();

// Example: Render plots in parallel using rayon
use rayon::prelude::*;
let plot_bytes: Result<Vec<Vec<u8>>> = raw_pixels_batch
    .par_iter()
    .enumerate()
    .map(|(i, raw_pixels)| {
        // Custom rendering logic here
        // You have access to raw_pixels and requests[i].1 (options)
        render_pixels(raw_pixels.clone(), &requests[i].1, &mut render_config)
    })
    .collect();

let plot_bytes = plot_bytes?;

Note: While batch processing is available, sequential processing (calling render() in a loop) is typically faster for most use cases. See GPU_EVALUATION.md for performance analysis.

Architecture

The library is organized into several modules:

  • options: Plot configuration types using the builder pattern
    • BasePlotOptions: Layout and display settings
    • AxisOptions: Axis configuration (range, transform, label)
    • DensityPlotOptions: Complete density plot configuration
  • plots: Plot implementations
    • DensityPlot: 2D density plot implementation
    • Plot trait: Interface for all plot types
  • render: Rendering infrastructure
    • RenderConfig: Configuration for rendering (progress callbacks)
    • ProgressInfo: Progress information structure
    • plotters_backend: Plotters-based rendering implementation
  • density: Density calculation algorithms
  • colormap: Color map implementations
  • helpers: Helper functions for common initialization patterns

Adding New Plot Types

To add a new plot type:

  1. Create a new options struct (e.g., DotPlotOptions) that implements PlotOptions
  2. Create a new plot struct (e.g., DotPlot) that implements the Plot trait
  3. Implement the render method with your plot-specific logic

Example:

use flow_plots::plots::traits::Plot;
use flow_plots::options::PlotOptions;
use flow_plots::render::RenderConfig;
use flow_plots::PlotBytes;
use anyhow::Result;

struct DotPlotOptions {
    base: BasePlotOptions,
    // ... your plot-specific options
}

impl PlotOptions for DotPlotOptions {
    fn base(&self) -> &BasePlotOptions {
        &self.base
    }
}

struct DotPlot;

impl Plot for DotPlot {
    type Options = DotPlotOptions;
    type Data = Vec<(f32, f32)>;

    fn render(
        &self,
        data: Self::Data,
        options: &Self::Options,
        render_config: &mut RenderConfig,
    ) -> Result<PlotBytes> {
        // ... your rendering logic
        Ok(vec![])
    }
}

Migration Guide

From Old PlotOptions API

Old API:

let options = PlotOptions::new(
    fcs,
    x_parameter,
    y_parameter,
    Some(800),
    Some(600),
    None,
    None,
    None,
    None,
    None,
)?;

New API:

// Option 1: Use helper function
let mut builder = helpers::density_options_from_fcs(fcs, x_param, y_param)?;
let options = builder
    .width(800)
    .height(600)
    .build()?;

// Option 2: Manual construction
let options = DensityPlotOptions::new()
    .width(800)
    .height(600)
    .x_axis(|axis| axis
        .range(0.0..=200_000.0)
        .transform(TransformType::Arcsinh { cofactor: 150.0 }))
    .y_axis(|axis| axis
        .range(0.0..=200_000.0)
        .transform(TransformType::Arcsinh { cofactor: 150.0 }))
    .build()?;

From Old draw_plot Function

Old API:

let (bytes, _, _, _) = draw_plot(pixels, &options)?;

New API:

let plot = DensityPlot::new();
let mut render_config = RenderConfig::default();
let bytes = plot.render(data, &options, &mut render_config)?;

License

MIT

Commit count: 0

cargo fmt