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