1pub mod colormap;
39pub mod density_calc;
40pub mod helpers;
41pub mod options;
42pub mod plots;
43pub mod render;
44
45pub use colormap::ColorMaps;
47pub use options::{AxisOptions, BasePlotOptions, DensityPlotOptions, PlotOptions};
48pub use plots::{DensityPlot, Plot, PlotType};
49pub use render::{ProgressCallback, ProgressInfo, RenderConfig};
50
51pub type PlotBytes = Vec<u8>;
53pub type PlotRange = std::ops::RangeInclusive<f32>;
54
55use flow_fcs::TransformType;
56use std::ops::Range;
57
58pub 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 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 *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 *plot_range_y.start()..*plot_range_y.end()
103 }
104 };
105
106 Ok((x_spec.into(), y_spec.into()))
107}
108
109pub 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 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 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); }
142
143 let range = max - min;
144 if range == 0.0 {
145 return (min - 0.5, min + 0.5); }
147
148 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 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 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 if value.is_sign_negative() {
201 -nice_value
202 } else {
203 nice_value
204 }
205}