pub fn compute_histogram(
data: &[f64],
range: Option<(f64, f64)>,
log: bool,
) -> Option<(Vec<u64>, Vec<f64>)> {
if data.is_empty() {
return None;
}
let xform = |v: f64| if log { v.log10() } else { v };
let (mut xmin, mut xmax) = match range {
Some((lo, hi)) => (xform(lo), xform(hi)),
None => {
let mut lo = f64::INFINITY;
let mut hi = f64::NEG_INFINITY;
for &v in data {
let t = xform(v);
if t.is_finite() {
lo = lo.min(t);
hi = hi.max(t);
}
}
(lo, hi)
}
};
if !xmin.is_finite() || !xmax.is_finite() {
return None;
}
if xmax < xmin {
std::mem::swap(&mut xmin, &mut xmax);
}
let nbins = ((data.len() as f64).sqrt().floor() as usize).clamp(2, 256);
let span = xmax - xmin;
let mut counts = vec![0u64; nbins];
if span > 0.0 {
for &v in data {
let t = xform(v);
if !t.is_finite() || t < xmin || t > xmax {
continue;
}
let idx = ((((t - xmin) / span) * nbins as f64) as usize).min(nbins - 1);
counts[idx] += 1;
}
} else {
for &v in data {
if xform(v).is_finite() {
counts[0] += 1;
}
}
}
let inv = |e: f64| if log { 10f64.powf(e) } else { e };
let edges: Vec<f64> = (0..=nbins)
.map(|i| inv(xmin + span * (i as f64) / nbins as f64))
.collect();
Some((counts, edges))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compute_histogram_bin_count_follows_silx_sqrt_rule() {
let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
let (counts, edges) = compute_histogram(&data, None, false).expect("histogram");
assert_eq!(counts.len(), 10);
assert_eq!(edges.len(), 11);
assert_eq!(counts.iter().sum::<u64>(), 100);
assert_eq!(edges[0], 0.0);
assert_eq!(*edges.last().unwrap(), 99.0);
let big = vec![0.5; 1_000_000];
let (c, _) = compute_histogram(&big, Some((0.0, 1.0)), false).expect("histogram");
assert_eq!(c.len(), 256);
let (c1, e1) = compute_histogram(&[3.0], Some((0.0, 6.0)), false).expect("histogram");
assert_eq!(c1.len(), 2);
assert_eq!(e1.len(), 3);
}
#[test]
fn compute_histogram_uses_supplied_range_and_counts_in_range() {
let data = vec![-5.0, 0.0, 2.5, 5.0, 5.0, 10.0];
let (counts, edges) = compute_histogram(&data, Some((0.0, 5.0)), false).expect("histogram");
assert_eq!(edges[0], 0.0);
assert_eq!(*edges.last().unwrap(), 5.0);
assert_eq!(counts.iter().sum::<u64>(), 4);
}
#[test]
fn compute_histogram_log_bins_uniformly_in_log_space() {
let data = vec![1.0, 10.0, 100.0, 1000.0];
let (counts, edges) = compute_histogram(&data, None, true).expect("histogram");
assert_eq!(counts.len(), 2);
assert!((edges[0] - 1.0).abs() < 1e-9, "{}", edges[0]);
assert!((edges[1] - 10f64.powf(1.5)).abs() < 1e-6, "{}", edges[1]);
assert!((edges[2] - 1000.0).abs() < 1e-6, "{}", edges[2]);
assert_eq!(counts.iter().sum::<u64>(), 4);
}
#[test]
fn compute_histogram_empty_or_nonfinite_is_none() {
assert!(compute_histogram(&[], None, false).is_none());
assert!(compute_histogram(&[f64::NAN, f64::INFINITY], None, false).is_none());
}
#[test]
fn compute_histogram_degenerate_range_counts_all_in_first_bin() {
let data = vec![7.0; 5];
let (counts, edges) = compute_histogram(&data, None, false).expect("histogram");
assert_eq!(counts[0], 5);
assert_eq!(counts.iter().sum::<u64>(), 5);
assert_eq!(edges[0], 7.0);
assert_eq!(*edges.last().unwrap(), 7.0);
}
}