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, &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 density_calc;
43pub mod helpers;
44pub mod options;
45pub mod plots;
46pub mod render;
47pub mod signal_heatmap;
48
49// Re-export commonly used types
50pub use colormap::ColorMaps;
51pub use options::{
52    AxisOptions, BasePlotOptions, DensityPlotOptions, PlotOptions, SpectralSignaturePlotOptions,
53};
54pub use plots::{DensityPlot, Plot, PlotType, SpectralSignaturePlot};
55pub use render::{ProgressCallback, ProgressInfo, RenderConfig};
56pub use signal_heatmap::{generate_normalized_spectral_signature_plot, generate_signal_heatmap};
57
58// Type aliases
59pub type PlotBytes = Vec<u8>;
60pub type PlotRange = std::ops::RangeInclusive<f32>;
61
62use flow_fcs::TransformType;
63use std::ops::Range;
64
65/// @deprecated The old PlotOptions struct has been removed. Use DensityPlotOptions with builder pattern instead.
66///
67/// The old `PlotOptions` struct mixed concerns and was difficult to extend.
68/// It has been replaced with a hierarchy of option types:
69/// - `BasePlotOptions`: Layout and display settings
70/// - `AxisOptions`: Axis configuration
71/// - `DensityPlotOptions`: Complete density plot configuration
72///
73/// See the module documentation for examples of the new API.
74
75/// Create appropriate axis specifications with nice bounds and labels
76///
77/// This function creates axis ranges that work well with the specified transforms,
78/// using "nice" number bounds for linear scales.
79pub fn create_axis_specs(
80    plot_range_x: &PlotRange,
81    plot_range_y: &PlotRange,
82    x_transform: &TransformType,
83    y_transform: &TransformType,
84) -> anyhow::Result<(Range<f32>, Range<f32>)> {
85    // For linear scales, use nice number bounds
86    // For arcsinh and biexponential, ensure we use proper transformed bounds
87    let x_spec = match x_transform {
88        TransformType::Linear => {
89            let min = plot_range_x.start();
90            let max = plot_range_x.end();
91            let (nice_min, nice_max) = nice_bounds(*min, *max);
92            nice_min..nice_max
93        }
94        TransformType::Arcsinh { cofactor: _ } | TransformType::Biexponential { .. } => {
95            // Keep the transformed range but we'll format nicely in the formatter
96            *plot_range_x.start()..*plot_range_x.end()
97        }
98    };
99
100    let y_spec = match y_transform {
101        TransformType::Linear => {
102            let min = plot_range_y.start();
103            let max = plot_range_y.end();
104            let (nice_min, nice_max) = nice_bounds(*min, *max);
105            nice_min..nice_max
106        }
107        TransformType::Arcsinh { cofactor: _ } | TransformType::Biexponential { .. } => {
108            // Keep the transformed range but we'll format nicely in the formatter
109            *plot_range_y.start()..*plot_range_y.end()
110        }
111    };
112
113    Ok((x_spec.into(), y_spec.into()))
114}
115
116/// Calculate percentile bounds for a dataset
117///
118/// Returns a range that encompasses the specified percentiles of the data,
119/// rounded to "nice" numbers for better axis display.
120pub fn get_percentile_bounds(
121    values: &[f32],
122    percentile_low: f32,
123    percentile_high: f32,
124) -> PlotRange {
125    let mut sorted_values = values.to_vec();
126    sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
127
128    let low_index = (percentile_low * sorted_values.len() as f32).floor() as usize;
129    let high_index = (percentile_high * sorted_values.len() as f32).ceil() as usize;
130
131    // Ensure indices are within bounds
132    let low_index = low_index.clamp(0, sorted_values.len() - 1);
133    let high_index = high_index.clamp(0, sorted_values.len() - 1);
134
135    let low_value = sorted_values[low_index];
136    let high_value = sorted_values[high_index];
137
138    // Round to nice numbers
139    let min_bound = nearest_nice_number(low_value, RoundingDirection::Down);
140    let max_bound = nearest_nice_number(high_value, RoundingDirection::Up);
141
142    min_bound..=max_bound
143}
144
145fn nice_bounds(min: f32, max: f32) -> (f32, f32) {
146    if min.is_infinite() || max.is_infinite() || min.is_nan() || max.is_nan() {
147        return (0.0, 1.0); // Fallback for invalid ranges
148    }
149
150    let range = max - min;
151    if range == 0.0 {
152        return (min - 0.5, min + 0.5); // Handle single-point case
153    }
154
155    // Find nice step size
156    let step_size = 10_f32.powf((range.log10()).floor());
157    let nice_min = (min / step_size).floor() * step_size;
158    let nice_max = (max / step_size).ceil() * step_size;
159
160    (nice_min, nice_max)
161}
162
163enum RoundingDirection {
164    Up,
165    Down,
166}
167
168fn nearest_nice_number(value: f32, direction: RoundingDirection) -> f32 {
169    // Handle edge cases
170    if value == 0.0 {
171        return 0.0;
172    }
173
174    let abs_value = value.abs();
175    let exponent = abs_value.log10().floor() as i32;
176    let factor = 10f32.powi(exponent);
177
178    // Find nearest nice number based on direction
179    let nice_value = match direction {
180        RoundingDirection::Up => {
181            let mantissa = (abs_value / factor).ceil();
182            if mantissa <= 1.0 {
183                1.0 * factor
184            } else if mantissa <= 2.0 {
185                2.0 * factor
186            } else if mantissa <= 5.0 {
187                5.0 * factor
188            } else {
189                10.0 * factor
190            }
191        }
192        RoundingDirection::Down => {
193            let mantissa = (abs_value / factor).floor();
194            if mantissa >= 5.0 {
195                5.0 * factor
196            } else if mantissa >= 2.0 {
197                2.0 * factor
198            } else if mantissa >= 1.0 {
199                1.0 * factor
200            } else {
201                0.5 * factor
202            }
203        }
204    };
205
206    // Preserve sign
207    if value.is_sign_negative() {
208        -nice_value
209    } else {
210        nice_value
211    }
212}