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)]
21pub struct BarChart {
22 pub labels: Vec<String>,
24 values: Option<Vec<f64>>,
25 value_count: usize,
26
27 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 pub orientation: Orientation,
36
37 pub group_index: usize,
40 pub group_count: usize,
41
42 pub stack_offsets: Option<Vec<f64>>,
45
46 pub label: Option<String>,
48 pub visible: bool,
49 histogram_bin_edges: Option<Vec<f64>>,
50
51 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 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), bar_width: 0.8, 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 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 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 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 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 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 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 pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
335 self.label = Some(label.into());
336 self
337 }
338
339 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 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 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 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 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 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 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 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 pub fn set_visible(&mut self, visible: bool) {
430 self.visible = visible;
431 }
432
433 pub fn len(&self) -> usize {
435 self.value_count
436 }
437
438 pub fn is_empty(&self) -> bool {
440 self.value_count == 0
441 }
442
443 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 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 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 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 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 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 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 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#[derive(Debug, Clone)]
784pub struct BarChartStatistics {
785 pub bar_count: usize,
786 pub value_range: (f64, f64),
787 pub memory_usage: usize,
788}
789
790pub mod matlab_compat {
792 use super::*;
793
794 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 pub fn bar_with_labels(labels: Vec<String>, values: Vec<f64>) -> Result<BarChart, String> {
802 BarChart::new(labels, values)
803 }
804
805 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 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 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 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 assert!(bounds.min.x < 1.0);
888 assert!(bounds.max.x > 3.0);
889
890 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 assert_eq!(vertices.len(), 8);
923
924 assert_eq!(indices.len(), 12);
926
927 assert_eq!(vertices[0].position[1], 0.0); assert_eq!(vertices[2].position[1], 3.0); }
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); assert!(render_data.indices.is_some());
943 assert_eq!(render_data.indices.as_ref().unwrap().len(), 6); 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}