1use crate::theme::Gradient;
7use presentar_core::{
8 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
9 LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
10};
11use std::any::Any;
12use std::time::Duration;
13
14#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
16pub enum ViolinOrientation {
17 #[default]
19 Vertical,
20 Horizontal,
22}
23
24#[derive(Debug, Clone)]
26pub struct ViolinData {
27 pub label: String,
29 pub values: Vec<f64>,
31 pub color: Color,
33 densities: Option<Vec<f64>>,
35 stats: Option<ViolinStats>,
37}
38
39#[derive(Debug, Clone)]
41pub struct ViolinStats {
42 pub min: f64,
43 pub max: f64,
44 pub median: f64,
45 pub q1: f64,
46 pub q3: f64,
47 pub mean: f64,
48}
49
50impl ViolinData {
51 #[must_use]
53 pub fn new(label: impl Into<String>, values: Vec<f64>) -> Self {
54 Self {
55 label: label.into(),
56 values,
57 color: Color::new(0.3, 0.7, 1.0, 1.0),
58 densities: None,
59 stats: None,
60 }
61 }
62
63 #[must_use]
65 pub fn with_color(mut self, color: Color) -> Self {
66 self.color = color;
67 self
68 }
69
70 fn compute_stats(&mut self) {
72 if self.values.is_empty() {
73 self.stats = Some(ViolinStats {
74 min: 0.0,
75 max: 0.0,
76 median: 0.0,
77 q1: 0.0,
78 q3: 0.0,
79 mean: 0.0,
80 });
81 return;
82 }
83
84 let mut sorted = self.values.clone();
85 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
86
87 let n = sorted.len();
88 let min = sorted[0];
89 let max = sorted[n - 1];
90 let median = if n.is_multiple_of(2) {
91 (sorted[n / 2 - 1] + sorted[n / 2]) / 2.0
92 } else {
93 sorted[n / 2]
94 };
95 let q1 = sorted[n / 4];
96 let q3 = sorted[3 * n / 4];
97 let mean = sorted.iter().sum::<f64>() / n as f64;
98
99 self.stats = Some(ViolinStats {
100 min,
101 max,
102 median,
103 q1,
104 q3,
105 mean,
106 });
107 }
108
109 fn stats(&mut self) -> &ViolinStats {
111 if self.stats.is_none() {
112 self.compute_stats();
113 }
114 self.stats.as_ref().expect("computed above")
115 }
116
117 fn compute_kde(&mut self, num_points: usize) {
120 if self.values.is_empty() {
121 self.densities = Some(vec![0.0; num_points]);
122 return;
123 }
124
125 let stats = self.stats().clone();
126 let range = stats.max - stats.min;
127 if range == 0.0 {
128 self.densities = Some(vec![1.0; num_points]);
129 return;
130 }
131
132 let n = self.values.len() as f64;
134 let std_dev = self.compute_std_dev();
135 let bandwidth = 1.06 * std_dev * n.powf(-0.2);
136
137 let mut densities = vec![0.0; num_points];
138
139 let use_simd = self.values.len() > 100;
142
143 for (i, density) in densities.iter_mut().enumerate() {
144 let x = stats.min + (i as f64 / (num_points - 1) as f64) * range;
145
146 *density = if use_simd {
147 self.kde_at_point_simd(x, bandwidth)
149 } else {
150 self.kde_at_point_scalar(x, bandwidth)
152 };
153 }
154
155 let max_density = densities.iter().copied().fold(0.0, f64::max);
157 if max_density > 0.0 {
158 for d in &mut densities {
159 *d /= max_density;
160 }
161 }
162
163 self.densities = Some(densities);
164 }
165
166 fn compute_std_dev(&self) -> f64 {
167 if self.values.len() < 2 {
168 return 1.0;
169 }
170 let mean = self.values.iter().sum::<f64>() / self.values.len() as f64;
171 let variance =
172 self.values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / self.values.len() as f64;
173 variance.sqrt().max(0.001)
174 }
175
176 fn kde_at_point_scalar(&self, x: f64, bandwidth: f64) -> f64 {
178 let mut sum = 0.0;
179 let inv_bw = 1.0 / bandwidth;
180 for &value in &self.values {
181 let u = (x - value) * inv_bw;
182 sum += (-0.5 * u * u).exp();
184 }
185 sum * inv_bw / (self.values.len() as f64 * (2.0 * std::f64::consts::PI).sqrt())
186 }
187
188 fn kde_at_point_simd(&self, x: f64, bandwidth: f64) -> f64 {
191 let inv_bw = 1.0 / bandwidth;
194 let mut sum = 0.0;
195 let mut i = 0;
196
197 while i + 4 <= self.values.len() {
199 let u0 = (x - self.values[i]) * inv_bw;
200 let u1 = (x - self.values[i + 1]) * inv_bw;
201 let u2 = (x - self.values[i + 2]) * inv_bw;
202 let u3 = (x - self.values[i + 3]) * inv_bw;
203
204 sum += (-0.5 * u0 * u0).exp();
205 sum += (-0.5 * u1 * u1).exp();
206 sum += (-0.5 * u2 * u2).exp();
207 sum += (-0.5 * u3 * u3).exp();
208
209 i += 4;
210 }
211
212 while i < self.values.len() {
214 let u = (x - self.values[i]) * inv_bw;
215 sum += (-0.5 * u * u).exp();
216 i += 1;
217 }
218
219 sum * inv_bw / (self.values.len() as f64 * (2.0 * std::f64::consts::PI).sqrt())
220 }
221}
222
223#[derive(Debug, Clone)]
225pub struct ViolinPlot {
226 violins: Vec<ViolinData>,
227 orientation: ViolinOrientation,
228 show_box: bool,
230 show_median: bool,
232 kde_points: usize,
234 gradient: Option<Gradient>,
236 bounds: Rect,
237}
238
239impl Default for ViolinPlot {
240 fn default() -> Self {
241 Self::new(Vec::new())
242 }
243}
244
245impl ViolinPlot {
246 #[must_use]
248 pub fn new(violins: Vec<ViolinData>) -> Self {
249 Self {
250 violins,
251 orientation: ViolinOrientation::default(),
252 show_box: true,
253 show_median: true,
254 kde_points: 50,
255 gradient: None,
256 bounds: Rect::default(),
257 }
258 }
259
260 #[must_use]
262 pub fn with_orientation(mut self, orientation: ViolinOrientation) -> Self {
263 self.orientation = orientation;
264 self
265 }
266
267 #[must_use]
269 pub fn with_box(mut self, show: bool) -> Self {
270 self.show_box = show;
271 self
272 }
273
274 #[must_use]
276 pub fn with_median(mut self, show: bool) -> Self {
277 self.show_median = show;
278 self
279 }
280
281 #[must_use]
283 pub fn with_kde_points(mut self, points: usize) -> Self {
284 self.kde_points = points.clamp(10, 200);
285 self
286 }
287
288 #[must_use]
290 pub fn with_gradient(mut self, gradient: Gradient) -> Self {
291 self.gradient = Some(gradient);
292 self
293 }
294
295 pub fn add_violin(&mut self, violin: ViolinData) {
297 self.violins.push(violin);
298 }
299
300 fn global_range(&self) -> (f64, f64) {
302 let mut min = f64::INFINITY;
303 let mut max = f64::NEG_INFINITY;
304
305 for violin in &self.violins {
306 for &v in &violin.values {
307 if v.is_finite() {
308 min = min.min(v);
309 max = max.max(v);
310 }
311 }
312 }
313
314 if min == f64::INFINITY {
315 (0.0, 1.0)
316 } else {
317 let padding = (max - min) * 0.05;
318 (min - padding, max + padding)
319 }
320 }
321
322 fn render_vertical(&mut self, canvas: &mut dyn Canvas) {
323 if self.violins.is_empty() {
324 return;
325 }
326
327 let (val_min, val_max) = self.global_range();
328 let n_violins = self.violins.len();
329 let violin_width = self.bounds.width / n_violins as f32;
330
331 for (idx, violin) in self.violins.iter_mut().enumerate() {
332 if violin.densities.is_none() {
333 violin.compute_stats();
334 violin.compute_kde(self.kde_points);
335 }
336
337 let densities = violin.densities.as_ref().expect("computed above");
338 let stats = violin.stats.as_ref().expect("computed above");
339 let center_x = self.bounds.x + (idx as f32 + 0.5) * violin_width;
340 let half_width = violin_width * 0.4;
341
342 for (i, &density) in densities.iter().enumerate() {
344 let t = i as f64 / (densities.len() - 1) as f64;
345 let value = val_min + t * (val_max - val_min);
346 let y = self.bounds.y
347 + (1.0 - (value - val_min) / (val_max - val_min)) as f32 * self.bounds.height;
348
349 if y < self.bounds.y || y >= self.bounds.y + self.bounds.height {
350 continue;
351 }
352
353 let width = (density * half_width as f64) as f32;
354 if width < 0.5 {
355 continue;
356 }
357
358 let color = if let Some(ref gradient) = self.gradient {
359 gradient.sample(density)
360 } else {
361 violin.color
362 };
363
364 let style = TextStyle {
365 color,
366 ..Default::default()
367 };
368
369 let chars = "▏▎▍▌▋▊▉█";
371 let char_vec: Vec<char> = chars.chars().collect();
372 let char_idx = ((width / half_width * 7.0) as usize).min(7);
373 let ch = char_vec[char_idx];
374
375 canvas.draw_text(&ch.to_string(), Point::new(center_x - 1.0, y), &style);
377 canvas.draw_text(&ch.to_string(), Point::new(center_x, y), &style);
379 }
380
381 if self.show_median {
383 let median_y = self.bounds.y
384 + (1.0 - (stats.median - val_min) / (val_max - val_min)) as f32
385 * self.bounds.height;
386 let style = TextStyle {
387 color: Color::new(1.0, 1.0, 1.0, 1.0),
388 ..Default::default()
389 };
390 canvas.draw_text("─", Point::new(center_x - 1.0, median_y), &style);
391 canvas.draw_text("─", Point::new(center_x, median_y), &style);
392 }
393
394 let label_style = TextStyle {
396 color: Color::new(0.6, 0.6, 0.6, 1.0),
397 ..Default::default()
398 };
399 let label_x = center_x - violin.label.len() as f32 / 2.0;
400 canvas.draw_text(
401 &violin.label,
402 Point::new(label_x, self.bounds.y + self.bounds.height),
403 &label_style,
404 );
405 }
406 }
407
408 fn render_horizontal(&mut self, canvas: &mut dyn Canvas) {
409 if self.violins.is_empty() {
410 return;
411 }
412
413 let (val_min, val_max) = self.global_range();
414 let n_violins = self.violins.len();
415 let violin_height = self.bounds.height / n_violins as f32;
416
417 for (idx, violin) in self.violins.iter_mut().enumerate() {
418 if violin.densities.is_none() {
419 violin.compute_stats();
420 violin.compute_kde(self.kde_points);
421 }
422
423 let densities = violin.densities.as_ref().expect("computed above");
424 let stats = violin.stats.as_ref().expect("computed above");
425 let center_y = self.bounds.y + (idx as f32 + 0.5) * violin_height;
426 let half_height = violin_height * 0.4;
427
428 for (i, &density) in densities.iter().enumerate() {
430 let t = i as f64 / (densities.len() - 1) as f64;
431 let value = val_min + t * (val_max - val_min);
432 let x = self.bounds.x
433 + ((value - val_min) / (val_max - val_min)) as f32 * self.bounds.width;
434
435 if x < self.bounds.x || x >= self.bounds.x + self.bounds.width {
436 continue;
437 }
438
439 let height = (density * half_height as f64) as f32;
440 if height < 0.5 {
441 continue;
442 }
443
444 let color = if let Some(ref gradient) = self.gradient {
445 gradient.sample(density)
446 } else {
447 violin.color
448 };
449
450 let style = TextStyle {
451 color,
452 ..Default::default()
453 };
454
455 let chars = "▁▂▃▄▅▆▇█";
457 let char_vec: Vec<char> = chars.chars().collect();
458 let char_idx = ((height / half_height * 7.0) as usize).min(7);
459 let ch = char_vec[char_idx];
460
461 canvas.draw_text(&ch.to_string(), Point::new(x, center_y), &style);
462 }
463
464 if self.show_median {
466 let median_x = self.bounds.x
467 + ((stats.median - val_min) / (val_max - val_min)) as f32 * self.bounds.width;
468 let style = TextStyle {
469 color: Color::new(1.0, 1.0, 1.0, 1.0),
470 ..Default::default()
471 };
472 canvas.draw_text("│", Point::new(median_x, center_y), &style);
473 }
474
475 let label_style = TextStyle {
477 color: Color::new(0.6, 0.6, 0.6, 1.0),
478 ..Default::default()
479 };
480 canvas.draw_text(
481 &violin.label,
482 Point::new(self.bounds.x - violin.label.len() as f32 - 1.0, center_y),
483 &label_style,
484 );
485 }
486 }
487}
488
489impl Widget for ViolinPlot {
490 fn type_id(&self) -> TypeId {
491 TypeId::of::<Self>()
492 }
493
494 fn measure(&self, constraints: Constraints) -> Size {
495 Size::new(
496 constraints.max_width.min(60.0),
497 constraints.max_height.min(20.0),
498 )
499 }
500
501 fn layout(&mut self, bounds: Rect) -> LayoutResult {
502 self.bounds = bounds;
503 LayoutResult {
504 size: Size::new(bounds.width, bounds.height),
505 }
506 }
507
508 fn paint(&self, canvas: &mut dyn Canvas) {
509 if self.bounds.width < 5.0 || self.bounds.height < 5.0 {
510 return;
511 }
512
513 let mut mutable_self = self.clone();
515 match self.orientation {
516 ViolinOrientation::Vertical => mutable_self.render_vertical(canvas),
517 ViolinOrientation::Horizontal => mutable_self.render_horizontal(canvas),
518 }
519 }
520
521 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
522 None
523 }
524
525 fn children(&self) -> &[Box<dyn Widget>] {
526 &[]
527 }
528
529 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
530 &mut []
531 }
532}
533
534impl Brick for ViolinPlot {
535 fn brick_name(&self) -> &'static str {
536 "ViolinPlot"
537 }
538
539 fn assertions(&self) -> &[BrickAssertion] {
540 static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
541 ASSERTIONS
542 }
543
544 fn budget(&self) -> BrickBudget {
545 BrickBudget::uniform(16)
546 }
547
548 fn verify(&self) -> BrickVerification {
549 let mut passed = Vec::new();
550 let mut failed = Vec::new();
551
552 if self.bounds.width >= 5.0 && self.bounds.height >= 5.0 {
553 passed.push(BrickAssertion::max_latency_ms(16));
554 } else {
555 failed.push((
556 BrickAssertion::max_latency_ms(16),
557 "Size too small".to_string(),
558 ));
559 }
560
561 BrickVerification {
562 passed,
563 failed,
564 verification_time: Duration::from_micros(5),
565 }
566 }
567
568 fn to_html(&self) -> String {
569 String::new()
570 }
571
572 fn to_css(&self) -> String {
573 String::new()
574 }
575}
576
577#[cfg(test)]
578mod tests {
579 use super::*;
580 use crate::{CellBuffer, DirectTerminalCanvas};
581
582 #[test]
583 fn test_violin_data_creation() {
584 let data = ViolinData::new("Test", vec![1.0, 2.0, 3.0, 4.0, 5.0]);
585 assert_eq!(data.label, "Test");
586 assert_eq!(data.values.len(), 5);
587 }
588
589 #[test]
590 fn test_violin_data_with_color() {
591 let color = Color::new(0.5, 0.6, 0.7, 1.0);
592 let data = ViolinData::new("Test", vec![1.0]).with_color(color);
593 assert!((data.color.r - 0.5).abs() < 0.001);
594 }
595
596 #[test]
597 fn test_violin_stats() {
598 let mut data = ViolinData::new("Test", vec![1.0, 2.0, 3.0, 4.0, 5.0]);
599 let stats = data.stats();
600 assert_eq!(stats.min, 1.0);
601 assert_eq!(stats.max, 5.0);
602 assert_eq!(stats.median, 3.0);
603 }
604
605 #[test]
606 fn test_violin_stats_even_count() {
607 let mut data = ViolinData::new("Test", vec![1.0, 2.0, 3.0, 4.0]);
608 let stats = data.stats();
609 assert!((stats.median - 2.5).abs() < 0.001);
610 }
611
612 #[test]
613 fn test_violin_empty_stats() {
614 let mut data = ViolinData::new("Empty", vec![]);
615 let stats = data.stats();
616 assert_eq!(stats.min, 0.0);
617 assert_eq!(stats.max, 0.0);
618 }
619
620 #[test]
621 fn test_violin_kde() {
622 let mut data = ViolinData::new("Test", vec![1.0, 2.0, 3.0, 4.0, 5.0]);
623 data.compute_kde(20);
624 assert!(data.densities.is_some());
625 let densities = data.densities.as_ref().expect("computed above");
626 assert_eq!(densities.len(), 20);
627 assert!(densities.iter().all(|&d| (0.0..=1.0).contains(&d)));
629 }
630
631 #[test]
632 fn test_violin_kde_empty() {
633 let mut data = ViolinData::new("Empty", vec![]);
634 data.compute_kde(20);
635 assert!(data.densities.is_some());
636 let densities = data.densities.as_ref().expect("computed");
637 assert_eq!(densities.len(), 20);
638 assert!(densities.iter().all(|&d| d == 0.0));
639 }
640
641 #[test]
642 fn test_violin_kde_single_value() {
643 let mut data = ViolinData::new("Single", vec![5.0]);
644 data.compute_kde(10);
645 assert!(data.densities.is_some());
646 }
647
648 #[test]
649 fn test_violin_kde_same_values() {
650 let mut data = ViolinData::new("Same", vec![5.0, 5.0, 5.0, 5.0]);
651 data.compute_kde(20);
652 assert!(data.densities.is_some());
653 let densities = data.densities.as_ref().expect("computed");
654 assert!(densities.iter().all(|&d| (d - 1.0).abs() < 0.001));
656 }
657
658 #[test]
659 fn test_violin_kde_large_dataset() {
660 let values: Vec<f64> = (0..200).map(|i| i as f64 / 10.0).collect();
662 let mut data = ViolinData::new("Large", values);
663 data.compute_kde(50);
664 assert!(data.densities.is_some());
665 }
666
667 #[test]
668 fn test_violin_std_dev() {
669 let data = ViolinData::new("Test", vec![1.0, 2.0, 3.0, 4.0, 5.0]);
670 let std_dev = data.compute_std_dev();
671 assert!(std_dev > 0.0);
672 }
673
674 #[test]
675 fn test_violin_std_dev_single() {
676 let data = ViolinData::new("Single", vec![5.0]);
677 let std_dev = data.compute_std_dev();
678 assert!((std_dev - 1.0).abs() < 0.001); }
680
681 #[test]
682 fn test_violin_plot_creation() {
683 let plot = ViolinPlot::new(vec![ViolinData::new("A", vec![1.0, 2.0, 3.0])]);
684 assert_eq!(plot.violins.len(), 1);
685 }
686
687 #[test]
688 fn test_violin_plot_default() {
689 let plot = ViolinPlot::default();
690 assert!(plot.violins.is_empty());
691 }
692
693 #[test]
694 fn test_violin_plot_with_orientation() {
695 let plot = ViolinPlot::default().with_orientation(ViolinOrientation::Horizontal);
696 assert_eq!(plot.orientation, ViolinOrientation::Horizontal);
697 }
698
699 #[test]
700 fn test_violin_plot_with_gradient() {
701 let gradient = Gradient::two(
702 Color::new(1.0, 0.0, 0.0, 1.0),
703 Color::new(0.0, 0.0, 1.0, 1.0),
704 );
705 let plot = ViolinPlot::default().with_gradient(gradient);
706 assert!(plot.gradient.is_some());
707 }
708
709 #[test]
710 fn test_violin_plot_measure() {
711 let plot = ViolinPlot::default();
712 let constraints = Constraints::new(0.0, 100.0, 0.0, 50.0);
713 let size = plot.measure(constraints);
714 assert_eq!(size.width, 60.0);
715 assert_eq!(size.height, 20.0);
716 }
717
718 #[test]
719 fn test_violin_plot_layout_and_paint_vertical() {
720 let mut plot = ViolinPlot::new(vec![
721 ViolinData::new("A", vec![1.0, 2.0, 3.0, 4.0, 5.0]).with_color(Color::BLUE),
722 ViolinData::new("B", vec![2.0, 3.0, 4.0, 5.0, 6.0]).with_color(Color::RED),
723 ]);
724
725 let mut buffer = CellBuffer::new(60, 20);
726 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
727
728 let result = plot.layout(Rect::new(0.0, 0.0, 60.0, 20.0));
729 assert_eq!(result.size.width, 60.0);
730
731 plot.paint(&mut canvas);
732
733 let cells = buffer.cells();
735 let non_empty = cells.iter().filter(|c| !c.symbol.is_empty()).count();
736 assert!(non_empty > 0, "Violin plot should render some content");
737 }
738
739 #[test]
740 fn test_violin_plot_layout_and_paint_horizontal() {
741 let mut plot = ViolinPlot::new(vec![
742 ViolinData::new("A", vec![1.0, 2.0, 3.0, 4.0, 5.0]),
743 ViolinData::new("B", vec![2.0, 3.0, 4.0, 5.0, 6.0]),
744 ])
745 .with_orientation(ViolinOrientation::Horizontal);
746
747 let mut buffer = CellBuffer::new(60, 20);
748 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
749
750 plot.layout(Rect::new(0.0, 0.0, 60.0, 20.0));
751 plot.paint(&mut canvas);
752 }
753
754 #[test]
755 fn test_violin_plot_paint_with_gradient() {
756 let gradient = Gradient::two(
757 Color::new(0.2, 0.4, 0.8, 1.0),
758 Color::new(0.8, 0.4, 0.2, 1.0),
759 );
760 let mut plot = ViolinPlot::new(vec![ViolinData::new("A", vec![1.0, 2.0, 3.0, 4.0, 5.0])])
761 .with_gradient(gradient);
762
763 let mut buffer = CellBuffer::new(60, 20);
764 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
765
766 plot.layout(Rect::new(0.0, 0.0, 60.0, 20.0));
767 plot.paint(&mut canvas);
768 }
769
770 #[test]
771 fn test_violin_plot_paint_no_median() {
772 let mut plot =
773 ViolinPlot::new(vec![ViolinData::new("A", vec![1.0, 2.0, 3.0])]).with_median(false);
774
775 let mut buffer = CellBuffer::new(60, 20);
776 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
777
778 plot.layout(Rect::new(0.0, 0.0, 60.0, 20.0));
779 plot.paint(&mut canvas);
780 }
781
782 #[test]
783 fn test_violin_plot_paint_small_bounds() {
784 let mut plot = ViolinPlot::new(vec![ViolinData::new("A", vec![1.0, 2.0, 3.0])]);
785
786 let mut buffer = CellBuffer::new(3, 3);
787 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
788
789 plot.layout(Rect::new(0.0, 0.0, 3.0, 3.0));
790 plot.paint(&mut canvas);
791 }
793
794 #[test]
795 fn test_violin_plot_paint_empty() {
796 let mut plot = ViolinPlot::default();
797
798 let mut buffer = CellBuffer::new(60, 20);
799 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
800
801 plot.layout(Rect::new(0.0, 0.0, 60.0, 20.0));
802 plot.paint(&mut canvas);
803 }
804
805 #[test]
806 fn test_violin_plot_assertions() {
807 let plot = ViolinPlot::default();
808 assert!(!plot.assertions().is_empty());
809 }
810
811 #[test]
812 #[allow(clippy::field_reassign_with_default)]
813 fn test_violin_plot_verify_valid() {
814 let mut plot = ViolinPlot::default();
815 plot.bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
816 assert!(plot.verify().is_valid());
817 }
818
819 #[test]
820 #[allow(clippy::field_reassign_with_default)]
821 fn test_violin_plot_verify_invalid() {
822 let mut plot = ViolinPlot::default();
823 plot.bounds = Rect::new(0.0, 0.0, 3.0, 3.0);
824 assert!(!plot.verify().is_valid());
825 }
826
827 #[test]
828 fn test_violin_plot_children() {
829 let plot = ViolinPlot::default();
830 assert!(plot.children().is_empty());
831 }
832
833 #[test]
834 fn test_violin_plot_children_mut() {
835 let mut plot = ViolinPlot::default();
836 assert!(plot.children_mut().is_empty());
837 }
838
839 #[test]
840 fn test_violin_global_range() {
841 let plot = ViolinPlot::new(vec![
842 ViolinData::new("A", vec![1.0, 2.0]),
843 ViolinData::new("B", vec![3.0, 10.0]),
844 ]);
845 let (min, max) = plot.global_range();
846 assert!(min < 1.0); assert!(max > 10.0);
848 }
849
850 #[test]
851 fn test_violin_global_range_empty() {
852 let plot = ViolinPlot::default();
853 let (min, max) = plot.global_range();
854 assert_eq!(min, 0.0);
855 assert_eq!(max, 1.0);
856 }
857
858 #[test]
859 fn test_violin_global_range_with_nan() {
860 let plot = ViolinPlot::new(vec![ViolinData::new("A", vec![1.0, f64::NAN, 5.0])]);
861 let (min, max) = plot.global_range();
862 assert!(min.is_finite());
863 assert!(max.is_finite());
864 }
865
866 #[test]
867 fn test_violin_add_violin() {
868 let mut plot = ViolinPlot::default();
869 plot.add_violin(ViolinData::new("New", vec![1.0, 2.0]));
870 assert_eq!(plot.violins.len(), 1);
871 }
872
873 #[test]
874 fn test_violin_with_box() {
875 let plot = ViolinPlot::default().with_box(false);
876 assert!(!plot.show_box);
877 }
878
879 #[test]
880 fn test_violin_with_median() {
881 let plot = ViolinPlot::default().with_median(false);
882 assert!(!plot.show_median);
883 }
884
885 #[test]
886 fn test_violin_with_kde_points() {
887 let plot = ViolinPlot::default().with_kde_points(100);
888 assert_eq!(plot.kde_points, 100);
889 }
890
891 #[test]
892 fn test_violin_kde_points_clamped() {
893 let plot = ViolinPlot::default().with_kde_points(5);
894 assert_eq!(plot.kde_points, 10); let plot = ViolinPlot::default().with_kde_points(500);
897 assert_eq!(plot.kde_points, 200); }
899
900 #[test]
901 fn test_violin_orientation_default() {
902 let orientation = ViolinOrientation::default();
903 assert_eq!(orientation, ViolinOrientation::Vertical);
904 }
905
906 #[test]
907 fn test_violin_plot_brick_name() {
908 let plot = ViolinPlot::new(vec![]);
909 assert_eq!(plot.brick_name(), "ViolinPlot");
910 }
911
912 #[test]
913 fn test_violin_plot_budget() {
914 let plot = ViolinPlot::new(vec![]);
915 let budget = plot.budget();
916 assert!(budget.layout_ms > 0);
917 }
918
919 #[test]
920 fn test_violin_plot_to_html() {
921 let plot = ViolinPlot::new(vec![]);
922 assert!(plot.to_html().is_empty());
923 }
924
925 #[test]
926 fn test_violin_plot_to_css() {
927 let plot = ViolinPlot::new(vec![]);
928 assert!(plot.to_css().is_empty());
929 }
930
931 #[test]
932 fn test_violin_plot_type_id() {
933 let plot = ViolinPlot::new(vec![]);
934 let type_id = Widget::type_id(&plot);
935 assert_eq!(type_id, TypeId::of::<ViolinPlot>());
936 }
937
938 #[test]
939 fn test_violin_plot_event() {
940 let mut plot = ViolinPlot::new(vec![]);
941 let event = Event::Resize {
942 width: 80.0,
943 height: 24.0,
944 };
945 assert!(plot.event(&event).is_none());
946 }
947
948 #[test]
949 fn test_violin_kde_scalar_and_simd_match() {
950 let values: Vec<f64> = (0..150).map(|i| i as f64 / 10.0).collect();
952 let data = ViolinData::new("Test", values);
953
954 let x = 7.5;
955 let bandwidth = 0.5;
956
957 let scalar_result = data.kde_at_point_scalar(x, bandwidth);
958 let simd_result = data.kde_at_point_simd(x, bandwidth);
959
960 assert!((scalar_result - simd_result).abs() < 1e-10);
962 }
963
964 #[test]
965 fn test_violin_kde_simd_unaligned() {
966 let values: Vec<f64> = (0..103).map(|i| i as f64 / 10.0).collect();
968 let data = ViolinData::new("Test", values);
969
970 let result = data.kde_at_point_simd(5.0, 0.5);
971 assert!(result.is_finite());
972 assert!(result > 0.0);
973 }
974
975 #[test]
976 fn test_violin_multiple_violins_paint() {
977 let mut plot = ViolinPlot::new(vec![
978 ViolinData::new("A", vec![1.0, 2.0, 3.0]),
979 ViolinData::new("B", vec![2.0, 3.0, 4.0]),
980 ViolinData::new("C", vec![3.0, 4.0, 5.0]),
981 ]);
982
983 let mut buffer = CellBuffer::new(90, 30);
984 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
985
986 plot.layout(Rect::new(0.0, 0.0, 90.0, 30.0));
987 plot.paint(&mut canvas);
988 }
989}