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