Skip to main content

flow_plots/
lib.rs

1//! # flow-plots
2//!
3//! A library for creating visualizations of flow cytometry data.
4//!
5//! ## Overview
6//!
7//! This library provides a flexible, extensible API for creating different types of plots
8//! from flow cytometry data. The architecture is designed to be easily extended with new
9//! plot types.
10//!
11//! ## Basic Usage
12//!
13//! ```rust,no_run
14//! use flow_plots::{DensityPlot, DensityPlotOptions, Plot};
15//! use flow_plots::options::BasePlotOptions;
16//! use flow_plots::render::RenderConfig;
17//!
18//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
19//! let plot = DensityPlot::new();
20//! let options = DensityPlotOptions::new()
21//!     .base(BasePlotOptions::new().width(800u32).height(600u32).build()?)
22//!     .build()?;
23//! let data: Vec<(f32, f32)> = vec![(100.0, 200.0)];
24//! let mut render_config = RenderConfig::default();
25//! let bytes = plot.render(data.into(), &options, &mut render_config)?;
26//! # Ok(())
27//! # }
28//! ```
29//!
30//! ## Architecture
31//!
32//! The library is organized into several modules:
33//!
34//! - `options`: Plot configuration types using the builder pattern
35//! - `plots`: Plot implementations (currently `DensityPlot`)
36//! - `render`: Rendering infrastructure and progress reporting
37//! - `density`: Density calculation algorithms
38//! - `colormap`: Color map implementations
39//! - `helpers`: Helper functions for common initialization patterns
40
41pub mod colormap;
42pub mod contour;
43pub mod density_calc;
44pub mod helpers;
45pub mod histogram_data;
46pub mod options;
47pub mod plots;
48pub mod scatter_data;
49pub mod render;
50pub mod signal_heatmap;
51
52// Re-export commonly used types
53pub use colormap::ColorMaps;
54pub use histogram_data::{HistogramData, HistogramDataError, HistogramSeries};
55pub use options::{
56    AxisOptions, BasePlotOptions, DensityPlotOptions, HistogramPlotOptions, PlotOptions,
57    SpectralSignaturePlotOptions,
58};
59pub use plots::{DensityPlot, HistogramPlot, Plot, PlotType, SpectralSignaturePlot};
60pub use scatter_data::{ScatterDataError, ScatterPlotData};
61pub use render::{ProgressCallback, ProgressInfo, RenderConfig};
62pub use signal_heatmap::{generate_normalized_spectral_signature_plot, generate_signal_heatmap};
63
64// Type aliases
65pub type PlotBytes = Vec<u8>;
66pub type PlotRange = std::ops::RangeInclusive<f32>;
67
68use flow_fcs::TransformType;
69use std::ops::Range;
70
71/// @deprecated The old PlotOptions struct has been removed. Use DensityPlotOptions with builder pattern instead.
72///
73/// The old `PlotOptions` struct mixed concerns and was difficult to extend.
74/// It has been replaced with a hierarchy of option types:
75/// - `BasePlotOptions`: Layout and display settings
76/// - `AxisOptions`: Axis configuration
77/// - `DensityPlotOptions`: Complete density plot configuration
78///
79/// See the module documentation for examples of the new API.
80
81/// Create appropriate axis specifications with nice bounds and labels
82///
83/// This function creates axis ranges that work well with the specified transforms,
84/// using "nice" number bounds for linear scales.
85pub fn create_axis_specs(
86    plot_range_x: &PlotRange,
87    plot_range_y: &PlotRange,
88    x_transform: &TransformType,
89    y_transform: &TransformType,
90) -> anyhow::Result<(Range<f32>, Range<f32>)> {
91    // For linear scales, use nice number bounds
92    // For arcsinh and biexponential, ensure we use proper transformed bounds
93    let x_spec = match x_transform {
94        TransformType::Linear => {
95            let min = plot_range_x.start();
96            let max = plot_range_x.end();
97            let (nice_min, nice_max) = nice_bounds(*min, *max);
98            nice_min..nice_max
99        }
100        TransformType::Arcsinh { cofactor: _ } | TransformType::Biexponential { .. } => {
101            // Keep the transformed range but we'll format nicely in the formatter
102            *plot_range_x.start()..*plot_range_x.end()
103        }
104    };
105
106    let y_spec = match y_transform {
107        TransformType::Linear => {
108            let min = plot_range_y.start();
109            let max = plot_range_y.end();
110            let (nice_min, nice_max) = nice_bounds(*min, *max);
111            nice_min..nice_max
112        }
113        TransformType::Arcsinh { cofactor: _ } | TransformType::Biexponential { .. } => {
114            // Keep the transformed range but we'll format nicely in the formatter
115            *plot_range_y.start()..*plot_range_y.end()
116        }
117    };
118
119    Ok((x_spec.into(), y_spec.into()))
120}
121
122/// Calculate percentile bounds for a dataset
123///
124/// Returns a range that encompasses the specified percentiles of the data,
125/// rounded to "nice" numbers for better axis display.
126pub fn get_percentile_bounds(
127    values: &[f32],
128    percentile_low: f32,
129    percentile_high: f32,
130) -> PlotRange {
131    let mut sorted_values = values.to_vec();
132    sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
133
134    let low_index = (percentile_low * sorted_values.len() as f32).floor() as usize;
135    let high_index = (percentile_high * sorted_values.len() as f32).ceil() as usize;
136
137    // Ensure indices are within bounds
138    let low_index = low_index.clamp(0, sorted_values.len() - 1);
139    let high_index = high_index.clamp(0, sorted_values.len() - 1);
140
141    let low_value = sorted_values[low_index];
142    let high_value = sorted_values[high_index];
143
144    // Round to nice numbers
145    let min_bound = nearest_nice_number(low_value, RoundingDirection::Down);
146    let max_bound = nearest_nice_number(high_value, RoundingDirection::Up);
147
148    min_bound..=max_bound
149}
150
151fn nice_bounds(min: f32, max: f32) -> (f32, f32) {
152    if min.is_infinite() || max.is_infinite() || min.is_nan() || max.is_nan() {
153        return (0.0, 1.0); // Fallback for invalid ranges
154    }
155
156    let range = max - min;
157    if range == 0.0 {
158        return (min - 0.5, min + 0.5); // Handle single-point case
159    }
160
161    // Find nice step size
162    let step_size = 10_f32.powf((range.log10()).floor());
163    let nice_min = (min / step_size).floor() * step_size;
164    let nice_max = (max / step_size).ceil() * step_size;
165
166    (nice_min, nice_max)
167}
168
169enum RoundingDirection {
170    Up,
171    Down,
172}
173
174fn nearest_nice_number(value: f32, direction: RoundingDirection) -> f32 {
175    // Handle edge cases
176    if value == 0.0 {
177        return 0.0;
178    }
179
180    let abs_value = value.abs();
181    let exponent = abs_value.log10().floor() as i32;
182    let factor = 10f32.powi(exponent);
183
184    // Find nearest nice number based on direction
185    let nice_value = match direction {
186        RoundingDirection::Up => {
187            let mantissa = (abs_value / factor).ceil();
188            if mantissa <= 1.0 {
189                1.0 * factor
190            } else if mantissa <= 2.0 {
191                2.0 * factor
192            } else if mantissa <= 5.0 {
193                5.0 * factor
194            } else {
195                10.0 * factor
196            }
197        }
198        RoundingDirection::Down => {
199            let mantissa = (abs_value / factor).floor();
200            if mantissa >= 5.0 {
201                5.0 * factor
202            } else if mantissa >= 2.0 {
203                2.0 * factor
204            } else if mantissa >= 1.0 {
205                1.0 * factor
206            } else {
207                0.5 * factor
208            }
209        }
210    };
211
212    // Preserve sign
213    if value.is_sign_negative() {
214        -nice_value
215    } else {
216        nice_value
217    }
218}