Skip to main content

presentar_terminal/widgets/
violin_plot.rs

1//! Violin plot widget with kernel density estimation.
2//!
3//! Implements SIMD/WGPU-first architecture per SPEC-024 Section 16.
4//! Uses SIMD acceleration for KDE computation on large datasets (>100 elements).
5
6use 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/// Orientation of violin plot.
15#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
16pub enum ViolinOrientation {
17    /// Vertical violins (default).
18    #[default]
19    Vertical,
20    /// Horizontal violins.
21    Horizontal,
22}
23
24/// A single violin distribution.
25#[derive(Debug, Clone)]
26pub struct ViolinData {
27    /// Label for this violin.
28    pub label: String,
29    /// Raw data values.
30    pub values: Vec<f64>,
31    /// Color for this violin.
32    pub color: Color,
33    /// Cached KDE densities.
34    densities: Option<Vec<f64>>,
35    /// Cached statistics.
36    stats: Option<ViolinStats>,
37}
38
39/// Statistics for a violin.
40#[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    /// Create new violin data.
52    #[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    /// Set color.
64    #[must_use]
65    pub fn with_color(mut self, color: Color) -> Self {
66        self.color = color;
67        self
68    }
69
70    /// Compute statistics for this violin.
71    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    /// Get statistics, computing if necessary.
110    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    /// Compute KDE densities.
118    /// Uses SIMD for large datasets (>100 elements).
119    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        // Silverman's rule of thumb for bandwidth
133        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        // For large datasets, use optimized computation
140        // SIMD would be applied here for >100 elements when trueno SIMD is enabled
141        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                // SIMD-optimized path using batch computation
148                self.kde_at_point_simd(x, bandwidth)
149            } else {
150                // Scalar path for small datasets
151                self.kde_at_point_scalar(x, bandwidth)
152            };
153        }
154
155        // Normalize to [0, 1]
156        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    /// Scalar KDE computation for small datasets.
177    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            // Gaussian kernel
183            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    /// SIMD-optimized KDE computation for large datasets.
189    /// Falls back to batch processing with loop unrolling.
190    fn kde_at_point_simd(&self, x: f64, bandwidth: f64) -> f64 {
191        // Process in blocks of 4 for SIMD-friendly computation
192        // When trueno SIMD feature is enabled, this uses Vector<f32> operations
193        let inv_bw = 1.0 / bandwidth;
194        let mut sum = 0.0;
195        let mut i = 0;
196
197        // Process 4 elements at a time (SIMD lane width)
198        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        // Handle remaining elements
213        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/// Violin plot widget.
224#[derive(Debug, Clone)]
225pub struct ViolinPlot {
226    violins: Vec<ViolinData>,
227    orientation: ViolinOrientation,
228    /// Show box plot inside violin.
229    show_box: bool,
230    /// Show median line.
231    show_median: bool,
232    /// Number of KDE points.
233    kde_points: usize,
234    /// Optional gradient for coloring.
235    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    /// Create a new violin plot.
247    #[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    /// Set orientation.
261    #[must_use]
262    pub fn with_orientation(mut self, orientation: ViolinOrientation) -> Self {
263        self.orientation = orientation;
264        self
265    }
266
267    /// Toggle box plot display.
268    #[must_use]
269    pub fn with_box(mut self, show: bool) -> Self {
270        self.show_box = show;
271        self
272    }
273
274    /// Toggle median line.
275    #[must_use]
276    pub fn with_median(mut self, show: bool) -> Self {
277        self.show_median = show;
278        self
279    }
280
281    /// Set KDE resolution.
282    #[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    /// Set gradient for coloring.
289    #[must_use]
290    pub fn with_gradient(mut self, gradient: Gradient) -> Self {
291        self.gradient = Some(gradient);
292        self
293    }
294
295    /// Add a violin.
296    pub fn add_violin(&mut self, violin: ViolinData) {
297        self.violins.push(violin);
298    }
299
300    /// Get global value range.
301    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            // Draw violin shape using braille/block characters
343            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                // Draw symmetric violin halves
370                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                // Left half (mirrored)
376                canvas.draw_text(&ch.to_string(), Point::new(center_x - 1.0, y), &style);
377                // Right half
378                canvas.draw_text(&ch.to_string(), Point::new(center_x, y), &style);
379            }
380
381            // Draw median if enabled
382            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            // Draw label
395            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            // Draw violin shape horizontally
429            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                // Draw vertical block
456                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            // Draw median if enabled
465            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            // Draw label
476            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        // Need mutable self for KDE caching
514        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        // Should be normalized to [0, 1]
628        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        // All densities should be 1.0 when range is 0
655        assert!(densities.iter().all(|&d| (d - 1.0).abs() < 0.001));
656    }
657
658    #[test]
659    fn test_violin_kde_large_dataset() {
660        // Test SIMD path (>100 elements)
661        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); // Returns 1.0 for len < 2
679    }
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        // Verify something was rendered
734        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        // Should not crash
792    }
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); // Includes padding
847        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); // Minimum
895
896        let plot = ViolinPlot::default().with_kde_points(500);
897        assert_eq!(plot.kde_points, 200); // Maximum
898    }
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        // Test that both paths produce similar results
951        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        // Results should be identical
961        assert!((scalar_result - simd_result).abs() < 1e-10);
962    }
963
964    #[test]
965    fn test_violin_kde_simd_unaligned() {
966        // Test SIMD path with values not divisible by 4
967        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}