runmat_plot/plots/
histogram.rs

1//! Histogram implementation
2//!
3//! High-performance histograms with GPU acceleration.
4
5use crate::core::{BoundingBox, DrawCall, Material, PipelineType, RenderData, Vertex};
6use glam::{Vec3, Vec4};
7
8/// High-performance GPU-accelerated histogram
9#[derive(Debug, Clone)]
10pub struct Histogram {
11    /// Raw data values
12    pub data: Vec<f64>,
13
14    /// Histogram configuration
15    pub bins: usize,
16    pub bin_edges: Vec<f64>,
17    pub bin_counts: Vec<u64>,
18
19    /// Visual styling
20    pub color: Vec4,
21    pub outline_color: Option<Vec4>,
22    pub outline_width: f32,
23    pub normalize: bool,
24
25    /// Metadata
26    pub label: Option<String>,
27    pub visible: bool,
28
29    /// Generated rendering data (cached)
30    vertices: Option<Vec<Vertex>>,
31    indices: Option<Vec<u32>>,
32    bounds: Option<BoundingBox>,
33    dirty: bool,
34}
35
36impl Histogram {
37    /// Create a new histogram with data and number of bins
38    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), // Default blue
53            outline_color: Some(Vec4::new(0.0, 0.0, 0.0, 1.0)), // Default black outline
54            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    /// Create a histogram with custom bin edges
69    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        // Verify bin edges are sorted
79        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    /// Set styling options
108    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    /// Add outline to bars
116    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    /// Remove outline
124    pub fn without_outline(mut self) -> Self {
125        self.outline_color = None;
126        self.dirty = true;
127        self
128    }
129
130    /// Set the histogram label for legends
131    pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
132        self.label = Some(label.into());
133        self
134    }
135
136    /// Update the data and recompute histogram
137    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    /// Set the number of bins and recompute
149    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    /// Set the histogram color
161    pub fn set_color(&mut self, color: Vec4) {
162        self.color = color;
163        self.dirty = true;
164    }
165
166    /// Enable or disable normalization
167    pub fn set_normalize(&mut self, normalize: bool) {
168        self.normalize = normalize;
169        self.dirty = true;
170    }
171
172    /// Show or hide the histogram
173    pub fn set_visible(&mut self, visible: bool) {
174        self.visible = visible;
175    }
176
177    /// Get the number of bins
178    pub fn len(&self) -> usize {
179        self.bins
180    }
181
182    /// Check if the histogram has no data
183    pub fn is_empty(&self) -> bool {
184        self.data.is_empty()
185    }
186
187    /// Compute the histogram from the data
188    fn compute_histogram(&mut self) {
189        if self.data.is_empty() {
190            return;
191        }
192
193        // Generate bin edges if not provided
194        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            // Handle case where all values are the same
199            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        // Count values in each bin
212        self.bin_counts = vec![0; self.bins];
213        for &value in &self.data {
214            // Find which bin this value belongs to
215            let mut bin_index = self.bins; // Default to overflow
216
217            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            // Handle the last bin edge (inclusive)
225            if bin_index == self.bins && value == self.bin_edges[self.bins] {
226                bin_index = self.bins - 1;
227            }
228
229            // Count the value if it's within bounds
230            if bin_index < self.bins {
231                self.bin_counts[bin_index] += 1;
232            }
233        }
234    }
235
236    /// Get bin heights (counts or normalized densities)
237    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    /// Generate vertices for GPU rendering
259    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    /// Create the geometry for the histogram bars
273    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            // Create vertices for this bar (rectangle)
286            let base_vertex_index = vertices.len() as u32;
287
288            // Four corners of the rectangle
289            vertices.push(Vertex::new(Vec3::new(left, bottom, 0.0), self.color)); // Bottom-left
290            vertices.push(Vertex::new(Vec3::new(right, bottom, 0.0), self.color)); // Bottom-right
291            vertices.push(Vertex::new(Vec3::new(right, top, 0.0), self.color)); // Top-right
292            vertices.push(Vertex::new(Vec3::new(left, top, 0.0), self.color)); // Top-left
293
294            // Two triangles to form the rectangle
295            indices.extend_from_slice(&[
296                base_vertex_index,
297                base_vertex_index + 1,
298                base_vertex_index + 2, // Bottom-right triangle
299                base_vertex_index,
300                base_vertex_index + 2,
301                base_vertex_index + 3, // Top-left triangle
302            ]);
303        }
304
305        (vertices, indices)
306    }
307
308    /// Get the bounding box of the histogram
309    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    /// Generate complete render data for the graphics pipeline
331    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    /// Get histogram statistics
359    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    /// Estimate memory usage in bytes
382    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/// Histogram statistics
400#[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
410/// MATLAB-compatible histogram creation utilities
411pub mod matlab_compat {
412    use super::*;
413
414    /// Create a histogram (equivalent to MATLAB's `hist(data, bins)`)
415    pub fn hist(data: Vec<f64>, bins: usize) -> Result<Histogram, String> {
416        Histogram::new(data, bins)
417    }
418
419    /// Create a histogram with custom bin edges (`hist(data, edges)`)
420    pub fn hist_with_edges(data: Vec<f64>, edges: Vec<f64>) -> Result<Histogram, String> {
421        Histogram::with_bin_edges(data, edges)
422    }
423
424    /// Create a normalized histogram (density)
425    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, // normalize
429        ))
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); // n+1 edges for n bins
445        assert_eq!(hist.bin_counts.len(), 5);
446        assert!(!hist.is_empty());
447    }
448
449    #[test]
450    fn test_histogram_validation() {
451        // Empty data should fail
452        assert!(Histogram::new(vec![], 5).is_err());
453
454        // Zero bins should fail
455        assert!(Histogram::new(vec![1.0, 2.0], 0).is_err());
456
457        // Invalid bin edges should fail
458        assert!(Histogram::with_bin_edges(vec![1.0, 2.0], vec![1.0]).is_err()); // Too few edges
459        assert!(Histogram::with_bin_edges(vec![1.0, 2.0], vec![2.0, 1.0]).is_err());
460        // Not sorted
461    }
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        // Should have created appropriate bin edges
469        assert!(hist.bin_edges[0] <= 1.0);
470        assert!(hist.bin_edges.last().unwrap() >= &3.0);
471
472        // Should have counted all data points
473        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        // Each bin should have exactly one value
487        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        // For normalized histogram, the sum of (height * width) should equal 1
498        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        // X bounds should span all bin edges
514        assert!(bounds.min.x <= 1.0);
515        assert!(bounds.max.x >= 5.0);
516
517        // Y should start at 0 and go to max count
518        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        // Should have 4 vertices per bin (rectangle)
529        assert_eq!(vertices.len(), 8);
530
531        // Should have 6 indices per bin (2 triangles)
532        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}