Skip to main content

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::{
6    BoundingBox, DrawCall, GpuVertexBuffer, Material, PipelineType, RenderData, Vertex,
7};
8use glam::{Vec3, Vec4};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Orientation {
12    Vertical,
13    Horizontal,
14}
15
16/// High-performance GPU-accelerated bar chart
17#[derive(Debug, Clone)]
18pub struct BarChart {
19    /// Category labels and values
20    pub labels: Vec<String>,
21    values: Option<Vec<f64>>,
22    value_count: usize,
23
24    /// Visual styling
25    pub color: Vec4,
26    pub bar_width: f32,
27    pub outline_color: Option<Vec4>,
28    pub outline_width: f32,
29    per_bar_colors: Option<Vec<Vec4>>,
30
31    /// Orientation (vertical = default bar, horizontal = barh)
32    pub orientation: Orientation,
33
34    /// Grouped bar configuration: index within group and total group count
35    /// Example: for 3-series grouped bars, group_count=3 and each series has group_index 0,1,2
36    pub group_index: usize,
37    pub group_count: usize,
38
39    /// Stacked bar offsets per category (bottom/base for each bar)
40    /// When provided, bars are drawn starting at offset[i] and extending by values[i]
41    pub stack_offsets: Option<Vec<f64>>,
42
43    /// Metadata
44    pub label: Option<String>,
45    pub visible: bool,
46    histogram_bin_edges: Option<Vec<f64>>,
47
48    /// Generated rendering data (cached)
49    vertices: Option<Vec<Vertex>>,
50    indices: Option<Vec<u32>>,
51    bounds: Option<BoundingBox>,
52    dirty: bool,
53    gpu_vertices: Option<GpuVertexBuffer>,
54    gpu_vertex_count: Option<usize>,
55    gpu_bounds: Option<BoundingBox>,
56}
57
58impl BarChart {
59    fn histogram_slot_geometry(&self, index: usize) -> Option<(f32, f32)> {
60        let edges = self.histogram_bin_edges.as_ref()?;
61        let left = *edges.get(index)? as f32;
62        let right = *edges.get(index + 1)? as f32;
63        if !(left.is_finite() && right.is_finite()) {
64            return None;
65        }
66        let bin_width = (right - left).abs().max(f32::EPSILON);
67        let direction = if right >= left { 1.0 } else { -1.0 };
68        let available_width = bin_width * self.bar_width.clamp(0.1, 1.0);
69        let per_group_width = (available_width / self.group_count.max(1) as f32).max(0.0);
70        let start = left + direction * ((bin_width - available_width) * 0.5);
71        let bar_start = start + direction * (per_group_width * self.group_index as f32);
72        let bar_end = bar_start + direction * per_group_width;
73        Some(if direction >= 0.0 {
74            (bar_start.min(bar_end), bar_start.max(bar_end))
75        } else {
76            (bar_end.min(bar_start), bar_end.max(bar_start))
77        })
78    }
79
80    /// Create a new bar chart with labels and values
81    pub fn new(labels: Vec<String>, values: Vec<f64>) -> Result<Self, String> {
82        if labels.len() != values.len() {
83            return Err(format!(
84                "Data length mismatch: {} labels, {} values",
85                labels.len(),
86                values.len()
87            ));
88        }
89
90        if labels.is_empty() {
91            return Err("Cannot create bar chart with empty data".to_string());
92        }
93
94        let count = values.len();
95        Ok(Self {
96            labels,
97            values: Some(values),
98            value_count: count,
99            color: Vec4::new(0.0, 0.5, 1.0, 1.0), // Default blue
100            bar_width: 0.8,                       // 80% of available space
101            outline_color: None,
102            outline_width: 1.0,
103            orientation: Orientation::Vertical,
104            group_index: 0,
105            group_count: 1,
106            stack_offsets: None,
107            label: None,
108            visible: true,
109            histogram_bin_edges: None,
110            vertices: None,
111            indices: None,
112            bounds: None,
113            dirty: true,
114            gpu_vertices: None,
115            gpu_vertex_count: None,
116            gpu_bounds: None,
117            per_bar_colors: None,
118        })
119    }
120
121    /// Build a bar chart backed by a GPU vertex buffer.
122    pub fn from_gpu_buffer(
123        labels: Vec<String>,
124        value_count: usize,
125        buffer: GpuVertexBuffer,
126        vertex_count: usize,
127        bounds: BoundingBox,
128        color: Vec4,
129        bar_width: f32,
130    ) -> Self {
131        Self {
132            labels,
133            values: None,
134            value_count,
135            color,
136            bar_width,
137            outline_color: None,
138            outline_width: 1.0,
139            orientation: Orientation::Vertical,
140            group_index: 0,
141            group_count: 1,
142            stack_offsets: None,
143            label: None,
144            visible: true,
145            histogram_bin_edges: None,
146            vertices: None,
147            indices: None,
148            bounds: Some(bounds),
149            dirty: false,
150            gpu_vertices: Some(buffer),
151            gpu_vertex_count: Some(vertex_count),
152            gpu_bounds: Some(bounds),
153            per_bar_colors: None,
154        }
155    }
156
157    pub fn set_data(&mut self, labels: Vec<String>, values: Vec<f64>) -> Result<(), String> {
158        if labels.len() != values.len() || labels.is_empty() {
159            return Err(
160                "Bar data must be non-empty and label/value lengths must match".to_string(),
161            );
162        }
163        self.labels = labels;
164        self.value_count = values.len();
165        self.values = Some(values);
166        self.vertices = None;
167        self.indices = None;
168        self.bounds = None;
169        self.gpu_vertices = None;
170        self.gpu_vertex_count = None;
171        self.gpu_bounds = None;
172        self.dirty = true;
173        Ok(())
174    }
175
176    fn invalidate_gpu_data(&mut self) {
177        self.gpu_vertices = None;
178        self.gpu_vertex_count = None;
179        self.gpu_bounds = None;
180    }
181
182    /// Create a bar chart with custom styling
183    pub fn with_style(mut self, color: Vec4, bar_width: f32) -> Self {
184        self.color = color;
185        self.bar_width = bar_width.clamp(0.1, 1.0);
186        self.dirty = true;
187        self
188    }
189
190    /// Add outline to bars
191    pub fn with_outline(mut self, outline_color: Vec4, outline_width: f32) -> Self {
192        self.outline_color = Some(outline_color);
193        self.outline_width = outline_width.max(0.1);
194        self.dirty = true;
195        self
196    }
197
198    /// Set orientation (vertical/horizontal)
199    pub fn with_orientation(mut self, orientation: Orientation) -> Self {
200        self.orientation = orientation;
201        self.dirty = true;
202        self
203    }
204
205    pub fn bar_count(&self) -> usize {
206        self.value_count
207    }
208
209    pub fn values(&self) -> Option<&[f64]> {
210        self.values.as_deref()
211    }
212
213    pub fn stack_offsets(&self) -> Option<&[f64]> {
214        self.stack_offsets.as_deref()
215    }
216
217    pub fn histogram_bin_edges(&self) -> Option<&[f64]> {
218        self.histogram_bin_edges.as_deref()
219    }
220
221    pub fn set_histogram_bin_edges(&mut self, edges: Vec<f64>) {
222        if edges.len() == self.value_count + 1 {
223            self.histogram_bin_edges = Some(edges);
224        }
225    }
226
227    pub fn set_per_bar_colors(&mut self, colors: Vec<Vec4>) {
228        if colors.is_empty() {
229            self.per_bar_colors = None;
230        } else {
231            self.per_bar_colors = Some(colors);
232        }
233        self.dirty = true;
234        self.invalidate_gpu_data();
235    }
236
237    pub fn clear_per_bar_colors(&mut self) {
238        if self.per_bar_colors.is_some() {
239            self.per_bar_colors = None;
240            self.dirty = true;
241        }
242    }
243
244    /// Configure grouped bars (index within group and total group count)
245    pub fn with_group(mut self, group_index: usize, group_count: usize) -> Self {
246        self.group_index = group_index.min(group_count.saturating_sub(1));
247        self.group_count = group_count.max(1);
248        self.dirty = true;
249        self
250    }
251
252    /// Configure stacked bars with per-category offsets (base values)
253    pub fn with_stack_offsets(mut self, offsets: Vec<f64>) -> Self {
254        if self
255            .values
256            .as_ref()
257            .is_some_and(|v| offsets.len() == v.len())
258            || offsets.len() == self.value_count
259        {
260            self.stack_offsets = Some(offsets);
261            self.dirty = true;
262            self.invalidate_gpu_data();
263        }
264        self
265    }
266
267    /// Set the chart label for legends
268    pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
269        self.label = Some(label.into());
270        self
271    }
272
273    /// Update the data
274    pub fn update_data(&mut self, labels: Vec<String>, values: Vec<f64>) -> Result<(), String> {
275        if labels.len() != values.len() {
276            return Err(format!(
277                "Data length mismatch: {} labels, {} values",
278                labels.len(),
279                values.len()
280            ));
281        }
282
283        if labels.is_empty() {
284            return Err("Cannot update with empty data".to_string());
285        }
286
287        self.labels = labels;
288        self.value_count = values.len();
289        self.values = Some(values);
290        self.dirty = true;
291        self.vertices = None;
292        self.indices = None;
293        self.bounds = None;
294        self.invalidate_gpu_data();
295        Ok(())
296    }
297
298    /// Set the bar color
299    pub fn set_color(&mut self, color: Vec4) {
300        self.color = color;
301        self.per_bar_colors = None;
302        self.dirty = true;
303    }
304
305    /// Set the bar width (0.1 to 1.0)
306    pub fn set_bar_width(&mut self, width: f32) {
307        self.bar_width = width.clamp(0.1, 1.0);
308        self.dirty = true;
309    }
310
311    /// Override the face color and width without invalidating GPU data.
312    pub fn apply_face_style(&mut self, color: Vec4, width: f32) {
313        if self.gpu_vertices.is_some() {
314            self.color = color;
315            self.bar_width = width.clamp(0.1, 1.0);
316        } else {
317            self.set_color(color);
318            self.set_bar_width(width);
319        }
320    }
321
322    /// Set the outline color (enables outline if not present)
323    pub fn set_outline_color(&mut self, color: Vec4) {
324        if self.outline_color.is_none() {
325            self.outline_width = self.outline_width.max(1.0);
326        }
327        self.outline_color = Some(color);
328        self.dirty = true;
329    }
330
331    /// Set the outline width (enables outline if not present)
332    pub fn set_outline_width(&mut self, width: f32) {
333        self.outline_width = width.max(0.1);
334        if self.outline_color.is_none() {
335            // Default to black if no color set
336            self.outline_color = Some(Vec4::new(0.0, 0.0, 0.0, 1.0));
337        }
338        self.dirty = true;
339        self.invalidate_gpu_data();
340    }
341
342    /// Override outline styling while preserving GPU geometry when possible.
343    pub fn apply_outline_style(&mut self, color: Option<Vec4>, width: f32) {
344        match color {
345            Some(color) => {
346                if self.gpu_vertices.is_some() {
347                    self.outline_color = Some(color);
348                    self.outline_width = width.max(0.1);
349                } else {
350                    self.set_outline_color(color);
351                    self.set_outline_width(width);
352                }
353            }
354            None => {
355                self.outline_color = None;
356            }
357        }
358    }
359
360    /// Show or hide the chart
361    pub fn set_visible(&mut self, visible: bool) {
362        self.visible = visible;
363    }
364
365    /// Get the number of bars
366    pub fn len(&self) -> usize {
367        self.value_count
368    }
369
370    /// Check if the chart has no data
371    pub fn is_empty(&self) -> bool {
372        self.value_count == 0
373    }
374
375    /// Generate vertices for GPU rendering
376    pub fn generate_vertices(&mut self) -> (&Vec<Vertex>, &Vec<u32>) {
377        if self.gpu_vertices.is_some() {
378            if self.vertices.is_none() {
379                self.vertices = Some(Vec::new());
380            }
381            if self.indices.is_none() {
382                self.indices = Some(Vec::new());
383            }
384            return (
385                self.vertices.as_ref().unwrap(),
386                self.indices.as_ref().unwrap(),
387            );
388        }
389
390        if self.dirty || self.vertices.is_none() {
391            let (vertices, indices) = self.create_bar_geometry();
392            self.vertices = Some(vertices);
393            self.indices = Some(indices);
394            self.dirty = false;
395        }
396        (
397            self.vertices.as_ref().unwrap(),
398            self.indices.as_ref().unwrap(),
399        )
400    }
401
402    /// Create the geometry for all bars
403    fn create_bar_geometry(&self) -> (Vec<Vertex>, Vec<u32>) {
404        let values = self
405            .values
406            .as_ref()
407            .expect("CPU bar geometry requested without host values");
408        let mut vertices = Vec::new();
409        let mut indices = Vec::new();
410
411        let group_count = self.group_count.max(1) as f32;
412        let per_group_width = (self.bar_width / group_count).max(0.01);
413        let group_offset_start = -self.bar_width * 0.5;
414        let local_offset = group_offset_start
415            + per_group_width * (self.group_index as f32)
416            + per_group_width * 0.5;
417
418        match self.orientation {
419            Orientation::Vertical => {
420                for (i, &value) in values.iter().enumerate() {
421                    if !value.is_finite() {
422                        continue;
423                    }
424                    let color = self.color_for_bar(i);
425                    let (left, right) = if self.histogram_bin_edges.is_some() {
426                        self.histogram_slot_geometry(i)
427                            .unwrap_or(((i as f32) + 0.6, (i as f32) + 1.4))
428                    } else {
429                        let x_center = (i as f32) + 1.0;
430                        let center = x_center + local_offset;
431                        let half = per_group_width * 0.5;
432                        (center - half, center + half)
433                    };
434                    let base = self
435                        .stack_offsets
436                        .as_ref()
437                        .map(|v| v[i] as f32)
438                        .unwrap_or(0.0);
439                    let bottom = base;
440                    let top = base + value as f32;
441
442                    let vertex_offset = vertices.len() as u32;
443                    vertices.push(Vertex::new(Vec3::new(left, bottom, 0.0), color));
444                    vertices.push(Vertex::new(Vec3::new(right, bottom, 0.0), color));
445                    vertices.push(Vertex::new(Vec3::new(right, top, 0.0), color));
446                    vertices.push(Vertex::new(Vec3::new(left, top, 0.0), color));
447                    indices.push(vertex_offset);
448                    indices.push(vertex_offset + 1);
449                    indices.push(vertex_offset + 2);
450                    indices.push(vertex_offset);
451                    indices.push(vertex_offset + 2);
452                    indices.push(vertex_offset + 3);
453                }
454            }
455            Orientation::Horizontal => {
456                for (i, &value) in values.iter().enumerate() {
457                    if !value.is_finite() {
458                        continue;
459                    }
460                    let color = self.color_for_bar(i);
461                    let (bottom, top) = if self.histogram_bin_edges.is_some() {
462                        self.histogram_slot_geometry(i)
463                            .unwrap_or(((i as f32) + 0.6, (i as f32) + 1.4))
464                    } else {
465                        let y_center = (i as f32) + 1.0;
466                        let center = y_center + local_offset;
467                        let half = per_group_width * 0.5;
468                        (center - half, center + half)
469                    };
470                    let base = self
471                        .stack_offsets
472                        .as_ref()
473                        .map(|v| v[i] as f32)
474                        .unwrap_or(0.0);
475                    let left = base;
476                    let right = base + value as f32;
477
478                    let vertex_offset = vertices.len() as u32;
479                    vertices.push(Vertex::new(Vec3::new(left, bottom, 0.0), color));
480                    vertices.push(Vertex::new(Vec3::new(right, bottom, 0.0), color));
481                    vertices.push(Vertex::new(Vec3::new(right, top, 0.0), color));
482                    vertices.push(Vertex::new(Vec3::new(left, top, 0.0), color));
483                    indices.push(vertex_offset);
484                    indices.push(vertex_offset + 1);
485                    indices.push(vertex_offset + 2);
486                    indices.push(vertex_offset);
487                    indices.push(vertex_offset + 2);
488                    indices.push(vertex_offset + 3);
489                }
490            }
491        }
492
493        (vertices, indices)
494    }
495
496    fn color_for_bar(&self, index: usize) -> Vec4 {
497        if let Some(colors) = &self.per_bar_colors {
498            if let Some(color) = colors.get(index) {
499                return *color;
500            }
501        }
502        self.color
503    }
504
505    /// Get the bounding box of the chart
506    pub fn bounds(&mut self) -> BoundingBox {
507        if let Some(bounds) = self.gpu_bounds {
508            self.bounds = Some(bounds);
509            return bounds;
510        }
511
512        if self.dirty || self.bounds.is_none() {
513            let values = self
514                .values
515                .as_ref()
516                .expect("CPU bar bounds requested without host values");
517            let num_bars = values.len();
518            if num_bars == 0 {
519                self.bounds = Some(BoundingBox::default());
520                return self.bounds.unwrap();
521            }
522
523            match self.orientation {
524                Orientation::Vertical => {
525                    let (min_x, max_x) = if self.histogram_bin_edges.is_some() {
526                        let mut min_x = f32::INFINITY;
527                        let mut max_x = f32::NEG_INFINITY;
528                        for i in 0..num_bars {
529                            if let Some((left, right)) = self.histogram_slot_geometry(i) {
530                                min_x = min_x.min(left);
531                                max_x = max_x.max(right);
532                            }
533                        }
534                        if !min_x.is_finite() || !max_x.is_finite() {
535                            (
536                                1.0 - self.bar_width * 0.5,
537                                num_bars as f32 + self.bar_width * 0.5,
538                            )
539                        } else {
540                            (min_x, max_x)
541                        }
542                    } else {
543                        (
544                            1.0 - self.bar_width * 0.5,
545                            num_bars as f32 + self.bar_width * 0.5,
546                        )
547                    };
548                    // Y spans min/max of values and optional stack offsets
549                    let (mut min_y, mut max_y) = (0.0f32, 0.0f32);
550                    if let Some(offsets) = &self.stack_offsets {
551                        for i in 0..num_bars {
552                            let base = offsets[i] as f32;
553                            let v = values[i];
554                            if !v.is_finite() {
555                                continue;
556                            }
557                            let top = base + v as f32;
558                            min_y = min_y.min(base.min(top));
559                            max_y = max_y.max(base.max(top));
560                        }
561                    } else {
562                        for &v in values {
563                            if !v.is_finite() {
564                                continue;
565                            }
566                            min_y = min_y.min(v as f32);
567                            max_y = max_y.max(v as f32);
568                        }
569                    }
570                    self.bounds = Some(BoundingBox::new(
571                        Vec3::new(min_x, min_y, 0.0),
572                        Vec3::new(max_x, max_y, 0.0),
573                    ));
574                }
575                Orientation::Horizontal => {
576                    let (min_y, max_y) = if self.histogram_bin_edges.is_some() {
577                        let mut min_y = f32::INFINITY;
578                        let mut max_y = f32::NEG_INFINITY;
579                        for i in 0..num_bars {
580                            if let Some((bottom, top)) = self.histogram_slot_geometry(i) {
581                                min_y = min_y.min(bottom);
582                                max_y = max_y.max(top);
583                            }
584                        }
585                        if !min_y.is_finite() || !max_y.is_finite() {
586                            (
587                                1.0 - self.bar_width * 0.5,
588                                num_bars as f32 + self.bar_width * 0.5,
589                            )
590                        } else {
591                            (min_y, max_y)
592                        }
593                    } else {
594                        (
595                            1.0 - self.bar_width * 0.5,
596                            num_bars as f32 + self.bar_width * 0.5,
597                        )
598                    };
599                    // X spans min/max of values and optional stack offsets
600                    let (mut min_x, mut max_x) = (0.0f32, 0.0f32);
601                    if let Some(offsets) = &self.stack_offsets {
602                        for i in 0..num_bars {
603                            let base = offsets[i] as f32;
604                            let v = values[i];
605                            if !v.is_finite() {
606                                continue;
607                            }
608                            let right = base + v as f32;
609                            min_x = min_x.min(base.min(right));
610                            max_x = max_x.max(base.max(right));
611                        }
612                    } else {
613                        for &v in values {
614                            if !v.is_finite() {
615                                continue;
616                            }
617                            min_x = min_x.min(v as f32);
618                            max_x = max_x.max(v as f32);
619                        }
620                    }
621                    self.bounds = Some(BoundingBox::new(
622                        Vec3::new(min_x, min_y, 0.0),
623                        Vec3::new(max_x, max_y, 0.0),
624                    ));
625                }
626            }
627        }
628        self.bounds.unwrap()
629    }
630
631    /// Generate complete render data for the graphics pipeline
632    pub fn render_data(&mut self) -> RenderData {
633        let using_gpu = self.gpu_vertices.is_some();
634        let gpu_vertices = self.gpu_vertices.clone();
635        let bounds = self.bounds();
636        let (vertices, indices, vertex_count) = if using_gpu {
637            let count = self
638                .gpu_vertex_count
639                .or_else(|| gpu_vertices.as_ref().map(|buf| buf.vertex_count))
640                .unwrap_or(0);
641            (Vec::new(), None, count)
642        } else {
643            let (verts, inds) = self.generate_vertices();
644            (verts.clone(), Some(inds.clone()), verts.len())
645        };
646
647        let material = Material {
648            albedo: self.color,
649            ..Default::default()
650        };
651
652        let draw_call = DrawCall {
653            vertex_offset: 0,
654            vertex_count,
655            index_offset: indices.as_ref().map(|_| 0),
656            index_count: indices.as_ref().map(|ind| ind.len()),
657            instance_count: 1,
658        };
659
660        RenderData {
661            pipeline_type: PipelineType::Triangles,
662            vertices,
663            indices,
664            gpu_vertices,
665            bounds: Some(bounds),
666            material,
667            draw_calls: vec![draw_call],
668            image: None,
669        }
670    }
671
672    /// Get chart statistics for debugging
673    pub fn statistics(&self) -> BarChartStatistics {
674        let (bar_count, value_range) = if let Some(values) = &self.values {
675            if values.is_empty() {
676                (0, (0.0, 0.0))
677            } else {
678                let min_val = values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
679                let max_val = values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
680                (values.len(), (min_val, max_val))
681            }
682        } else if let Some(bounds) = self.gpu_bounds.or(self.bounds) {
683            (self.value_count, (bounds.min.y as f64, bounds.max.y as f64))
684        } else {
685            (self.value_count, (0.0, 0.0))
686        };
687
688        BarChartStatistics {
689            bar_count,
690            value_range,
691            memory_usage: self.estimated_memory_usage(),
692        }
693    }
694
695    /// Estimate memory usage in bytes
696    pub fn estimated_memory_usage(&self) -> usize {
697        let labels_size: usize = self.labels.iter().map(|s| s.len()).sum();
698        let values_size = self
699            .values
700            .as_ref()
701            .map_or(0, |v| v.len() * std::mem::size_of::<f64>());
702        let vertices_size = self
703            .vertices
704            .as_ref()
705            .map_or(0, |v| v.len() * std::mem::size_of::<Vertex>());
706        let indices_size = self
707            .indices
708            .as_ref()
709            .map_or(0, |i| i.len() * std::mem::size_of::<u32>());
710
711        labels_size + values_size + vertices_size + indices_size
712    }
713}
714
715/// Bar chart statistics
716#[derive(Debug, Clone)]
717pub struct BarChartStatistics {
718    pub bar_count: usize,
719    pub value_range: (f64, f64),
720    pub memory_usage: usize,
721}
722
723/// MATLAB-compatible bar chart creation utilities
724pub mod matlab_compat {
725    use super::*;
726
727    /// Create a simple bar chart (equivalent to MATLAB's `bar(values)`)
728    pub fn bar(values: Vec<f64>) -> Result<BarChart, String> {
729        let labels: Vec<String> = (1..=values.len()).map(|i| i.to_string()).collect();
730        BarChart::new(labels, values)
731    }
732
733    /// Create a bar chart with custom labels (`bar(labels, values)`)
734    pub fn bar_with_labels(labels: Vec<String>, values: Vec<f64>) -> Result<BarChart, String> {
735        BarChart::new(labels, values)
736    }
737
738    /// Create a bar chart with specified color
739    pub fn bar_with_color(values: Vec<f64>, color: &str) -> Result<BarChart, String> {
740        let color_vec = parse_matlab_color(color)?;
741        let labels: Vec<String> = (1..=values.len()).map(|i| i.to_string()).collect();
742        Ok(BarChart::new(labels, values)?.with_style(color_vec, 0.8))
743    }
744
745    /// Parse MATLAB color specifications
746    fn parse_matlab_color(color: &str) -> Result<Vec4, String> {
747        match color {
748            "r" | "red" => Ok(Vec4::new(1.0, 0.0, 0.0, 1.0)),
749            "g" | "green" => Ok(Vec4::new(0.0, 1.0, 0.0, 1.0)),
750            "b" | "blue" => Ok(Vec4::new(0.0, 0.0, 1.0, 1.0)),
751            "c" | "cyan" => Ok(Vec4::new(0.0, 1.0, 1.0, 1.0)),
752            "m" | "magenta" => Ok(Vec4::new(1.0, 0.0, 1.0, 1.0)),
753            "y" | "yellow" => Ok(Vec4::new(1.0, 1.0, 0.0, 1.0)),
754            "k" | "black" => Ok(Vec4::new(0.0, 0.0, 0.0, 1.0)),
755            "w" | "white" => Ok(Vec4::new(1.0, 1.0, 1.0, 1.0)),
756            _ => Err(format!("Unknown color: {color}")),
757        }
758    }
759}
760
761#[cfg(test)]
762mod tests {
763    use super::*;
764
765    #[test]
766    fn test_bar_chart_creation() {
767        let labels = vec!["A".to_string(), "B".to_string(), "C".to_string()];
768        let values = vec![10.0, 25.0, 15.0];
769
770        let chart = BarChart::new(labels.clone(), values.clone()).unwrap();
771
772        assert_eq!(chart.labels, labels);
773        assert_eq!(chart.values.as_ref(), Some(&values));
774        assert_eq!(chart.len(), 3);
775        assert!(!chart.is_empty());
776        assert!(chart.visible);
777    }
778
779    #[test]
780    fn test_bar_chart_data_validation() {
781        // Mismatched lengths should fail
782        let labels = vec!["A".to_string(), "B".to_string()];
783        let values = vec![10.0, 25.0, 15.0];
784        assert!(BarChart::new(labels, values).is_err());
785
786        // Empty data should fail
787        let empty_labels: Vec<String> = vec![];
788        let empty_values: Vec<f64> = vec![];
789        assert!(BarChart::new(empty_labels, empty_values).is_err());
790    }
791
792    #[test]
793    fn test_bar_chart_styling() {
794        let labels = vec!["X".to_string(), "Y".to_string()];
795        let values = vec![5.0, 10.0];
796        let color = Vec4::new(1.0, 0.0, 0.0, 1.0);
797
798        let chart = BarChart::new(labels, values)
799            .unwrap()
800            .with_style(color, 0.6)
801            .with_outline(Vec4::new(0.0, 0.0, 0.0, 1.0), 2.0)
802            .with_label("Test Chart");
803
804        assert_eq!(chart.color, color);
805        assert_eq!(chart.bar_width, 0.6);
806        assert_eq!(chart.outline_color, Some(Vec4::new(0.0, 0.0, 0.0, 1.0)));
807        assert_eq!(chart.outline_width, 2.0);
808        assert_eq!(chart.label, Some("Test Chart".to_string()));
809    }
810
811    #[test]
812    fn test_bar_chart_bounds() {
813        let labels = vec!["A".to_string(), "B".to_string(), "C".to_string()];
814        let values = vec![5.0, -2.0, 8.0];
815
816        let mut chart = BarChart::new(labels, values).unwrap();
817        let bounds = chart.bounds();
818
819        // X bounds should span all bars (centers are 1-based)
820        assert!(bounds.min.x < 1.0);
821        assert!(bounds.max.x > 3.0);
822
823        // Y bounds should include negative and positive values
824        assert_eq!(bounds.min.y, -2.0);
825        assert_eq!(bounds.max.y, 8.0);
826    }
827
828    #[test]
829    fn test_bar_chart_vertex_generation() {
830        let labels = vec!["A".to_string(), "B".to_string()];
831        let values = vec![3.0, 5.0];
832
833        let mut chart = BarChart::new(labels, values).unwrap();
834        let (vertices, indices) = chart.generate_vertices();
835
836        // Should have 4 vertices per bar (rectangle)
837        assert_eq!(vertices.len(), 8);
838
839        // Should have 6 indices per bar (2 triangles)
840        assert_eq!(indices.len(), 12);
841
842        // Check first bar vertices are reasonable
843        assert_eq!(vertices[0].position[1], 0.0); // Bottom
844        assert_eq!(vertices[2].position[1], 3.0); // Top of first bar
845    }
846
847    #[test]
848    fn test_bar_chart_render_data() {
849        let labels = vec!["Test".to_string()];
850        let values = vec![10.0];
851
852        let mut chart = BarChart::new(labels, values).unwrap();
853        let render_data = chart.render_data();
854
855        assert_eq!(render_data.pipeline_type, PipelineType::Triangles);
856        assert_eq!(render_data.vertices.len(), 4); // One rectangle
857        assert!(render_data.indices.is_some());
858        assert_eq!(render_data.indices.as_ref().unwrap().len(), 6); // Two triangles
859        let bounds = render_data.bounds.expect("bar render data bounds");
860        assert_eq!(bounds.min.x, 0.6);
861        assert_eq!(bounds.max.x, 1.4);
862        assert_eq!(bounds.min.y, 0.0);
863        assert_eq!(bounds.max.y, 10.0);
864    }
865
866    #[test]
867    fn histogram_edges_drive_bar_geometry_and_bounds() {
868        let labels = vec!["bin1".to_string(), "bin2".to_string()];
869        let values = vec![2.0, 3.0];
870
871        let mut chart = BarChart::new(labels, values).unwrap();
872        chart.set_histogram_bin_edges(vec![0.0, 0.5, 1.0]);
873        chart.set_bar_width(1.0);
874
875        let bounds = chart.bounds();
876        assert_eq!(bounds.min.x, 0.0);
877        assert_eq!(bounds.max.x, 1.0);
878
879        let (vertices, _) = chart.generate_vertices();
880        assert_eq!(vertices[0].position[0], 0.0);
881        assert_eq!(vertices[1].position[0], 0.5);
882        assert_eq!(vertices[4].position[0], 0.5);
883        assert_eq!(vertices[5].position[0], 1.0);
884    }
885
886    #[test]
887    fn test_bar_chart_statistics() {
888        let labels = vec!["A".to_string(), "B".to_string(), "C".to_string()];
889        let values = vec![1.0, 5.0, 3.0];
890
891        let chart = BarChart::new(labels, values).unwrap();
892        let stats = chart.statistics();
893
894        assert_eq!(stats.bar_count, 3);
895        assert_eq!(stats.value_range, (1.0, 5.0));
896        assert!(stats.memory_usage > 0);
897    }
898
899    #[test]
900    fn test_matlab_compat_bar() {
901        use super::matlab_compat::*;
902
903        let values = vec![1.0, 3.0, 2.0];
904
905        let chart1 = bar(values.clone()).unwrap();
906        assert_eq!(chart1.len(), 3);
907        assert_eq!(chart1.labels, vec!["1", "2", "3"]);
908
909        let labels = vec!["X".to_string(), "Y".to_string(), "Z".to_string()];
910        let chart2 = bar_with_labels(labels.clone(), values.clone()).unwrap();
911        assert_eq!(chart2.labels, labels);
912
913        let chart3 = bar_with_color(values, "g").unwrap();
914        assert_eq!(chart3.color, Vec4::new(0.0, 1.0, 0.0, 1.0));
915    }
916}