use crate::scale::Ticks;
#[derive(Debug, Clone, PartialEq)]
pub struct Bins {
start: f64,
width: f64,
counts: Vec<u64>,
}
impl Bins {
pub fn new(start: f64, width: f64, bins: usize) -> Bins {
assert!(
start.is_finite() && width.is_finite() && width > 0.0 && bins > 0,
"Bins::new requires a finite start, positive width, and at least one bin"
);
Bins {
start,
width,
counts: vec![0; bins],
}
}
pub fn auto(values: &[f64], limit: usize) -> Option<Bins> {
let mut finite: Vec<f64> = values.iter().copied().filter(|v| v.is_finite()).collect();
if finite.is_empty() {
return None;
}
let n = finite.len();
let (min, max) = finite
.iter()
.fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), &v| {
(lo.min(v), hi.max(v))
});
if min == max {
let mut bins = Bins::new(min - 0.5, 1.0, 1);
bins.counts[0] = n as u64;
return Some(bins);
}
let sturges = (n as f64).log2().ceil() as usize + 1;
let quarter = n / 4;
let (_, q1, _) = finite.select_nth_unstable_by(quarter, f64::total_cmp);
let q1 = *q1;
let upper = (3 * n) / 4;
let (_, q3, _) = finite.select_nth_unstable_by(upper.min(n - 1), f64::total_cmp);
let q3 = *q3;
let iqr = q3 - q1;
let fd = if iqr > 0.0 {
let width = 2.0 * iqr / (n as f64).cbrt();
((max - min) / width).ceil() as usize
} else {
0
};
let target = sturges.max(fd).clamp(1, limit.max(1));
let ticks = Ticks::linear(min, max, target.min(50));
let width = if ticks.step() > 0.0 {
ticks.step()
} else {
(max - min) / target as f64
};
let start = (min / width).floor() * width;
let bins = (((max - start) / width).ceil() as usize).max(1);
let mut result = Bins::new(start, width, bins.min(limit.max(1) * 2));
for &value in &finite {
result.add(value);
}
Some(result)
}
pub fn add(&mut self, value: f64) {
if !value.is_finite() {
return;
}
let position = (value - self.start) / self.width;
if position < 0.0 {
return;
}
let index = position as usize;
if index < self.counts.len() {
self.counts[index] += 1;
} else if index == self.counts.len() && value <= self.end() {
*self.counts.last_mut().expect("at least one bin") += 1;
}
}
pub fn merge(&mut self, other: &Bins) {
assert!(
self.start == other.start
&& self.width == other.width
&& self.counts.len() == other.counts.len(),
"Bins::merge requires identical geometry"
);
for (mine, theirs) in self.counts.iter_mut().zip(other.counts.iter()) {
*mine += theirs;
}
}
pub fn start(&self) -> f64 {
self.start
}
pub fn width(&self) -> f64 {
self.width
}
pub fn end(&self) -> f64 {
self.start + self.width * self.counts.len() as f64
}
pub fn counts(&self) -> &[u64] {
&self.counts
}
}
#[cfg(test)]
#[path = "tests/bin_tests.rs"]
mod tests;