pub mod colormap;
pub mod contour;
pub mod density_calc;
pub mod helpers;
pub mod histogram_data;
pub mod options;
pub mod plots;
pub mod scatter_data;
pub mod render;
pub mod signal_heatmap;
pub use colormap::ColorMaps;
pub use histogram_data::{HistogramData, HistogramDataError, HistogramSeries};
pub use options::{
AxisOptions, BasePlotOptions, DensityPlotOptions, HistogramPlotOptions, PlotOptions,
SpectralSignaturePlotOptions,
};
pub use plots::{DensityPlot, HistogramPlot, Plot, PlotType, SpectralSignaturePlot};
pub use scatter_data::{ScatterDataError, ScatterPlotData};
pub use render::{ProgressCallback, ProgressInfo, RenderConfig};
pub use signal_heatmap::{generate_normalized_spectral_signature_plot, generate_signal_heatmap};
pub type PlotBytes = Vec<u8>;
pub type PlotRange = std::ops::RangeInclusive<f32>;
use flow_fcs::TransformType;
use std::ops::Range;
pub fn create_axis_specs(
plot_range_x: &PlotRange,
plot_range_y: &PlotRange,
x_transform: &TransformType,
y_transform: &TransformType,
) -> anyhow::Result<(Range<f32>, Range<f32>)> {
let x_spec = match x_transform {
TransformType::Linear => {
let min = plot_range_x.start();
let max = plot_range_x.end();
let (nice_min, nice_max) = nice_bounds(*min, *max);
nice_min..nice_max
}
TransformType::Arcsinh { cofactor: _ } | TransformType::Biexponential { .. } => {
*plot_range_x.start()..*plot_range_x.end()
}
};
let y_spec = match y_transform {
TransformType::Linear => {
let min = plot_range_y.start();
let max = plot_range_y.end();
let (nice_min, nice_max) = nice_bounds(*min, *max);
nice_min..nice_max
}
TransformType::Arcsinh { cofactor: _ } | TransformType::Biexponential { .. } => {
*plot_range_y.start()..*plot_range_y.end()
}
};
Ok((x_spec.into(), y_spec.into()))
}
pub fn get_percentile_bounds(
values: &[f32],
percentile_low: f32,
percentile_high: f32,
) -> PlotRange {
let mut sorted_values = values.to_vec();
sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let low_index = (percentile_low * sorted_values.len() as f32).floor() as usize;
let high_index = (percentile_high * sorted_values.len() as f32).ceil() as usize;
let low_index = low_index.clamp(0, sorted_values.len() - 1);
let high_index = high_index.clamp(0, sorted_values.len() - 1);
let low_value = sorted_values[low_index];
let high_value = sorted_values[high_index];
let min_bound = nearest_nice_number(low_value, RoundingDirection::Down);
let max_bound = nearest_nice_number(high_value, RoundingDirection::Up);
min_bound..=max_bound
}
fn nice_bounds(min: f32, max: f32) -> (f32, f32) {
if min.is_infinite() || max.is_infinite() || min.is_nan() || max.is_nan() {
return (0.0, 1.0); }
let range = max - min;
if range == 0.0 {
return (min - 0.5, min + 0.5); }
let step_size = 10_f32.powf((range.log10()).floor());
let nice_min = (min / step_size).floor() * step_size;
let nice_max = (max / step_size).ceil() * step_size;
(nice_min, nice_max)
}
enum RoundingDirection {
Up,
Down,
}
fn nearest_nice_number(value: f32, direction: RoundingDirection) -> f32 {
if value == 0.0 {
return 0.0;
}
let abs_value = value.abs();
let exponent = abs_value.log10().floor() as i32;
let factor = 10f32.powi(exponent);
let nice_value = match direction {
RoundingDirection::Up => {
let mantissa = (abs_value / factor).ceil();
if mantissa <= 1.0 {
1.0 * factor
} else if mantissa <= 2.0 {
2.0 * factor
} else if mantissa <= 5.0 {
5.0 * factor
} else {
10.0 * factor
}
}
RoundingDirection::Down => {
let mantissa = (abs_value / factor).floor();
if mantissa >= 5.0 {
5.0 * factor
} else if mantissa >= 2.0 {
2.0 * factor
} else if mantissa >= 1.0 {
1.0 * factor
} else {
0.5 * factor
}
}
};
if value.is_sign_negative() {
-nice_value
} else {
nice_value
}
}