Skip to main content

pineal_bars/
histogram.rs

1//! Bineado de una muestra en conteos, para alimentar [`paint_bars`] como
2//! histograma. Zero-boxing: la entrada es un `&[f32]` plano (P1 del SDD).
3
4use crate::paint::Bar;
5use pineal_render::Color;
6
7/// Conteos por bin de una muestra sobre `[lo, hi]`.
8#[derive(Debug, Clone)]
9pub struct Histogram {
10    /// Conteo por bin (longitud = nº de bins).
11    pub counts: Vec<f64>,
12    pub lo: f32,
13    pub hi: f32,
14    /// Ancho de cada bin en unidades de dato.
15    pub bin_width: f32,
16}
17
18impl Histogram {
19    /// Binea `values` en `n_bins` sobre su propio min..max. Valores
20    /// fuera de rango (NaN) se ignoran. Con `n_bins == 0` o muestra
21    /// vacía devuelve un histograma vacío.
22    pub fn new(values: &[f32], n_bins: usize) -> Self {
23        if n_bins == 0 || values.is_empty() {
24            return Self { counts: Vec::new(), lo: 0.0, hi: 0.0, bin_width: 0.0 };
25        }
26        let mut lo = f32::INFINITY;
27        let mut hi = f32::NEG_INFINITY;
28        for &v in values {
29            if v.is_nan() {
30                continue;
31            }
32            lo = lo.min(v);
33            hi = hi.max(v);
34        }
35        if !lo.is_finite() || !hi.is_finite() {
36            return Self { counts: vec![0.0; n_bins], lo: 0.0, hi: 0.0, bin_width: 0.0 };
37        }
38        // Rango degenerado (todos iguales): ensancha para no dividir por 0.
39        if (hi - lo).abs() < f32::EPSILON {
40            hi = lo + 1.0;
41        }
42        let bin_width = (hi - lo) / n_bins as f32;
43        let mut counts = vec![0.0_f64; n_bins];
44        for &v in values {
45            if v.is_nan() {
46                continue;
47            }
48            let mut idx = ((v - lo) / bin_width) as usize;
49            if idx >= n_bins {
50                idx = n_bins - 1; // el máximo cae en el último bin
51            }
52            counts[idx] += 1.0;
53        }
54        Self { counts, lo, hi, bin_width }
55    }
56
57    /// Convierte los conteos en barras de un mismo color, listas para
58    /// [`crate::paint_bars`].
59    pub fn to_bars(&self, color: Color) -> Vec<Bar> {
60        self.counts.iter().map(|&c| Bar::new(c, color)).collect()
61    }
62
63    /// Conteo del bin más poblado (útil para fijar el rango del eje).
64    pub fn max_count(&self) -> f64 {
65        self.counts.iter().copied().fold(0.0, f64::max)
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn counts_sum_to_sample_size() {
75        let v = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0];
76        let h = Histogram::new(&v, 5);
77        let total: f64 = h.counts.iter().sum();
78        assert_eq!(total, 10.0);
79        assert_eq!(h.counts.len(), 5);
80    }
81
82    #[test]
83    fn max_value_lands_in_last_bin() {
84        let v = [0.0, 10.0];
85        let h = Histogram::new(&v, 4);
86        assert_eq!(h.counts[3], 1.0, "el máximo cae en el último bin");
87        assert_eq!(h.counts[0], 1.0);
88    }
89
90    #[test]
91    fn empty_and_zero_bins_are_safe() {
92        assert!(Histogram::new(&[], 5).counts.iter().all(|&c| c == 0.0) || Histogram::new(&[], 5).counts.is_empty());
93        assert!(Histogram::new(&[1.0, 2.0], 0).counts.is_empty());
94    }
95
96    #[test]
97    fn degenerate_range_does_not_panic() {
98        let v = [3.0, 3.0, 3.0];
99        let h = Histogram::new(&v, 4);
100        assert_eq!(h.counts.iter().sum::<f64>(), 3.0);
101    }
102}