1use 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#[derive(Debug, Clone)]
27pub struct BarChart {
28 pub labels: Vec<String>,
30 values: Option<Vec<f64>>,
31 value_count: usize,
32
33 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 pub orientation: Orientation,
42
43 pub group_index: usize,
46 pub group_count: usize,
47
48 pub stack_offsets: Option<Vec<f64>>,
51
52 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 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 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), bar_width: 0.8, 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 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 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 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 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 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 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 pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
400 self.label = Some(label.into());
401 self
402 }
403
404 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 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 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 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 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 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 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 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 pub fn set_visible(&mut self, visible: bool) {
495 self.visible = visible;
496 }
497
498 pub fn len(&self) -> usize {
500 self.value_count
501 }
502
503 pub fn is_empty(&self) -> bool {
505 self.value_count == 0
506 }
507
508 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 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 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 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 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 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 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 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#[derive(Debug, Clone)]
968pub struct BarChartStatistics {
969 pub bar_count: usize,
970 pub value_range: (f64, f64),
971 pub memory_usage: usize,
972}
973
974pub mod matlab_compat {
976 use super::*;
977
978 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 pub fn bar_with_labels(labels: Vec<String>, values: Vec<f64>) -> Result<BarChart, String> {
986 BarChart::new(labels, values)
987 }
988
989 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 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 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 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 assert!(bounds.min.x < 1.0);
1072 assert!(bounds.max.x > 3.0);
1073
1074 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 assert_eq!(vertices.len(), 8);
1107
1108 assert_eq!(indices.len(), 12);
1110
1111 assert_eq!(vertices[0].position[1], 0.0); assert_eq!(vertices[2].position[1], 3.0); }
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); assert!(render_data.indices.is_some());
1127 assert_eq!(render_data.indices.as_ref().unwrap().len(), 6); 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}