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