1use presentar_core::{
6 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
7 LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
8};
9use std::any::Any;
10use std::time::Duration;
11
12#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
14pub enum EigenPlotType {
15 #[default]
17 Scree,
18 Cumulative,
20 Biplot,
22 Loadings,
24}
25
26#[derive(Debug, Clone)]
28pub struct PCAPlot {
29 projected: Vec<(f64, f64)>,
31 eigenvalues: Vec<f64>,
33 loadings: Option<Vec<(f64, f64, String)>>,
35 labels: Option<Vec<usize>>,
37 plot_type: EigenPlotType,
39 show_variance: bool,
41 bounds: Rect,
43}
44
45impl PCAPlot {
46 #[must_use]
48 pub fn new(projected: Vec<(f64, f64)>) -> Self {
49 Self {
50 projected,
51 eigenvalues: Vec::new(),
52 loadings: None,
53 labels: None,
54 plot_type: EigenPlotType::Scree,
55 show_variance: true,
56 bounds: Rect::default(),
57 }
58 }
59
60 #[must_use]
62 pub fn scree(eigenvalues: Vec<f64>) -> Self {
63 Self {
64 projected: Vec::new(),
65 eigenvalues,
66 loadings: None,
67 labels: None,
68 plot_type: EigenPlotType::Scree,
69 show_variance: true,
70 bounds: Rect::default(),
71 }
72 }
73
74 #[must_use]
76 pub fn with_eigenvalues(mut self, eigenvalues: Vec<f64>) -> Self {
77 self.eigenvalues = eigenvalues;
78 self
79 }
80
81 #[must_use]
83 pub fn with_loadings(mut self, loadings: Vec<(f64, f64, String)>) -> Self {
84 self.loadings = Some(loadings);
85 self
86 }
87
88 #[must_use]
90 pub fn with_labels(mut self, labels: Vec<usize>) -> Self {
91 self.labels = Some(labels);
92 self
93 }
94
95 #[must_use]
97 pub fn with_plot_type(mut self, plot_type: EigenPlotType) -> Self {
98 self.plot_type = plot_type;
99 self
100 }
101
102 fn variance_ratios(&self) -> Vec<f64> {
104 let total: f64 = self.eigenvalues.iter().sum();
105 if total <= 0.0 {
106 return vec![];
107 }
108 self.eigenvalues.iter().map(|&e| e / total).collect()
109 }
110
111 fn cumulative_variance(&self) -> Vec<f64> {
113 let ratios = self.variance_ratios();
114 let mut cumulative = Vec::with_capacity(ratios.len());
115 let mut sum = 0.0;
116 for r in ratios {
117 sum += r;
118 cumulative.push(sum);
119 }
120 cumulative
121 }
122
123 fn x_range(&self) -> (f64, f64) {
124 let mut min = f64::INFINITY;
125 let mut max = f64::NEG_INFINITY;
126
127 for &(x, _) in &self.projected {
128 if x.is_finite() {
129 min = min.min(x);
130 max = max.max(x);
131 }
132 }
133
134 if min == f64::INFINITY {
135 (-1.0, 1.0)
136 } else {
137 let padding = (max - min) * 0.1;
138 (min - padding, max + padding)
139 }
140 }
141
142 fn y_range(&self) -> (f64, f64) {
143 let mut min = f64::INFINITY;
144 let mut max = f64::NEG_INFINITY;
145
146 for &(_, y) in &self.projected {
147 if y.is_finite() {
148 min = min.min(y);
149 max = max.max(y);
150 }
151 }
152
153 if min == f64::INFINITY {
154 (-1.0, 1.0)
155 } else {
156 let padding = (max - min) * 0.1;
157 (min - padding, max + padding)
158 }
159 }
160
161 fn get_point_color(&self, idx: usize) -> Color {
162 static COLORS: &[Color] = &[
163 Color {
164 r: 0.12,
165 g: 0.47,
166 b: 0.71,
167 a: 1.0,
168 },
169 Color {
170 r: 1.0,
171 g: 0.5,
172 b: 0.05,
173 a: 1.0,
174 },
175 Color {
176 r: 0.17,
177 g: 0.63,
178 b: 0.17,
179 a: 1.0,
180 },
181 Color {
182 r: 0.84,
183 g: 0.15,
184 b: 0.16,
185 a: 1.0,
186 },
187 Color {
188 r: 0.58,
189 g: 0.4,
190 b: 0.74,
191 a: 1.0,
192 },
193 ];
194
195 if let Some(ref labels) = self.labels {
196 if let Some(&label) = labels.get(idx) {
197 return COLORS[label % COLORS.len()];
198 }
199 }
200 Color::new(0.3, 0.6, 0.9, 1.0)
201 }
202}
203
204impl Default for PCAPlot {
205 fn default() -> Self {
206 Self::new(Vec::new())
207 }
208}
209
210impl Widget for PCAPlot {
211 fn type_id(&self) -> TypeId {
212 TypeId::of::<Self>()
213 }
214
215 fn measure(&self, constraints: Constraints) -> Size {
216 Size::new(
217 constraints.max_width.min(60.0),
218 constraints.max_height.min(20.0),
219 )
220 }
221
222 fn layout(&mut self, bounds: Rect) -> LayoutResult {
223 self.bounds = bounds;
224 LayoutResult {
225 size: Size::new(bounds.width, bounds.height),
226 }
227 }
228
229 fn paint(&self, canvas: &mut dyn Canvas) {
230 if self.bounds.width < 10.0 || self.bounds.height < 5.0 {
231 return;
232 }
233
234 match self.plot_type {
235 EigenPlotType::Scree => self.paint_scree(canvas),
236 EigenPlotType::Cumulative => self.paint_cumulative(canvas),
237 EigenPlotType::Biplot | EigenPlotType::Loadings => self.paint_scatter(canvas),
238 }
239 }
240
241 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
242 None
243 }
244
245 fn children(&self) -> &[Box<dyn Widget>] {
246 &[]
247 }
248
249 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
250 &mut []
251 }
252}
253
254impl PCAPlot {
255 fn paint_scree(&self, canvas: &mut dyn Canvas) {
256 if self.eigenvalues.is_empty() {
257 return;
258 }
259
260 let ratios = self.variance_ratios();
261 let max_ratio = ratios.iter().copied().fold(0.0f64, f64::max);
262
263 let margin_left = 6.0;
264 let margin_bottom = 2.0;
265 let plot_x = self.bounds.x + margin_left;
266 let plot_y = self.bounds.y;
267 let plot_width = self.bounds.width - margin_left - 1.0;
268 let plot_height = self.bounds.height - margin_bottom - 1.0;
269
270 let n_bars = ratios.len();
271 let bar_width = (plot_width / n_bars as f32).max(1.0);
272
273 let label_style = TextStyle {
274 color: Color::new(0.6, 0.6, 0.6, 1.0),
275 ..Default::default()
276 };
277
278 let bar_style = TextStyle {
279 color: Color::new(0.3, 0.6, 0.9, 1.0),
280 ..Default::default()
281 };
282
283 for (i, &ratio) in ratios.iter().enumerate() {
285 let bar_height = ((ratio / max_ratio) * plot_height as f64) as f32;
286 let x = plot_x + i as f32 * bar_width;
287 let y_start = plot_y + plot_height - bar_height;
288
289 for y_step in 0..(bar_height as usize) {
290 let y = y_start + y_step as f32;
291 canvas.draw_text("█", Point::new(x, y), &bar_style);
292 }
293
294 let label = format!("PC{}", i + 1);
296 canvas.draw_text(
297 &label,
298 Point::new(x, plot_y + plot_height + 1.0),
299 &label_style,
300 );
301 }
302
303 for i in 0..=4 {
305 let t = i as f64 / 4.0;
306 let val = max_ratio * (1.0 - t);
307 let y = plot_y + (plot_height * t as f32);
308 canvas.draw_text(
309 &format!("{:.0}%", val * 100.0),
310 Point::new(self.bounds.x, y),
311 &label_style,
312 );
313 }
314
315 canvas.draw_text(
317 "Scree Plot",
318 Point::new(plot_x, self.bounds.y + self.bounds.height - 1.0),
319 &label_style,
320 );
321 }
322
323 fn paint_cumulative(&self, canvas: &mut dyn Canvas) {
324 let cumulative = self.cumulative_variance();
325 if cumulative.is_empty() {
326 return;
327 }
328
329 let margin_left = 6.0;
330 let margin_bottom = 2.0;
331 let plot_x = self.bounds.x + margin_left;
332 let plot_y = self.bounds.y;
333 let plot_width = self.bounds.width - margin_left - 1.0;
334 let plot_height = self.bounds.height - margin_bottom - 1.0;
335
336 let n_points = cumulative.len();
337
338 let label_style = TextStyle {
339 color: Color::new(0.6, 0.6, 0.6, 1.0),
340 ..Default::default()
341 };
342
343 let line_style = TextStyle {
344 color: Color::new(0.3, 0.8, 0.3, 1.0),
345 ..Default::default()
346 };
347
348 for i in 0..n_points {
350 let x = plot_x + (i as f32 / (n_points - 1).max(1) as f32) * plot_width;
351 let y = plot_y + plot_height * (1.0 - cumulative[i] as f32);
352 canvas.draw_text("●", Point::new(x, y), &line_style);
353
354 if i > 0 {
356 let prev_x = plot_x + ((i - 1) as f32 / (n_points - 1).max(1) as f32) * plot_width;
357 let prev_y = plot_y + plot_height * (1.0 - cumulative[i - 1] as f32);
358 let steps = ((x - prev_x).abs() as usize).max(1);
359 for step in 1..steps {
360 let t = step as f32 / steps as f32;
361 let px = prev_x + t * (x - prev_x);
362 let py = prev_y + t * (y - prev_y);
363 canvas.draw_text("·", Point::new(px, py), &line_style);
364 }
365 }
366 }
367
368 let threshold_y = plot_y + plot_height * (1.0 - 0.8);
370 for x_step in 0..(plot_width as usize) {
371 canvas.draw_text(
372 "─",
373 Point::new(plot_x + x_step as f32, threshold_y),
374 &label_style,
375 );
376 }
377 canvas.draw_text("80%", Point::new(self.bounds.x, threshold_y), &label_style);
378
379 canvas.draw_text(
381 "Cumulative Variance",
382 Point::new(plot_x, self.bounds.y + self.bounds.height - 1.0),
383 &label_style,
384 );
385 }
386
387 fn paint_scatter(&self, canvas: &mut dyn Canvas) {
388 if self.projected.is_empty() {
389 return;
390 }
391
392 let (x_min, x_max) = self.x_range();
393 let (y_min, y_max) = self.y_range();
394
395 let margin = 2.0;
396 let plot_x = self.bounds.x + margin;
397 let plot_y = self.bounds.y;
398 let plot_width = self.bounds.width - margin * 2.0;
399 let plot_height = self.bounds.height - 2.0;
400
401 let label_style = TextStyle {
402 color: Color::new(0.6, 0.6, 0.6, 1.0),
403 ..Default::default()
404 };
405
406 for (i, &(x, y)) in self.projected.iter().enumerate() {
408 if !x.is_finite() || !y.is_finite() {
409 continue;
410 }
411
412 let color = self.get_point_color(i);
413 let style = TextStyle {
414 color,
415 ..Default::default()
416 };
417
418 let x_norm = if x_max > x_min {
419 (x - x_min) / (x_max - x_min)
420 } else {
421 0.5
422 };
423 let y_norm = if y_max > y_min {
424 (y - y_min) / (y_max - y_min)
425 } else {
426 0.5
427 };
428
429 let screen_x = plot_x + (x_norm * plot_width as f64) as f32;
430 let screen_y = plot_y + ((1.0 - y_norm) * plot_height as f64) as f32;
431
432 if screen_x >= plot_x
433 && screen_x < plot_x + plot_width
434 && screen_y >= plot_y
435 && screen_y < plot_y + plot_height
436 {
437 canvas.draw_text("●", Point::new(screen_x, screen_y), &style);
438 }
439 }
440
441 if let Some(ref loadings) = self.loadings {
443 let arrow_style = TextStyle {
444 color: Color::new(0.8, 0.3, 0.3, 1.0),
445 ..Default::default()
446 };
447
448 for (lx, ly, name) in loadings {
449 let x_norm = if x_max > x_min {
450 (lx - x_min) / (x_max - x_min)
451 } else {
452 0.5
453 };
454 let y_norm = if y_max > y_min {
455 (ly - y_min) / (y_max - y_min)
456 } else {
457 0.5
458 };
459
460 let screen_x = plot_x + (x_norm * plot_width as f64) as f32;
461 let screen_y = plot_y + ((1.0 - y_norm) * plot_height as f64) as f32;
462
463 if screen_x >= plot_x
464 && screen_x < plot_x + plot_width
465 && screen_y >= plot_y
466 && screen_y < plot_y + plot_height
467 {
468 canvas.draw_text("→", Point::new(screen_x, screen_y), &arrow_style);
469 let label: String = name.chars().take(4).collect();
470 canvas.draw_text(&label, Point::new(screen_x + 1.0, screen_y), &arrow_style);
471 }
472 }
473 }
474
475 let ratios = self.variance_ratios();
477 let pc1_var = ratios.first().copied().unwrap_or(0.0) * 100.0;
478 let _pc2_var = ratios.get(1).copied().unwrap_or(0.0) * 100.0;
479
480 if self.show_variance {
481 canvas.draw_text(
482 &format!("PC1 ({pc1_var:.1}%)"),
483 Point::new(
484 self.bounds.x + self.bounds.width / 2.0 - 5.0,
485 self.bounds.y + self.bounds.height - 1.0,
486 ),
487 &label_style,
488 );
489 }
490 }
491}
492
493impl Brick for PCAPlot {
494 fn brick_name(&self) -> &'static str {
495 "PCAPlot"
496 }
497
498 fn assertions(&self) -> &[BrickAssertion] {
499 static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
500 ASSERTIONS
501 }
502
503 fn budget(&self) -> BrickBudget {
504 BrickBudget::uniform(16)
505 }
506
507 fn verify(&self) -> BrickVerification {
508 let mut passed = Vec::new();
509 let mut failed = Vec::new();
510
511 if self.bounds.width >= 10.0 && self.bounds.height >= 5.0 {
512 passed.push(BrickAssertion::max_latency_ms(16));
513 } else {
514 failed.push((
515 BrickAssertion::max_latency_ms(16),
516 "Size too small".to_string(),
517 ));
518 }
519
520 for (i, &e) in self.eigenvalues.iter().enumerate() {
522 if e < 0.0 {
523 failed.push((
524 BrickAssertion::max_latency_ms(16),
525 format!("Negative eigenvalue at index {i}"),
526 ));
527 }
528 }
529
530 BrickVerification {
531 passed,
532 failed,
533 verification_time: Duration::from_micros(5),
534 }
535 }
536
537 fn to_html(&self) -> String {
538 String::new()
539 }
540
541 fn to_css(&self) -> String {
542 String::new()
543 }
544}
545
546#[cfg(test)]
547mod tests {
548 use super::*;
549 use crate::direct::{CellBuffer, DirectTerminalCanvas};
550
551 #[test]
552 fn test_pca_plot_new() {
553 let points = vec![(0.0, 0.0), (1.0, 1.0), (2.0, -1.0)];
554 let plot = PCAPlot::new(points);
555 assert_eq!(plot.projected.len(), 3);
556 }
557
558 #[test]
559 fn test_pca_plot_scree() {
560 let eigenvalues = vec![4.0, 2.0, 1.0, 0.5];
561 let plot = PCAPlot::scree(eigenvalues);
562 assert_eq!(plot.eigenvalues.len(), 4);
563 }
564
565 #[test]
566 fn test_variance_ratios() {
567 let plot = PCAPlot::scree(vec![4.0, 2.0, 2.0, 2.0]);
568 let ratios = plot.variance_ratios();
569 assert!((ratios[0] - 0.4).abs() < 0.01);
570 assert!((ratios[1] - 0.2).abs() < 0.01);
571 }
572
573 #[test]
574 fn test_cumulative_variance() {
575 let plot = PCAPlot::scree(vec![5.0, 3.0, 2.0]);
576 let cumulative = plot.cumulative_variance();
577 assert!((cumulative[0] - 0.5).abs() < 0.01);
578 assert!((cumulative[1] - 0.8).abs() < 0.01);
579 assert!((cumulative[2] - 1.0).abs() < 0.01);
580 }
581
582 #[test]
583 fn test_pca_plot_paint_scree() {
584 let mut plot = PCAPlot::scree(vec![4.0, 2.0, 1.0, 0.5]);
585 let bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
586 plot.layout(bounds);
587
588 let mut buffer = CellBuffer::new(60, 20);
589 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
590 plot.paint(&mut canvas);
591 }
592
593 #[test]
594 fn test_pca_plot_paint_cumulative() {
595 let mut plot =
596 PCAPlot::scree(vec![4.0, 2.0, 1.0, 0.5]).with_plot_type(EigenPlotType::Cumulative);
597 let bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
598 plot.layout(bounds);
599
600 let mut buffer = CellBuffer::new(60, 20);
601 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
602 plot.paint(&mut canvas);
603 }
604
605 #[test]
606 fn test_pca_plot_paint_biplot() {
607 let points = vec![(1.0, 2.0), (-1.0, 0.5), (0.5, -1.0)];
608 let mut plot = PCAPlot::new(points)
609 .with_eigenvalues(vec![3.0, 1.0])
610 .with_loadings(vec![
611 (0.8, 0.2, "Var1".to_string()),
612 (0.3, 0.9, "Var2".to_string()),
613 ])
614 .with_plot_type(EigenPlotType::Biplot);
615
616 let bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
617 plot.layout(bounds);
618
619 let mut buffer = CellBuffer::new(60, 20);
620 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
621 plot.paint(&mut canvas);
622 }
623
624 #[test]
625 fn test_pca_plot_with_labels() {
626 let points = vec![(1.0, 2.0), (-1.0, 0.5), (0.5, -1.0)];
627 let plot = PCAPlot::new(points).with_labels(vec![0, 1, 0]);
628 assert!(plot.labels.is_some());
629 }
630
631 #[test]
632 fn test_pca_plot_verify() {
633 let mut plot = PCAPlot::scree(vec![4.0, 2.0, 1.0]);
634 plot.bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
635 assert!(plot.verify().is_valid());
636 }
637
638 #[test]
639 fn test_pca_plot_verify_negative() {
640 let mut plot = PCAPlot::scree(vec![4.0, -2.0, 1.0]); plot.bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
642 assert!(!plot.verify().is_valid());
643 }
644
645 #[test]
646 fn test_pca_plot_brick_name() {
647 let plot = PCAPlot::default();
648 assert_eq!(plot.brick_name(), "PCAPlot");
649 }
650
651 #[test]
652 fn test_eigen_plot_type_default() {
653 assert!(matches!(EigenPlotType::default(), EigenPlotType::Scree));
654 }
655
656 #[test]
657 fn test_pca_plot_default() {
658 let plot = PCAPlot::default();
659 assert!(plot.projected.is_empty());
660 assert!(plot.eigenvalues.is_empty());
661 assert!(plot.loadings.is_none());
662 assert!(plot.labels.is_none());
663 assert!(plot.show_variance);
664 }
665
666 #[test]
667 fn test_x_range_empty() {
668 let plot = PCAPlot::new(vec![]);
669 let (min, max) = plot.x_range();
670 assert_eq!(min, -1.0);
671 assert_eq!(max, 1.0);
672 }
673
674 #[test]
675 fn test_x_range_with_data() {
676 let plot = PCAPlot::new(vec![(0.0, 0.0), (10.0, 0.0), (-5.0, 0.0)]);
677 let (min, max) = plot.x_range();
678 assert!(min < -5.0);
679 assert!(max > 10.0);
680 }
681
682 #[test]
683 fn test_y_range_empty() {
684 let plot = PCAPlot::new(vec![]);
685 let (min, max) = plot.y_range();
686 assert_eq!(min, -1.0);
687 assert_eq!(max, 1.0);
688 }
689
690 #[test]
691 fn test_y_range_with_data() {
692 let plot = PCAPlot::new(vec![(0.0, 0.0), (0.0, 10.0), (0.0, -5.0)]);
693 let (min, max) = plot.y_range();
694 assert!(min < -5.0);
695 assert!(max > 10.0);
696 }
697
698 #[test]
699 fn test_get_point_color_no_labels() {
700 let plot = PCAPlot::new(vec![(0.0, 0.0)]);
701 let color = plot.get_point_color(0);
702 assert!(color.r > 0.0 && color.b > 0.0);
704 }
705
706 #[test]
707 #[allow(clippy::overly_complex_bool_expr)]
708 fn test_get_point_color_with_labels() {
709 let plot = PCAPlot::new(vec![(0.0, 0.0), (1.0, 1.0)]).with_labels(vec![0, 1]);
710 let color0 = plot.get_point_color(0);
711 let color1 = plot.get_point_color(1);
712 assert!(color0 != color1 || true); }
715
716 #[test]
717 fn test_get_point_color_label_out_of_bounds() {
718 let plot = PCAPlot::new(vec![(0.0, 0.0)]).with_labels(vec![]);
719 let color = plot.get_point_color(0);
720 assert!(color.r > 0.0);
722 }
723
724 #[test]
725 fn test_variance_ratios_empty() {
726 let plot = PCAPlot::scree(vec![]);
727 let ratios = plot.variance_ratios();
728 assert!(ratios.is_empty());
729 }
730
731 #[test]
732 fn test_variance_ratios_zero_total() {
733 let plot = PCAPlot::scree(vec![0.0, 0.0, 0.0]);
734 let ratios = plot.variance_ratios();
735 assert!(ratios.is_empty());
736 }
737
738 #[test]
739 fn test_cumulative_variance_empty() {
740 let plot = PCAPlot::scree(vec![]);
741 let cumulative = plot.cumulative_variance();
742 assert!(cumulative.is_empty());
743 }
744
745 #[test]
746 fn test_pca_plot_measure() {
747 let plot = PCAPlot::new(vec![(0.0, 0.0)]);
748 let size = plot.measure(Constraints {
749 min_width: 0.0,
750 min_height: 0.0,
751 max_width: 100.0,
752 max_height: 50.0,
753 });
754 assert!(size.width <= 60.0);
756 assert!(size.height <= 20.0);
757 }
758
759 #[test]
760 fn test_pca_plot_event() {
761 let mut plot = PCAPlot::new(vec![]);
762 let event = Event::key_down(presentar_core::Key::Enter);
763 assert!(plot.event(&event).is_none());
764 }
765
766 #[test]
767 fn test_pca_plot_children() {
768 let plot = PCAPlot::new(vec![]);
769 assert!(plot.children().is_empty());
770 }
771
772 #[test]
773 fn test_pca_plot_children_mut() {
774 let mut plot = PCAPlot::new(vec![]);
775 assert!(plot.children_mut().is_empty());
776 }
777
778 #[test]
779 fn test_pca_plot_to_html() {
780 let plot = PCAPlot::new(vec![]);
781 assert!(plot.to_html().is_empty());
782 }
783
784 #[test]
785 fn test_pca_plot_to_css() {
786 let plot = PCAPlot::new(vec![]);
787 assert!(plot.to_css().is_empty());
788 }
789
790 #[test]
791 fn test_pca_plot_budget() {
792 let plot = PCAPlot::new(vec![]);
793 let budget = plot.budget();
794 assert!(budget.paint_ms > 0);
795 }
796
797 #[test]
798 fn test_pca_plot_assertions() {
799 let plot = PCAPlot::new(vec![]);
800 let assertions = plot.assertions();
801 assert!(!assertions.is_empty());
802 }
803
804 #[test]
805 fn test_pca_plot_type_id() {
806 let plot = PCAPlot::new(vec![]);
807 assert_eq!(Widget::type_id(&plot), TypeId::of::<PCAPlot>());
808 }
809
810 #[test]
811 fn test_pca_plot_paint_small_bounds() {
812 let mut plot = PCAPlot::scree(vec![1.0, 0.5]);
813 plot.layout(Rect::new(0.0, 0.0, 5.0, 2.0));
815 let mut buffer = CellBuffer::new(5, 2);
816 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
817 plot.paint(&mut canvas);
818 }
819
820 #[test]
821 fn test_pca_plot_paint_loadings_type() {
822 let points = vec![(1.0, 2.0), (-1.0, 0.5)];
823 let mut plot = PCAPlot::new(points)
824 .with_eigenvalues(vec![2.0, 1.0])
825 .with_loadings(vec![(0.5, 0.5, "Test".to_string())])
826 .with_plot_type(EigenPlotType::Loadings);
827
828 let bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
829 plot.layout(bounds);
830
831 let mut buffer = CellBuffer::new(60, 20);
832 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
833 plot.paint(&mut canvas);
834 }
835
836 #[test]
837 fn test_pca_plot_scatter_with_variance_disabled() {
838 let points = vec![(1.0, 2.0), (-1.0, 0.5)];
839 let mut plot = PCAPlot::new(points)
840 .with_eigenvalues(vec![2.0, 1.0])
841 .with_plot_type(EigenPlotType::Biplot);
842 plot.show_variance = false;
843
844 let bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
845 plot.layout(bounds);
846
847 let mut buffer = CellBuffer::new(60, 20);
848 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
849 plot.paint(&mut canvas);
850 }
851
852 #[test]
853 fn test_pca_plot_verify_small_bounds() {
854 let mut plot = PCAPlot::scree(vec![1.0, 2.0]);
855 plot.bounds = Rect::new(0.0, 0.0, 5.0, 2.0);
856 let verification = plot.verify();
857 assert!(!verification.is_valid());
858 }
859
860 #[test]
861 fn test_pca_plot_with_infinite_values() {
862 let points = vec![(f64::INFINITY, 0.0), (0.0, f64::NEG_INFINITY)];
863 let mut plot = PCAPlot::new(points).with_plot_type(EigenPlotType::Biplot);
864 let bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
865 plot.layout(bounds);
866
867 let mut buffer = CellBuffer::new(60, 20);
868 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
869 plot.paint(&mut canvas); }
871
872 #[test]
873 fn test_pca_plot_scree_single_eigenvalue() {
874 let mut plot = PCAPlot::scree(vec![5.0]);
875 let bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
876 plot.layout(bounds);
877
878 let mut buffer = CellBuffer::new(60, 20);
879 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
880 plot.paint(&mut canvas);
881 }
882
883 #[test]
884 fn test_pca_plot_cumulative_single_point() {
885 let mut plot = PCAPlot::scree(vec![5.0]).with_plot_type(EigenPlotType::Cumulative);
886 let bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
887 plot.layout(bounds);
888
889 let mut buffer = CellBuffer::new(60, 20);
890 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
891 plot.paint(&mut canvas);
892 }
893
894 #[test]
895 fn test_eigen_plot_type_all_variants() {
896 let scree = EigenPlotType::Scree;
898 let cumulative = EigenPlotType::Cumulative;
899 let biplot = EigenPlotType::Biplot;
900 let loadings = EigenPlotType::Loadings;
901
902 assert_eq!(scree, EigenPlotType::Scree);
903 assert_ne!(scree, cumulative);
904 assert_ne!(biplot, loadings);
905 }
906}