1use crate::core::{BoundingBox, DrawCall, Material, PipelineType, RenderData, Vertex};
6use glam::{Vec3, Vec4};
7
8#[derive(Debug, Clone)]
10pub struct Histogram {
11 pub data: Vec<f64>,
13
14 pub bins: usize,
16 pub bin_edges: Vec<f64>,
17 pub bin_counts: Vec<u64>,
18
19 pub color: Vec4,
21 pub outline_color: Option<Vec4>,
22 pub outline_width: f32,
23 pub normalize: bool,
24
25 pub label: Option<String>,
27 pub visible: bool,
28
29 vertices: Option<Vec<Vertex>>,
31 indices: Option<Vec<u32>>,
32 bounds: Option<BoundingBox>,
33 dirty: bool,
34}
35
36impl Histogram {
37 pub fn new(data: Vec<f64>, bins: usize) -> Result<Self, String> {
39 if data.is_empty() {
40 return Err("Cannot create histogram with empty data".to_string());
41 }
42
43 if bins == 0 {
44 return Err("Number of bins must be greater than zero".to_string());
45 }
46
47 let mut histogram = Self {
48 data,
49 bins,
50 bin_edges: Vec::new(),
51 bin_counts: Vec::new(),
52 color: Vec4::new(0.0, 0.5, 1.0, 1.0), outline_color: Some(Vec4::new(0.0, 0.0, 0.0, 1.0)), outline_width: 1.0,
55 normalize: false,
56 label: None,
57 visible: true,
58 vertices: None,
59 indices: None,
60 bounds: None,
61 dirty: true,
62 };
63
64 histogram.compute_histogram();
65 Ok(histogram)
66 }
67
68 pub fn with_bin_edges(data: Vec<f64>, bin_edges: Vec<f64>) -> Result<Self, String> {
70 if data.is_empty() {
71 return Err("Cannot create histogram with empty data".to_string());
72 }
73
74 if bin_edges.len() < 2 {
75 return Err("Must have at least 2 bin edges".to_string());
76 }
77
78 for i in 1..bin_edges.len() {
80 if bin_edges[i] <= bin_edges[i - 1] {
81 return Err("Bin edges must be strictly increasing".to_string());
82 }
83 }
84
85 let bins = bin_edges.len() - 1;
86 let mut histogram = Self {
87 data,
88 bins,
89 bin_edges,
90 bin_counts: Vec::new(),
91 color: Vec4::new(0.0, 0.5, 1.0, 1.0),
92 outline_color: Some(Vec4::new(0.0, 0.0, 0.0, 1.0)),
93 outline_width: 1.0,
94 normalize: false,
95 label: None,
96 visible: true,
97 vertices: None,
98 indices: None,
99 bounds: None,
100 dirty: true,
101 };
102
103 histogram.compute_histogram();
104 Ok(histogram)
105 }
106
107 pub fn with_style(mut self, color: Vec4, normalize: bool) -> Self {
109 self.color = color;
110 self.normalize = normalize;
111 self.dirty = true;
112 self
113 }
114
115 pub fn with_outline(mut self, outline_color: Vec4, outline_width: f32) -> Self {
117 self.outline_color = Some(outline_color);
118 self.outline_width = outline_width.max(0.1);
119 self.dirty = true;
120 self
121 }
122
123 pub fn without_outline(mut self) -> Self {
125 self.outline_color = None;
126 self.dirty = true;
127 self
128 }
129
130 pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
132 self.label = Some(label.into());
133 self
134 }
135
136 pub fn update_data(&mut self, data: Vec<f64>) -> Result<(), String> {
138 if data.is_empty() {
139 return Err("Cannot update with empty data".to_string());
140 }
141
142 self.data = data;
143 self.compute_histogram();
144 self.dirty = true;
145 Ok(())
146 }
147
148 pub fn set_bins(&mut self, bins: usize) -> Result<(), String> {
150 if bins == 0 {
151 return Err("Number of bins must be greater than zero".to_string());
152 }
153
154 self.bins = bins;
155 self.compute_histogram();
156 self.dirty = true;
157 Ok(())
158 }
159
160 pub fn set_color(&mut self, color: Vec4) {
162 self.color = color;
163 self.dirty = true;
164 }
165
166 pub fn set_normalize(&mut self, normalize: bool) {
168 self.normalize = normalize;
169 self.dirty = true;
170 }
171
172 pub fn set_visible(&mut self, visible: bool) {
174 self.visible = visible;
175 }
176
177 pub fn len(&self) -> usize {
179 self.bins
180 }
181
182 pub fn is_empty(&self) -> bool {
184 self.data.is_empty()
185 }
186
187 fn compute_histogram(&mut self) {
189 if self.data.is_empty() {
190 return;
191 }
192
193 if self.bin_edges.is_empty() {
195 let min_val = self.data.iter().fold(f64::INFINITY, |a, &b| a.min(b));
196 let max_val = self.data.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
197
198 let (min_val, max_val) = if (max_val - min_val).abs() < f64::EPSILON {
200 (min_val - 0.5, max_val + 0.5)
201 } else {
202 (min_val, max_val)
203 };
204
205 let bin_width = (max_val - min_val) / self.bins as f64;
206 self.bin_edges = (0..=self.bins)
207 .map(|i| min_val + i as f64 * bin_width)
208 .collect();
209 }
210
211 self.bin_counts = vec![0; self.bins];
213 for &value in &self.data {
214 let mut bin_index = self.bins; for i in 0..self.bins {
218 if value >= self.bin_edges[i] && value < self.bin_edges[i + 1] {
219 bin_index = i;
220 break;
221 }
222 }
223
224 if bin_index == self.bins && value == self.bin_edges[self.bins] {
226 bin_index = self.bins - 1;
227 }
228
229 if bin_index < self.bins {
231 self.bin_counts[bin_index] += 1;
232 }
233 }
234 }
235
236 fn get_bin_heights(&self) -> Vec<f64> {
238 if self.normalize {
239 let total_count: u64 = self.bin_counts.iter().sum();
240
241 if total_count == 0 {
242 return vec![0.0; self.bin_counts.len()];
243 }
244
245 self.bin_counts
246 .iter()
247 .zip(self.bin_edges.windows(2))
248 .map(|(&count, edges)| {
249 let bin_width = edges[1] - edges[0];
250 count as f64 / (total_count as f64 * bin_width)
251 })
252 .collect()
253 } else {
254 self.bin_counts.iter().map(|&c| c as f64).collect()
255 }
256 }
257
258 pub fn generate_vertices(&mut self) -> (&Vec<Vertex>, &Vec<u32>) {
260 if self.dirty || self.vertices.is_none() {
261 let (vertices, indices) = self.create_histogram_geometry();
262 self.vertices = Some(vertices);
263 self.indices = Some(indices);
264 self.dirty = false;
265 }
266 (
267 self.vertices.as_ref().unwrap(),
268 self.indices.as_ref().unwrap(),
269 )
270 }
271
272 fn create_histogram_geometry(&self) -> (Vec<Vertex>, Vec<u32>) {
274 let mut vertices = Vec::new();
275 let mut indices = Vec::new();
276
277 let heights = self.get_bin_heights();
278
279 for (&height, edges) in heights.iter().zip(self.bin_edges.windows(2)) {
280 let left = edges[0] as f32;
281 let right = edges[1] as f32;
282 let bottom = 0.0;
283 let top = height as f32;
284
285 let base_vertex_index = vertices.len() as u32;
287
288 vertices.push(Vertex::new(Vec3::new(left, bottom, 0.0), self.color)); vertices.push(Vertex::new(Vec3::new(right, bottom, 0.0), self.color)); vertices.push(Vertex::new(Vec3::new(right, top, 0.0), self.color)); vertices.push(Vertex::new(Vec3::new(left, top, 0.0), self.color)); indices.extend_from_slice(&[
296 base_vertex_index,
297 base_vertex_index + 1,
298 base_vertex_index + 2, base_vertex_index,
300 base_vertex_index + 2,
301 base_vertex_index + 3, ]);
303 }
304
305 (vertices, indices)
306 }
307
308 pub fn bounds(&mut self) -> BoundingBox {
310 if self.dirty || self.bounds.is_none() {
311 if self.bin_edges.is_empty() {
312 self.bounds = Some(BoundingBox::default());
313 return self.bounds.unwrap();
314 }
315
316 let min_x = *self.bin_edges.first().unwrap() as f32;
317 let max_x = *self.bin_edges.last().unwrap() as f32;
318
319 let heights = self.get_bin_heights();
320 let max_height = heights.iter().fold(0.0f64, |a, &b| a.max(b)) as f32;
321
322 self.bounds = Some(BoundingBox::new(
323 Vec3::new(min_x, 0.0, 0.0),
324 Vec3::new(max_x, max_height, 0.0),
325 ));
326 }
327 self.bounds.unwrap()
328 }
329
330 pub fn render_data(&mut self) -> RenderData {
332 let (vertices, indices) = self.generate_vertices();
333 let vertices = vertices.clone();
334 let indices = indices.clone();
335
336 let material = Material {
337 albedo: self.color,
338 ..Default::default()
339 };
340
341 let draw_call = DrawCall {
342 vertex_offset: 0,
343 vertex_count: vertices.len(),
344 index_offset: Some(0),
345 index_count: Some(indices.len()),
346 instance_count: 1,
347 };
348
349 RenderData {
350 pipeline_type: PipelineType::Triangles,
351 vertices,
352 indices: Some(indices),
353 material,
354 draw_calls: vec![draw_call],
355 }
356 }
357
358 pub fn statistics(&self) -> HistogramStatistics {
360 let data_range = if self.data.is_empty() {
361 (0.0, 0.0)
362 } else {
363 let min_val = self.data.iter().fold(f64::INFINITY, |a, &b| a.min(b));
364 let max_val = self.data.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
365 (min_val, max_val)
366 };
367
368 let total_count: u64 = self.bin_counts.iter().sum();
369 let max_count = self.bin_counts.iter().max().copied().unwrap_or(0);
370
371 HistogramStatistics {
372 data_count: self.data.len(),
373 bin_count: self.bins,
374 data_range,
375 total_count,
376 max_bin_count: max_count,
377 memory_usage: self.estimated_memory_usage(),
378 }
379 }
380
381 pub fn estimated_memory_usage(&self) -> usize {
383 let data_size = self.data.len() * std::mem::size_of::<f64>();
384 let edges_size = self.bin_edges.len() * std::mem::size_of::<f64>();
385 let counts_size = self.bin_counts.len() * std::mem::size_of::<u64>();
386 let vertices_size = self
387 .vertices
388 .as_ref()
389 .map_or(0, |v| v.len() * std::mem::size_of::<Vertex>());
390 let indices_size = self
391 .indices
392 .as_ref()
393 .map_or(0, |i| i.len() * std::mem::size_of::<u32>());
394
395 data_size + edges_size + counts_size + vertices_size + indices_size
396 }
397}
398
399#[derive(Debug, Clone)]
401pub struct HistogramStatistics {
402 pub data_count: usize,
403 pub bin_count: usize,
404 pub data_range: (f64, f64),
405 pub total_count: u64,
406 pub max_bin_count: u64,
407 pub memory_usage: usize,
408}
409
410pub mod matlab_compat {
412 use super::*;
413
414 pub fn hist(data: Vec<f64>, bins: usize) -> Result<Histogram, String> {
416 Histogram::new(data, bins)
417 }
418
419 pub fn hist_with_edges(data: Vec<f64>, edges: Vec<f64>) -> Result<Histogram, String> {
421 Histogram::with_bin_edges(data, edges)
422 }
423
424 pub fn histogram_normalized(data: Vec<f64>, bins: usize) -> Result<Histogram, String> {
426 Ok(Histogram::new(data, bins)?.with_style(
427 Vec4::new(0.0, 0.5, 1.0, 1.0),
428 true, ))
430 }
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 #[test]
438 fn test_histogram_creation() {
439 let data = vec![1.0, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0];
440 let hist = Histogram::new(data.clone(), 5).unwrap();
441
442 assert_eq!(hist.data, data);
443 assert_eq!(hist.bins, 5);
444 assert_eq!(hist.bin_edges.len(), 6); assert_eq!(hist.bin_counts.len(), 5);
446 assert!(!hist.is_empty());
447 }
448
449 #[test]
450 fn test_histogram_validation() {
451 assert!(Histogram::new(vec![], 5).is_err());
453
454 assert!(Histogram::new(vec![1.0, 2.0], 0).is_err());
456
457 assert!(Histogram::with_bin_edges(vec![1.0, 2.0], vec![1.0]).is_err()); assert!(Histogram::with_bin_edges(vec![1.0, 2.0], vec![2.0, 1.0]).is_err());
460 }
462
463 #[test]
464 fn test_histogram_computation() {
465 let data = vec![1.0, 1.5, 2.0, 2.5, 3.0];
466 let hist = Histogram::new(data, 3).unwrap();
467
468 assert!(hist.bin_edges[0] <= 1.0);
470 assert!(hist.bin_edges.last().unwrap() >= &3.0);
471
472 let total_count: u64 = hist.bin_counts.iter().sum();
474 assert_eq!(total_count, 5);
475 }
476
477 #[test]
478 fn test_histogram_custom_edges() {
479 let data = vec![0.5, 1.5, 2.5, 3.5];
480 let edges = vec![0.0, 1.0, 2.0, 3.0, 4.0];
481 let hist = Histogram::with_bin_edges(data, edges.clone()).unwrap();
482
483 assert_eq!(hist.bin_edges, edges);
484 assert_eq!(hist.bins, 4);
485
486 assert_eq!(hist.bin_counts, vec![1, 1, 1, 1]);
488 }
489
490 #[test]
491 fn test_histogram_normalization() {
492 let data = vec![1.0, 1.0, 2.0, 2.0, 3.0, 3.0];
493 let hist = Histogram::new(data, 3).unwrap().with_style(Vec4::ONE, true);
494
495 let heights = hist.get_bin_heights();
496
497 let total_area: f64 = heights
499 .iter()
500 .zip(hist.bin_edges.windows(2))
501 .map(|(&height, edges)| height * (edges[1] - edges[0]))
502 .sum();
503
504 assert!((total_area - 1.0).abs() < 1e-10);
505 }
506
507 #[test]
508 fn test_histogram_bounds() {
509 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
510 let mut hist = Histogram::new(data, 4).unwrap();
511 let bounds = hist.bounds();
512
513 assert!(bounds.min.x <= 1.0);
515 assert!(bounds.max.x >= 5.0);
516
517 assert_eq!(bounds.min.y, 0.0);
519 assert!(bounds.max.y > 0.0);
520 }
521
522 #[test]
523 fn test_histogram_vertex_generation() {
524 let data = vec![1.0, 2.0];
525 let mut hist = Histogram::new(data, 2).unwrap();
526 let (vertices, indices) = hist.generate_vertices();
527
528 assert_eq!(vertices.len(), 8);
530
531 assert_eq!(indices.len(), 12);
533 }
534
535 #[test]
536 fn test_histogram_render_data() {
537 let data = vec![1.0, 1.5, 2.0];
538 let mut hist = Histogram::new(data, 2).unwrap();
539 let render_data = hist.render_data();
540
541 assert_eq!(render_data.pipeline_type, PipelineType::Triangles);
542 assert!(!render_data.vertices.is_empty());
543 assert!(render_data.indices.is_some());
544 }
545
546 #[test]
547 fn test_histogram_statistics() {
548 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
549 let hist = Histogram::new(data, 3).unwrap();
550 let stats = hist.statistics();
551
552 assert_eq!(stats.data_count, 5);
553 assert_eq!(stats.bin_count, 3);
554 assert_eq!(stats.data_range, (1.0, 5.0));
555 assert_eq!(stats.total_count, 5);
556 assert!(stats.memory_usage > 0);
557 }
558
559 #[test]
560 fn test_matlab_compat_hist() {
561 use super::matlab_compat::*;
562
563 let data = vec![1.0, 2.0, 3.0];
564
565 let hist1 = hist(data.clone(), 2).unwrap();
566 assert_eq!(hist1.len(), 2);
567
568 let edges = vec![0.0, 1.5, 3.5];
569 let hist2 = hist_with_edges(data.clone(), edges).unwrap();
570 assert_eq!(hist2.bins, 2);
571
572 let hist3 = histogram_normalized(data, 3).unwrap();
573 assert!(hist3.normalize);
574 }
575}