Skip to main content

flow_plots/
histogram_data.rs

1//! Histogram data types supporting raw values, pre-binned data, and overlaid series.
2//!
3//! # Examples
4//!
5//! Single histogram from raw values:
6//! ```ignore
7//! let data = HistogramData::from_values(vec![1.0, 2.0, 2.5, 3.0, 3.0]);
8//! ```
9//!
10//! Pre-binned histogram:
11//! ```ignore
12//! let data = HistogramData::pre_binned(
13//!     vec![0.0, 1.0, 2.0, 3.0, 4.0],  // bin edges
14//!     vec![5.0, 10.0, 7.0, 3.0],     // counts per bin
15//! );
16//! ```
17//!
18//! Overlaid histograms (multiple series with gate IDs):
19//! ```ignore
20//! let data = HistogramData::overlaid(vec![
21//!     (vec![1.0, 2.0, 2.0], 0),  // gate 0
22//!     (vec![2.5, 3.0, 3.0], 1),  // gate 1
23//! ]);
24//! ```
25
26/// Histogram data - either raw values, pre-binned, or overlaid series.
27#[derive(Clone, Debug)]
28pub enum HistogramData {
29    /// Raw values to bin; binning is done at render time
30    RawValues(Vec<f32>),
31    /// Pre-binned: bin_edges has length N+1, counts has length N (one per bin)
32    PreBinned {
33        bin_edges: Vec<f32>,
34        counts: Vec<f32>,
35    },
36    /// Multiple series for overlaid histograms; each (values, gate_id) is one series
37    Overlaid(Vec<HistogramSeries>),
38}
39
40/// A single histogram series (values + gate ID for coloring)
41#[derive(Clone, Debug)]
42pub struct HistogramSeries {
43    /// Values to bin, or use pre-binned via HistogramData::PreBinned for single series
44    pub values: Vec<f32>,
45    /// Gate ID indexes into gate_colors in options
46    pub gate_id: u32,
47}
48
49impl HistogramData {
50    /// Create from raw values (will be binned at render time)
51    pub fn from_values(values: Vec<f32>) -> Self {
52        Self::RawValues(values)
53    }
54
55    /// Create from pre-binned data. bin_edges.len() must equal counts.len() + 1.
56    ///
57    /// # Errors
58    /// Returns `Err` if lengths are inconsistent.
59    pub fn pre_binned(
60        bin_edges: Vec<f32>,
61        counts: Vec<f32>,
62    ) -> Result<Self, HistogramDataError> {
63        if bin_edges.len() != counts.len().saturating_add(1) {
64            return Err(HistogramDataError::BinCountMismatch {
65                edges_len: bin_edges.len(),
66                counts_len: counts.len(),
67            });
68        }
69        if bin_edges.len() < 2 {
70            return Err(HistogramDataError::TooFewBins);
71        }
72        Ok(Self::PreBinned { bin_edges, counts })
73    }
74
75    /// Create overlaid histogram data (multiple series, each with gate_id for color)
76    pub fn overlaid(series: Vec<(Vec<f32>, u32)>) -> Self {
77        Self::Overlaid(
78            series
79                .into_iter()
80                .map(|(values, gate_id)| HistogramSeries { values, gate_id })
81                .collect(),
82        )
83    }
84
85    /// Whether this is overlaid (multiple series)
86    pub fn is_overlaid(&self) -> bool {
87        matches!(self, Self::Overlaid(_))
88    }
89}
90
91/// Error when constructing histogram data
92#[derive(Debug, Clone)]
93pub enum HistogramDataError {
94    BinCountMismatch { edges_len: usize, counts_len: usize },
95    TooFewBins,
96}
97
98impl std::fmt::Display for HistogramDataError {
99    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100        match self {
101            Self::BinCountMismatch { edges_len, counts_len } => {
102                write!(
103                    f,
104                    "bin_edges.len() ({}) must equal counts.len() + 1 (got {})",
105                    edges_len, counts_len
106                )
107            }
108            Self::TooFewBins => write!(f, "need at least 2 bin edges"),
109        }
110    }
111}
112
113impl std::error::Error for HistogramDataError {}
114
115/// Result of binning: (bin_centers, counts) for drawing
116#[derive(Clone, Debug)]
117pub struct BinnedHistogram {
118    pub bin_centers: Vec<f64>,
119    pub counts: Vec<f64>,
120}
121
122/// Bin raw values into histogram counts.
123///
124/// Uses linear spacing from data min to max. Values outside [x_min, x_max] are ignored.
125pub fn bin_values(
126    values: &[f32],
127    num_bins: usize,
128    x_min: f32,
129    x_max: f32,
130) -> Option<BinnedHistogram> {
131    if values.is_empty() || num_bins == 0 || x_max <= x_min {
132        return None;
133    }
134
135    let mut counts = vec![0.0f64; num_bins];
136    let bin_width = (x_max - x_min) as f64 / num_bins as f64;
137    if bin_width <= 0.0 {
138        return None;
139    }
140
141    for &v in values {
142        if v.is_nan() || v.is_infinite() {
143            continue;
144        }
145        if v < x_min || v > x_max {
146            continue;
147        }
148        let idx = ((v - x_min) as f64 / bin_width).floor() as usize;
149        let idx = idx.min(num_bins.saturating_sub(1));
150        counts[idx] += 1.0;
151    }
152
153    let bin_centers: Vec<f64> = (0..num_bins)
154        .map(|i| {
155            x_min as f64 + (i as f64 + 0.5) * bin_width
156        })
157        .collect();
158
159    Some(BinnedHistogram {
160        bin_centers,
161        counts,
162    })
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn test_histogram_data_from_values() {
171        let data = HistogramData::from_values(vec![1.0, 2.0, 3.0]);
172        assert!(!data.is_overlaid());
173    }
174
175    #[test]
176    fn test_histogram_data_pre_binned_ok() {
177        let data = HistogramData::pre_binned(
178            vec![0.0, 1.0, 2.0, 3.0],
179            vec![5.0, 10.0, 7.0],
180        )
181        .unwrap();
182        assert!(!data.is_overlaid());
183    }
184
185    #[test]
186    fn test_histogram_data_pre_binned_mismatch() {
187        let result = HistogramData::pre_binned(vec![0.0, 1.0], vec![5.0, 10.0]);
188        assert!(result.is_err());
189    }
190
191    #[test]
192    fn test_histogram_data_overlaid() {
193        let data = HistogramData::overlaid(vec![
194            (vec![1.0, 2.0], 0),
195            (vec![2.0, 3.0], 1),
196        ]);
197        assert!(data.is_overlaid());
198    }
199
200    #[test]
201    fn test_bin_values() {
202        let values = vec![0.0f32, 1.0, 1.0, 2.0, 2.0, 2.0, 3.0];
203        let binned = bin_values(&values, 3, 0.0, 3.0).unwrap();
204        assert_eq!(binned.bin_centers.len(), 3);
205        assert_eq!(binned.counts.len(), 3);
206        // Bin 0: [0, 1) -> 0, 1, 1 = 3 values... actually 0 and 1.0
207        // 0.0 in [0,1), 1.0 at boundary - (1.0-0)/1 = 1.0, floor=1, min(1,2)=1, so idx 1
208        // So 0.0 -> idx 0, 1.0 -> idx 1, 2.0 -> idx 2, 3.0 -> idx 2 (since 3/1=3, floor 3, min 2)
209        // Let me recalc: bin_width = 1.0
210        // v=0: (0-0)/1=0, floor=0 -> idx 0
211        // v=1: (1-0)/1=1, floor=1 -> idx 1 (but 1 is right edge of bin 0)
212        // v=2: idx 2
213        // v=3: (3-0)/1=3, floor=3, min(3,2)=2 -> idx 2
214        // So counts: bin0 has 0.0 (1 val), bin1 has 1.0, 1.0 (2 vals), bin2 has 2,2,2,3 (4 vals)
215        assert!(binned.counts[0] >= 1.0);
216        assert!(binned.counts[1] >= 1.0);
217        assert!(binned.counts[2] >= 1.0);
218    }
219
220    #[test]
221    fn test_bin_values_empty() {
222        assert!(bin_values(&[], 10, 0.0, 100.0).is_none());
223    }
224}