# 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
```rust
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:
```rust
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:
```rust
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
```rust
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
plot.render(data, &options, &mut render_config)
})?;
```
### Batch Rendering
For processing multiple plots together, use the batch API:
```rust
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:
```rust
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:
```rust
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:**
```rust
let options = PlotOptions::new(
fcs,
x_parameter,
y_parameter,
Some(800),
Some(600),
None,
None,
None,
None,
None,
)?;
```
**New API:**
```rust
// 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:**
```rust
let (bytes, _, _, _) = draw_plot(pixels, &options)?;
```
**New API:**
```rust
let plot = DensityPlot::new();
let mut render_config = RenderConfig::default();
let bytes = plot.render(data, &options, &mut render_config)?;
```
## License
MIT