1pub 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
52pub 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
64pub type PlotBytes = Vec<u8>;
66pub type PlotRange = std::ops::RangeInclusive<f32>;
67
68use flow_fcs::TransformType;
69use std::ops::Range;
70
71pub 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 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 *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 *plot_range_y.start()..*plot_range_y.end()
116 }
117 };
118
119 Ok((x_spec.into(), y_spec.into()))
120}
121
122pub 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 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 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); }
155
156 let range = max - min;
157 if range == 0.0 {
158 return (min - 0.5, min + 0.5); }
160
161 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 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 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 if value.is_sign_negative() {
214 -nice_value
215 } else {
216 nice_value
217 }
218}