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