runmat_plot/plots/
bar.rs

1//! Bar chart implementation
2//!
3//! High-performance bar charts with GPU acceleration and MATLAB-compatible styling.
4
5use crate::core::{BoundingBox, DrawCall, Material, PipelineType, RenderData, Vertex};
6use glam::{Vec3, Vec4};
7
8/// High-performance GPU-accelerated bar chart
9#[derive(Debug, Clone)]
10pub struct BarChart {
11    /// Category labels and values
12    pub labels: Vec<String>,
13    pub values: Vec<f64>,
14
15    /// Visual styling
16    pub color: Vec4,
17    pub bar_width: f32,
18    pub outline_color: Option<Vec4>,
19    pub outline_width: f32,
20
21    /// Metadata
22    pub label: Option<String>,
23    pub visible: bool,
24
25    /// Generated rendering data (cached)
26    vertices: Option<Vec<Vertex>>,
27    indices: Option<Vec<u32>>,
28    bounds: Option<BoundingBox>,
29    dirty: bool,
30}
31
32impl BarChart {
33    /// Create a new bar chart with labels and values
34    pub fn new(labels: Vec<String>, values: Vec<f64>) -> Result<Self, String> {
35        if labels.len() != values.len() {
36            return Err(format!(
37                "Data length mismatch: {} labels, {} values",
38                labels.len(),
39                values.len()
40            ));
41        }
42
43        if labels.is_empty() {
44            return Err("Cannot create bar chart with empty data".to_string());
45        }
46
47        Ok(Self {
48            labels,
49            values,
50            color: Vec4::new(0.0, 0.5, 1.0, 1.0), // Default blue
51            bar_width: 0.8,                       // 80% of available space
52            outline_color: None,
53            outline_width: 1.0,
54            label: None,
55            visible: true,
56            vertices: None,
57            indices: None,
58            bounds: None,
59            dirty: true,
60        })
61    }
62
63    /// Create a bar chart with custom styling
64    pub fn with_style(mut self, color: Vec4, bar_width: f32) -> Self {
65        self.color = color;
66        self.bar_width = bar_width.clamp(0.1, 1.0);
67        self.dirty = true;
68        self
69    }
70
71    /// Add outline to bars
72    pub fn with_outline(mut self, outline_color: Vec4, outline_width: f32) -> Self {
73        self.outline_color = Some(outline_color);
74        self.outline_width = outline_width.max(0.1);
75        self.dirty = true;
76        self
77    }
78
79    /// Set the chart label for legends
80    pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
81        self.label = Some(label.into());
82        self
83    }
84
85    /// Update the data
86    pub fn update_data(&mut self, labels: Vec<String>, values: Vec<f64>) -> Result<(), String> {
87        if labels.len() != values.len() {
88            return Err(format!(
89                "Data length mismatch: {} labels, {} values",
90                labels.len(),
91                values.len()
92            ));
93        }
94
95        if labels.is_empty() {
96            return Err("Cannot update with empty data".to_string());
97        }
98
99        self.labels = labels;
100        self.values = values;
101        self.dirty = true;
102        Ok(())
103    }
104
105    /// Set the bar color
106    pub fn set_color(&mut self, color: Vec4) {
107        self.color = color;
108        self.dirty = true;
109    }
110
111    /// Set the bar width (0.1 to 1.0)
112    pub fn set_bar_width(&mut self, width: f32) {
113        self.bar_width = width.clamp(0.1, 1.0);
114        self.dirty = true;
115    }
116
117    /// Show or hide the chart
118    pub fn set_visible(&mut self, visible: bool) {
119        self.visible = visible;
120    }
121
122    /// Get the number of bars
123    pub fn len(&self) -> usize {
124        self.labels.len()
125    }
126
127    /// Check if the chart has no data
128    pub fn is_empty(&self) -> bool {
129        self.labels.is_empty()
130    }
131
132    /// Generate vertices for GPU rendering
133    pub fn generate_vertices(&mut self) -> (&Vec<Vertex>, &Vec<u32>) {
134        if self.dirty || self.vertices.is_none() {
135            let (vertices, indices) = self.create_bar_geometry();
136            self.vertices = Some(vertices);
137            self.indices = Some(indices);
138            self.dirty = false;
139        }
140        (
141            self.vertices.as_ref().unwrap(),
142            self.indices.as_ref().unwrap(),
143        )
144    }
145
146    /// Create the geometry for all bars
147    fn create_bar_geometry(&self) -> (Vec<Vertex>, Vec<u32>) {
148        let mut vertices = Vec::new();
149        let mut indices = Vec::new();
150
151        let _bar_spacing = 1.0; // Space between bar centers
152        let half_width = self.bar_width * 0.5;
153
154        for (i, &value) in self.values.iter().enumerate() {
155            let x_center = i as f32; // Bar position along X axis
156            let left = x_center - half_width;
157            let right = x_center + half_width;
158            let bottom = 0.0; // Baseline
159            let top = value as f32;
160
161            // Create rectangle vertices for this bar (4 vertices per bar)
162            let vertex_offset = vertices.len() as u32;
163
164            // Bottom left
165            vertices.push(Vertex::new(Vec3::new(left, bottom, 0.0), self.color));
166
167            // Bottom right
168            vertices.push(Vertex::new(Vec3::new(right, bottom, 0.0), self.color));
169
170            // Top right
171            vertices.push(Vertex::new(Vec3::new(right, top, 0.0), self.color));
172
173            // Top left
174            vertices.push(Vertex::new(Vec3::new(left, top, 0.0), self.color));
175
176            // Create indices for two triangles per bar (6 indices per bar)
177            // Triangle 1: bottom-left, bottom-right, top-right
178            indices.push(vertex_offset);
179            indices.push(vertex_offset + 1);
180            indices.push(vertex_offset + 2);
181
182            // Triangle 2: bottom-left, top-right, top-left
183            indices.push(vertex_offset);
184            indices.push(vertex_offset + 2);
185            indices.push(vertex_offset + 3);
186        }
187
188        (vertices, indices)
189    }
190
191    /// Get the bounding box of the chart
192    pub fn bounds(&mut self) -> BoundingBox {
193        if self.dirty || self.bounds.is_none() {
194            let num_bars = self.values.len();
195            if num_bars == 0 {
196                self.bounds = Some(BoundingBox::default());
197                return self.bounds.unwrap();
198            }
199
200            let min_x = -self.bar_width * 0.5;
201            let max_x = (num_bars - 1) as f32 + self.bar_width * 0.5;
202
203            let min_y = self.values.iter().fold(0.0f64, |acc, &val| acc.min(val)) as f32;
204            let max_y = self.values.iter().fold(0.0f64, |acc, &val| acc.max(val)) as f32;
205
206            self.bounds = Some(BoundingBox::new(
207                Vec3::new(min_x, min_y, 0.0),
208                Vec3::new(max_x, max_y, 0.0),
209            ));
210        }
211        self.bounds.unwrap()
212    }
213
214    /// Generate complete render data for the graphics pipeline
215    pub fn render_data(&mut self) -> RenderData {
216        let (vertices, indices) = self.generate_vertices();
217        let vertices = vertices.clone();
218        let indices = indices.clone();
219
220        let material = Material {
221            albedo: self.color,
222            ..Default::default()
223        };
224
225        let draw_call = DrawCall {
226            vertex_offset: 0,
227            vertex_count: vertices.len(),
228            index_offset: Some(0),
229            index_count: Some(indices.len()),
230            instance_count: 1,
231        };
232
233        RenderData {
234            pipeline_type: PipelineType::Triangles,
235            vertices,
236            indices: Some(indices),
237            material,
238            draw_calls: vec![draw_call],
239        }
240    }
241
242    /// Get chart statistics for debugging
243    pub fn statistics(&self) -> BarChartStatistics {
244        let value_range = if self.values.is_empty() {
245            (0.0, 0.0)
246        } else {
247            let min_val = self.values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
248            let max_val = self.values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
249            (min_val, max_val)
250        };
251
252        BarChartStatistics {
253            bar_count: self.values.len(),
254            value_range,
255            memory_usage: self.estimated_memory_usage(),
256        }
257    }
258
259    /// Estimate memory usage in bytes
260    pub fn estimated_memory_usage(&self) -> usize {
261        let labels_size: usize = self.labels.iter().map(|s| s.len()).sum();
262        let values_size = self.values.len() * std::mem::size_of::<f64>();
263        let vertices_size = self
264            .vertices
265            .as_ref()
266            .map_or(0, |v| v.len() * std::mem::size_of::<Vertex>());
267        let indices_size = self
268            .indices
269            .as_ref()
270            .map_or(0, |i| i.len() * std::mem::size_of::<u32>());
271
272        labels_size + values_size + vertices_size + indices_size
273    }
274}
275
276/// Bar chart statistics
277#[derive(Debug, Clone)]
278pub struct BarChartStatistics {
279    pub bar_count: usize,
280    pub value_range: (f64, f64),
281    pub memory_usage: usize,
282}
283
284/// MATLAB-compatible bar chart creation utilities
285pub mod matlab_compat {
286    use super::*;
287
288    /// Create a simple bar chart (equivalent to MATLAB's `bar(values)`)
289    pub fn bar(values: Vec<f64>) -> Result<BarChart, String> {
290        let labels: Vec<String> = (1..=values.len()).map(|i| i.to_string()).collect();
291        BarChart::new(labels, values)
292    }
293
294    /// Create a bar chart with custom labels (`bar(labels, values)`)
295    pub fn bar_with_labels(labels: Vec<String>, values: Vec<f64>) -> Result<BarChart, String> {
296        BarChart::new(labels, values)
297    }
298
299    /// Create a bar chart with specified color
300    pub fn bar_with_color(values: Vec<f64>, color: &str) -> Result<BarChart, String> {
301        let color_vec = parse_matlab_color(color)?;
302        let labels: Vec<String> = (1..=values.len()).map(|i| i.to_string()).collect();
303        Ok(BarChart::new(labels, values)?.with_style(color_vec, 0.8))
304    }
305
306    /// Parse MATLAB color specifications
307    fn parse_matlab_color(color: &str) -> Result<Vec4, String> {
308        match color {
309            "r" | "red" => Ok(Vec4::new(1.0, 0.0, 0.0, 1.0)),
310            "g" | "green" => Ok(Vec4::new(0.0, 1.0, 0.0, 1.0)),
311            "b" | "blue" => Ok(Vec4::new(0.0, 0.0, 1.0, 1.0)),
312            "c" | "cyan" => Ok(Vec4::new(0.0, 1.0, 1.0, 1.0)),
313            "m" | "magenta" => Ok(Vec4::new(1.0, 0.0, 1.0, 1.0)),
314            "y" | "yellow" => Ok(Vec4::new(1.0, 1.0, 0.0, 1.0)),
315            "k" | "black" => Ok(Vec4::new(0.0, 0.0, 0.0, 1.0)),
316            "w" | "white" => Ok(Vec4::new(1.0, 1.0, 1.0, 1.0)),
317            _ => Err(format!("Unknown color: {color}")),
318        }
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn test_bar_chart_creation() {
328        let labels = vec!["A".to_string(), "B".to_string(), "C".to_string()];
329        let values = vec![10.0, 25.0, 15.0];
330
331        let chart = BarChart::new(labels.clone(), values.clone()).unwrap();
332
333        assert_eq!(chart.labels, labels);
334        assert_eq!(chart.values, values);
335        assert_eq!(chart.len(), 3);
336        assert!(!chart.is_empty());
337        assert!(chart.visible);
338    }
339
340    #[test]
341    fn test_bar_chart_data_validation() {
342        // Mismatched lengths should fail
343        let labels = vec!["A".to_string(), "B".to_string()];
344        let values = vec![10.0, 25.0, 15.0];
345        assert!(BarChart::new(labels, values).is_err());
346
347        // Empty data should fail
348        let empty_labels: Vec<String> = vec![];
349        let empty_values: Vec<f64> = vec![];
350        assert!(BarChart::new(empty_labels, empty_values).is_err());
351    }
352
353    #[test]
354    fn test_bar_chart_styling() {
355        let labels = vec!["X".to_string(), "Y".to_string()];
356        let values = vec![5.0, 10.0];
357        let color = Vec4::new(1.0, 0.0, 0.0, 1.0);
358
359        let chart = BarChart::new(labels, values)
360            .unwrap()
361            .with_style(color, 0.6)
362            .with_outline(Vec4::new(0.0, 0.0, 0.0, 1.0), 2.0)
363            .with_label("Test Chart");
364
365        assert_eq!(chart.color, color);
366        assert_eq!(chart.bar_width, 0.6);
367        assert_eq!(chart.outline_color, Some(Vec4::new(0.0, 0.0, 0.0, 1.0)));
368        assert_eq!(chart.outline_width, 2.0);
369        assert_eq!(chart.label, Some("Test Chart".to_string()));
370    }
371
372    #[test]
373    fn test_bar_chart_bounds() {
374        let labels = vec!["A".to_string(), "B".to_string(), "C".to_string()];
375        let values = vec![5.0, -2.0, 8.0];
376
377        let mut chart = BarChart::new(labels, values).unwrap();
378        let bounds = chart.bounds();
379
380        // X bounds should span all bars
381        assert!(bounds.min.x < 0.0);
382        assert!(bounds.max.x > 2.0);
383
384        // Y bounds should include negative and positive values
385        assert_eq!(bounds.min.y, -2.0);
386        assert_eq!(bounds.max.y, 8.0);
387    }
388
389    #[test]
390    fn test_bar_chart_vertex_generation() {
391        let labels = vec!["A".to_string(), "B".to_string()];
392        let values = vec![3.0, 5.0];
393
394        let mut chart = BarChart::new(labels, values).unwrap();
395        let (vertices, indices) = chart.generate_vertices();
396
397        // Should have 4 vertices per bar (rectangle)
398        assert_eq!(vertices.len(), 8);
399
400        // Should have 6 indices per bar (2 triangles)
401        assert_eq!(indices.len(), 12);
402
403        // Check first bar vertices are reasonable
404        assert_eq!(vertices[0].position[1], 0.0); // Bottom
405        assert_eq!(vertices[2].position[1], 3.0); // Top of first bar
406    }
407
408    #[test]
409    fn test_bar_chart_render_data() {
410        let labels = vec!["Test".to_string()];
411        let values = vec![10.0];
412
413        let mut chart = BarChart::new(labels, values).unwrap();
414        let render_data = chart.render_data();
415
416        assert_eq!(render_data.pipeline_type, PipelineType::Triangles);
417        assert_eq!(render_data.vertices.len(), 4); // One rectangle
418        assert!(render_data.indices.is_some());
419        assert_eq!(render_data.indices.as_ref().unwrap().len(), 6); // Two triangles
420    }
421
422    #[test]
423    fn test_bar_chart_statistics() {
424        let labels = vec!["A".to_string(), "B".to_string(), "C".to_string()];
425        let values = vec![1.0, 5.0, 3.0];
426
427        let chart = BarChart::new(labels, values).unwrap();
428        let stats = chart.statistics();
429
430        assert_eq!(stats.bar_count, 3);
431        assert_eq!(stats.value_range, (1.0, 5.0));
432        assert!(stats.memory_usage > 0);
433    }
434
435    #[test]
436    fn test_matlab_compat_bar() {
437        use super::matlab_compat::*;
438
439        let values = vec![1.0, 3.0, 2.0];
440
441        let chart1 = bar(values.clone()).unwrap();
442        assert_eq!(chart1.len(), 3);
443        assert_eq!(chart1.labels, vec!["1", "2", "3"]);
444
445        let labels = vec!["X".to_string(), "Y".to_string(), "Z".to_string()];
446        let chart2 = bar_with_labels(labels.clone(), values.clone()).unwrap();
447        assert_eq!(chart2.labels, labels);
448
449        let chart3 = bar_with_color(values, "g").unwrap();
450        assert_eq!(chart3.color, Vec4::new(0.0, 1.0, 0.0, 1.0));
451    }
452}