flow_plots/
histogram_data.rs1#[derive(Clone, Debug)]
28pub enum HistogramData {
29 RawValues(Vec<f32>),
31 PreBinned {
33 bin_edges: Vec<f32>,
34 counts: Vec<f32>,
35 },
36 Overlaid(Vec<HistogramSeries>),
38}
39
40#[derive(Clone, Debug)]
42pub struct HistogramSeries {
43 pub values: Vec<f32>,
45 pub gate_id: u32,
47}
48
49impl HistogramData {
50 pub fn from_values(values: Vec<f32>) -> Self {
52 Self::RawValues(values)
53 }
54
55 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 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 pub fn is_overlaid(&self) -> bool {
87 matches!(self, Self::Overlaid(_))
88 }
89}
90
91#[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#[derive(Clone, Debug)]
117pub struct BinnedHistogram {
118 pub bin_centers: Vec<f64>,
119 pub counts: Vec<f64>,
120}
121
122pub 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 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}