1pub mod colormap;
42pub mod density_calc;
43pub mod helpers;
44pub mod options;
45pub mod plots;
46pub mod render;
47pub mod signal_heatmap;
48
49pub 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
58pub type PlotBytes = Vec<u8>;
60pub type PlotRange = std::ops::RangeInclusive<f32>;
61
62use flow_fcs::TransformType;
63use std::ops::Range;
64
65pub 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 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 *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 *plot_range_y.start()..*plot_range_y.end()
110 }
111 };
112
113 Ok((x_spec.into(), y_spec.into()))
114}
115
116pub 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 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 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); }
149
150 let range = max - min;
151 if range == 0.0 {
152 return (min - 0.5, min + 0.5); }
154
155 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 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 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 if value.is_sign_negative() {
208 -nice_value
209 } else {
210 nice_value
211 }
212}