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)]
14pub enum ClusterAlgorithm {
15 KMeans { k: usize },
16 DBSCAN { eps: f64, min_samples: usize },
17 Hierarchical { n_clusters: usize },
18 HDBSCAN { min_cluster_size: usize },
19}
20
21impl Default for ClusterAlgorithm {
22 fn default() -> Self {
23 Self::KMeans { k: 3 }
24 }
25}
26
27#[derive(Debug, Clone)]
29pub struct ClusterPlot {
30 points: Vec<(f64, f64)>,
32 labels: Vec<i32>,
34 centroids: Vec<(f64, f64)>,
36 algorithm: ClusterAlgorithm,
38 show_centroids: bool,
40 colors: Vec<Color>,
42 bounds: Rect,
44}
45
46impl ClusterPlot {
47 #[must_use]
49 pub fn new(points: Vec<(f64, f64)>, labels: Vec<i32>) -> Self {
50 let colors = Self::default_colors();
51 Self {
52 points,
53 labels,
54 centroids: Vec::new(),
55 algorithm: ClusterAlgorithm::default(),
56 show_centroids: true,
57 colors,
58 bounds: Rect::default(),
59 }
60 }
61
62 #[must_use]
64 pub fn with_centroids(mut self, centroids: Vec<(f64, f64)>) -> Self {
65 self.centroids = centroids;
66 self
67 }
68
69 #[must_use]
71 pub fn with_algorithm(mut self, algorithm: ClusterAlgorithm) -> Self {
72 self.algorithm = algorithm;
73 self
74 }
75
76 #[must_use]
78 pub fn with_show_centroids(mut self, show: bool) -> Self {
79 self.show_centroids = show;
80 self
81 }
82
83 #[must_use]
85 pub fn with_colors(mut self, colors: Vec<Color>) -> Self {
86 self.colors = colors;
87 self
88 }
89
90 fn default_colors() -> Vec<Color> {
91 vec![
92 Color::new(0.12, 0.47, 0.71, 1.0), Color::new(1.0, 0.5, 0.05, 1.0), Color::new(0.17, 0.63, 0.17, 1.0), Color::new(0.84, 0.15, 0.16, 1.0), Color::new(0.58, 0.4, 0.74, 1.0), Color::new(0.55, 0.34, 0.29, 1.0), Color::new(0.89, 0.47, 0.76, 1.0), Color::new(0.5, 0.5, 0.5, 1.0), Color::new(0.74, 0.74, 0.13, 1.0), Color::new(0.09, 0.75, 0.81, 1.0), ]
103 }
104
105 fn get_cluster_color(&self, label: i32) -> Color {
106 if label < 0 {
107 Color::new(0.3, 0.3, 0.3, 0.5)
109 } else {
110 self.colors[label as usize % self.colors.len()]
111 }
112 }
113
114 fn x_range(&self) -> (f64, f64) {
115 let mut x_min = f64::INFINITY;
116 let mut x_max = f64::NEG_INFINITY;
117
118 for &(x, _) in &self.points {
119 if x.is_finite() {
120 x_min = x_min.min(x);
121 x_max = x_max.max(x);
122 }
123 }
124
125 if x_min == f64::INFINITY {
126 (0.0, 1.0)
127 } else {
128 let padding = (x_max - x_min) * 0.1;
129 (x_min - padding, x_max + padding)
130 }
131 }
132
133 fn y_range(&self) -> (f64, f64) {
134 let mut y_min = f64::INFINITY;
135 let mut y_max = f64::NEG_INFINITY;
136
137 for &(_, y) in &self.points {
138 if y.is_finite() {
139 y_min = y_min.min(y);
140 y_max = y_max.max(y);
141 }
142 }
143
144 if y_min == f64::INFINITY {
145 (0.0, 1.0)
146 } else {
147 let padding = (y_max - y_min) * 0.1;
148 (y_min - padding, y_max + padding)
149 }
150 }
151
152 #[must_use]
154 pub fn cluster_count(&self) -> usize {
155 let mut unique: Vec<i32> = self.labels.iter().filter(|&&l| l >= 0).copied().collect();
156 unique.sort_unstable();
157 unique.dedup();
158 unique.len()
159 }
160}
161
162impl Default for ClusterPlot {
163 fn default() -> Self {
164 Self::new(Vec::new(), Vec::new())
165 }
166}
167
168impl Widget for ClusterPlot {
169 fn type_id(&self) -> TypeId {
170 TypeId::of::<Self>()
171 }
172
173 fn measure(&self, constraints: Constraints) -> Size {
174 Size::new(
175 constraints.max_width.min(60.0),
176 constraints.max_height.min(20.0),
177 )
178 }
179
180 fn layout(&mut self, bounds: Rect) -> LayoutResult {
181 self.bounds = bounds;
182 LayoutResult {
183 size: Size::new(bounds.width, bounds.height),
184 }
185 }
186
187 fn paint(&self, canvas: &mut dyn Canvas) {
188 if self.bounds.width < 10.0 || self.bounds.height < 5.0 {
189 return;
190 }
191
192 let (x_min, x_max) = self.x_range();
193 let (y_min, y_max) = self.y_range();
194
195 let margin = 2.0;
196 let plot_x = self.bounds.x + margin;
197 let plot_y = self.bounds.y;
198 let plot_width = self.bounds.width - margin * 2.0;
199 let plot_height = self.bounds.height - 1.0;
200
201 if plot_width <= 0.0 || plot_height <= 0.0 {
202 return;
203 }
204
205 for (i, &(x, y)) in self.points.iter().enumerate() {
207 if !x.is_finite() || !y.is_finite() {
208 continue;
209 }
210
211 let label = self.labels.get(i).copied().unwrap_or(-1);
212 let color = self.get_cluster_color(label);
213
214 let x_norm = if x_max > x_min {
215 (x - x_min) / (x_max - x_min)
216 } else {
217 0.5
218 };
219 let y_norm = if y_max > y_min {
220 (y - y_min) / (y_max - y_min)
221 } else {
222 0.5
223 };
224
225 let screen_x = plot_x + (x_norm * plot_width as f64) as f32;
226 let screen_y = plot_y + ((1.0 - y_norm) * plot_height as f64) as f32;
227
228 if screen_x >= plot_x
229 && screen_x < plot_x + plot_width
230 && screen_y >= plot_y
231 && screen_y < plot_y + plot_height
232 {
233 let marker = if label < 0 { '·' } else { '●' };
234 let style = TextStyle {
235 color,
236 ..Default::default()
237 };
238 canvas.draw_text(&marker.to_string(), Point::new(screen_x, screen_y), &style);
239 }
240 }
241
242 if self.show_centroids {
244 for (i, &(cx, cy)) in self.centroids.iter().enumerate() {
245 if !cx.is_finite() || !cy.is_finite() {
246 continue;
247 }
248
249 #[allow(clippy::cast_possible_wrap)]
250 let color = self.get_cluster_color(i as i32);
251
252 let x_norm = if x_max > x_min {
253 (cx - x_min) / (x_max - x_min)
254 } else {
255 0.5
256 };
257 let y_norm = if y_max > y_min {
258 (cy - y_min) / (y_max - y_min)
259 } else {
260 0.5
261 };
262
263 let screen_x = plot_x + (x_norm * plot_width as f64) as f32;
264 let screen_y = plot_y + ((1.0 - y_norm) * plot_height as f64) as f32;
265
266 if screen_x >= plot_x
267 && screen_x < plot_x + plot_width
268 && screen_y >= plot_y
269 && screen_y < plot_y + plot_height
270 {
271 let style = TextStyle {
272 color,
273 ..Default::default()
274 };
275 canvas.draw_text("✚", Point::new(screen_x, screen_y), &style);
276 }
277 }
278 }
279
280 let legend_y = self.bounds.y + self.bounds.height - 1.0;
282 let label_style = TextStyle {
283 color: Color::new(0.6, 0.6, 0.6, 1.0),
284 ..Default::default()
285 };
286
287 let algo_name = match &self.algorithm {
288 ClusterAlgorithm::KMeans { k } => format!("K-Means (k={k})"),
289 ClusterAlgorithm::DBSCAN { eps, min_samples } => {
290 format!("DBSCAN (eps={eps:.2}, min={min_samples})")
291 }
292 ClusterAlgorithm::Hierarchical { n_clusters } => {
293 format!("Hierarchical (n={n_clusters})")
294 }
295 ClusterAlgorithm::HDBSCAN { min_cluster_size } => {
296 format!("HDBSCAN (min={min_cluster_size})")
297 }
298 };
299
300 canvas.draw_text(
301 &format!("{} | {} clusters", algo_name, self.cluster_count()),
302 Point::new(self.bounds.x, legend_y),
303 &label_style,
304 );
305 }
306
307 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
308 None
309 }
310
311 fn children(&self) -> &[Box<dyn Widget>] {
312 &[]
313 }
314
315 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
316 &mut []
317 }
318}
319
320impl Brick for ClusterPlot {
321 fn brick_name(&self) -> &'static str {
322 "ClusterPlot"
323 }
324
325 fn assertions(&self) -> &[BrickAssertion] {
326 static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
327 ASSERTIONS
328 }
329
330 fn budget(&self) -> BrickBudget {
331 BrickBudget::uniform(16)
332 }
333
334 fn verify(&self) -> BrickVerification {
335 let mut passed = Vec::new();
336 let mut failed = Vec::new();
337
338 if self.bounds.width >= 10.0 && self.bounds.height >= 5.0 {
339 passed.push(BrickAssertion::max_latency_ms(16));
340 } else {
341 failed.push((
342 BrickAssertion::max_latency_ms(16),
343 "Size too small".to_string(),
344 ));
345 }
346
347 if !self.points.is_empty() && self.labels.len() != self.points.len() {
349 failed.push((
350 BrickAssertion::max_latency_ms(16),
351 "Labels length mismatch".to_string(),
352 ));
353 }
354
355 BrickVerification {
356 passed,
357 failed,
358 verification_time: Duration::from_micros(5),
359 }
360 }
361
362 fn to_html(&self) -> String {
363 String::new()
364 }
365
366 fn to_css(&self) -> String {
367 String::new()
368 }
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374 use crate::direct::{CellBuffer, DirectTerminalCanvas};
375
376 #[test]
377 fn test_cluster_plot_new() {
378 let points = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0)];
379 let labels = vec![0, 0, 1];
380 let plot = ClusterPlot::new(points, labels);
381 assert_eq!(plot.points.len(), 3);
382 assert_eq!(plot.labels.len(), 3);
383 }
384
385 #[test]
386 fn test_cluster_plot_empty() {
387 let plot = ClusterPlot::default();
388 assert_eq!(plot.cluster_count(), 0);
389 }
390
391 #[test]
392 fn test_cluster_plot_with_centroids() {
393 let plot = ClusterPlot::new(vec![(0.0, 0.0)], vec![0])
394 .with_centroids(vec![(0.5, 0.5), (1.5, 1.5)]);
395 assert_eq!(plot.centroids.len(), 2);
396 }
397
398 #[test]
399 fn test_cluster_plot_cluster_count() {
400 let labels = vec![0, 0, 1, 1, 2, -1]; let points = vec![(0.0, 0.0); 6];
402 let plot = ClusterPlot::new(points, labels);
403 assert_eq!(plot.cluster_count(), 3);
404 }
405
406 #[test]
407 fn test_cluster_plot_paint() {
408 let points = vec![
409 (0.0, 0.0),
410 (1.0, 0.0),
411 (0.0, 1.0),
412 (5.0, 5.0),
413 (6.0, 5.0),
414 (5.0, 6.0),
415 ];
416 let labels = vec![0, 0, 0, 1, 1, 1];
417 let mut plot =
418 ClusterPlot::new(points, labels).with_centroids(vec![(0.33, 0.33), (5.33, 5.33)]);
419
420 let bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
421 plot.layout(bounds);
422
423 let mut buffer = CellBuffer::new(60, 20);
424 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
425 plot.paint(&mut canvas);
426 }
427
428 #[test]
429 fn test_cluster_plot_algorithms() {
430 let plot1 = ClusterPlot::default().with_algorithm(ClusterAlgorithm::KMeans { k: 5 });
431 assert!(matches!(plot1.algorithm, ClusterAlgorithm::KMeans { k: 5 }));
432
433 let plot2 = ClusterPlot::default().with_algorithm(ClusterAlgorithm::DBSCAN {
434 eps: 0.5,
435 min_samples: 5,
436 });
437 assert!(matches!(plot2.algorithm, ClusterAlgorithm::DBSCAN { .. }));
438 }
439
440 #[test]
441 fn test_cluster_plot_verify() {
442 let mut plot = ClusterPlot::new(vec![(0.0, 0.0)], vec![0]);
443 plot.bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
444 assert!(plot.verify().is_valid());
445 }
446
447 #[test]
448 fn test_cluster_plot_verify_mismatch() {
449 let mut plot = ClusterPlot::new(vec![(0.0, 0.0), (1.0, 1.0)], vec![0]); plot.bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
451 assert!(!plot.verify().is_valid());
452 }
453
454 #[test]
455 fn test_cluster_plot_brick_name() {
456 let plot = ClusterPlot::default();
457 assert_eq!(plot.brick_name(), "ClusterPlot");
458 }
459
460 #[test]
461 fn test_cluster_colors() {
462 let colors = ClusterPlot::default_colors();
463 assert!(colors.len() >= 10);
464 }
465
466 #[test]
471 fn test_with_show_centroids() {
472 let plot = ClusterPlot::default().with_show_centroids(false);
473 assert!(!plot.show_centroids);
474
475 let plot2 = plot.with_show_centroids(true);
476 assert!(plot2.show_centroids);
477 }
478
479 #[test]
480 fn test_with_colors() {
481 let custom_colors = vec![Color::RED, Color::GREEN, Color::BLUE];
482 let plot = ClusterPlot::default().with_colors(custom_colors);
483 assert_eq!(plot.colors.len(), 3);
484 }
485
486 #[test]
487 fn test_get_cluster_color_noise() {
488 let plot = ClusterPlot::default();
489 let noise_color = plot.get_cluster_color(-1);
490 assert!(noise_color.a < 1.0);
492 }
493
494 #[test]
495 fn test_get_cluster_color_normal() {
496 let plot = ClusterPlot::default();
497 let color0 = plot.get_cluster_color(0);
498 let color1 = plot.get_cluster_color(1);
499 assert!(color0.r != color1.r || color0.g != color1.g || color0.b != color1.b);
501 }
502
503 #[test]
504 fn test_get_cluster_color_wraps() {
505 let plot = ClusterPlot::default();
506 let colors_len = plot.colors.len();
507 let color_high = plot.get_cluster_color(colors_len as i32 + 2);
509 let color_wrapped = plot.get_cluster_color(2);
510 assert_eq!(color_high, color_wrapped);
511 }
512
513 #[test]
514 fn test_x_range_empty() {
515 let plot = ClusterPlot::default();
516 let (x_min, x_max) = plot.x_range();
517 assert_eq!(x_min, 0.0);
518 assert_eq!(x_max, 1.0);
519 }
520
521 #[test]
522 fn test_x_range_with_data() {
523 let points = vec![(0.0, 0.0), (10.0, 5.0), (5.0, 2.0)];
524 let plot = ClusterPlot::new(points, vec![0, 0, 0]);
525 let (x_min, x_max) = plot.x_range();
526 assert!(x_min < 0.0);
528 assert!(x_max > 10.0);
529 }
530
531 #[test]
532 fn test_x_range_with_nan() {
533 let points = vec![(f64::NAN, 0.0), (5.0, 1.0), (10.0, 2.0)];
534 let plot = ClusterPlot::new(points, vec![0, 0, 0]);
535 let (x_min, x_max) = plot.x_range();
536 assert!(x_min < 5.0);
538 assert!(x_max > 10.0);
539 }
540
541 #[test]
542 fn test_y_range_empty() {
543 let plot = ClusterPlot::default();
544 let (y_min, y_max) = plot.y_range();
545 assert_eq!(y_min, 0.0);
546 assert_eq!(y_max, 1.0);
547 }
548
549 #[test]
550 fn test_y_range_with_data() {
551 let points = vec![(0.0, 0.0), (1.0, 10.0), (2.0, 5.0)];
552 let plot = ClusterPlot::new(points, vec![0, 0, 0]);
553 let (y_min, y_max) = plot.y_range();
554 assert!(y_min < 0.0);
555 assert!(y_max > 10.0);
556 }
557
558 #[test]
559 fn test_y_range_with_nan() {
560 let points = vec![(0.0, f64::NAN), (1.0, 5.0), (2.0, 10.0)];
561 let plot = ClusterPlot::new(points, vec![0, 0, 0]);
562 let (y_min, y_max) = plot.y_range();
563 assert!(y_min < 5.0);
564 assert!(y_max > 10.0);
565 }
566
567 #[test]
568 fn test_paint_too_small_width() {
569 let mut plot = ClusterPlot::new(vec![(0.0, 0.0)], vec![0]);
570 plot.bounds = Rect::new(0.0, 0.0, 5.0, 20.0); let mut buffer = CellBuffer::new(5, 20);
573 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
574 plot.paint(&mut canvas); }
576
577 #[test]
578 fn test_paint_too_small_height() {
579 let mut plot = ClusterPlot::new(vec![(0.0, 0.0)], vec![0]);
580 plot.bounds = Rect::new(0.0, 0.0, 60.0, 3.0); let mut buffer = CellBuffer::new(60, 3);
583 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
584 plot.paint(&mut canvas); }
586
587 #[test]
588 fn test_paint_with_noise_points() {
589 let points = vec![
590 (0.0, 0.0),
591 (1.0, 1.0),
592 (5.0, 5.0), ];
594 let labels = vec![0, 0, -1]; let mut plot = ClusterPlot::new(points, labels);
596 plot.bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
597
598 let mut buffer = CellBuffer::new(60, 20);
599 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
600 plot.paint(&mut canvas);
601 }
602
603 #[test]
604 fn test_paint_without_centroids() {
605 let points = vec![(0.0, 0.0), (1.0, 1.0)];
606 let labels = vec![0, 0];
607 let mut plot = ClusterPlot::new(points, labels)
608 .with_centroids(vec![(0.5, 0.5)])
609 .with_show_centroids(false);
610 plot.bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
611
612 let mut buffer = CellBuffer::new(60, 20);
613 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
614 plot.paint(&mut canvas);
615 }
616
617 #[test]
618 fn test_paint_with_nan_centroid() {
619 let points = vec![(0.0, 0.0), (1.0, 1.0)];
620 let labels = vec![0, 0];
621 let mut plot =
622 ClusterPlot::new(points, labels).with_centroids(vec![(f64::NAN, 0.5), (0.5, f64::NAN)]);
623 plot.bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
624
625 let mut buffer = CellBuffer::new(60, 20);
626 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
627 plot.paint(&mut canvas); }
629
630 #[test]
631 fn test_paint_with_nan_point() {
632 let points = vec![(f64::NAN, 0.0), (0.0, f64::NAN), (1.0, 1.0)];
633 let labels = vec![0, 0, 0];
634 let mut plot = ClusterPlot::new(points, labels);
635 plot.bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
636
637 let mut buffer = CellBuffer::new(60, 20);
638 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
639 plot.paint(&mut canvas); }
641
642 #[test]
643 fn test_paint_single_point() {
644 let points = vec![(5.0, 5.0)];
646 let labels = vec![0];
647 let mut plot = ClusterPlot::new(points, labels);
648 plot.bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
649
650 let mut buffer = CellBuffer::new(60, 20);
651 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
652 plot.paint(&mut canvas);
653 }
654
655 #[test]
656 fn test_paint_negative_plot_dimensions() {
657 let mut plot = ClusterPlot::new(vec![(0.0, 0.0)], vec![0]);
658 plot.bounds = Rect::new(0.0, 0.0, 2.0, 2.0);
660
661 let mut buffer = CellBuffer::new(10, 10);
662 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
663 plot.paint(&mut canvas); }
665
666 #[test]
667 fn test_algorithm_hierarchical() {
668 let plot =
669 ClusterPlot::default().with_algorithm(ClusterAlgorithm::Hierarchical { n_clusters: 4 });
670 assert!(matches!(
671 plot.algorithm,
672 ClusterAlgorithm::Hierarchical { n_clusters: 4 }
673 ));
674 }
675
676 #[test]
677 fn test_algorithm_hdbscan() {
678 let plot = ClusterPlot::default().with_algorithm(ClusterAlgorithm::HDBSCAN {
679 min_cluster_size: 10,
680 });
681 assert!(matches!(
682 plot.algorithm,
683 ClusterAlgorithm::HDBSCAN {
684 min_cluster_size: 10
685 }
686 ));
687 }
688
689 #[test]
690 fn test_verify_too_small() {
691 let mut plot = ClusterPlot::new(vec![(0.0, 0.0)], vec![0]);
692 plot.bounds = Rect::new(0.0, 0.0, 5.0, 3.0); let result = plot.verify();
694 assert!(!result.failed.is_empty());
695 }
696
697 #[test]
698 fn test_brick_assertions() {
699 let plot = ClusterPlot::default();
700 let assertions = plot.assertions();
701 assert!(!assertions.is_empty());
702 }
703
704 #[test]
705 fn test_brick_budget() {
706 let plot = ClusterPlot::default();
707 let budget = plot.budget();
708 assert!(budget.total_ms > 0);
709 }
710
711 #[test]
712 fn test_brick_to_html() {
713 let plot = ClusterPlot::default();
714 assert!(plot.to_html().is_empty());
715 }
716
717 #[test]
718 fn test_brick_to_css() {
719 let plot = ClusterPlot::default();
720 assert!(plot.to_css().is_empty());
721 }
722
723 #[test]
724 fn test_widget_type_id() {
725 let plot = ClusterPlot::default();
726 let id = Widget::type_id(&plot);
727 assert_eq!(id, TypeId::of::<ClusterPlot>());
728 }
729
730 #[test]
731 fn test_widget_measure() {
732 let plot = ClusterPlot::default();
733 let constraints = Constraints::tight(Size::new(100.0, 50.0));
734 let size = plot.measure(constraints);
735 assert!(size.width <= 60.0);
736 assert!(size.height <= 20.0);
737 }
738
739 #[test]
740 fn test_widget_layout() {
741 let mut plot = ClusterPlot::default();
742 let bounds = Rect::new(10.0, 20.0, 40.0, 15.0);
743 let result = plot.layout(bounds);
744 assert_eq!(result.size.width, 40.0);
745 assert_eq!(result.size.height, 15.0);
746 assert_eq!(plot.bounds, bounds);
747 }
748
749 #[test]
750 fn test_widget_event() {
751 let mut plot = ClusterPlot::default();
752 let result = plot.event(&Event::FocusIn);
753 assert!(result.is_none());
754 }
755
756 #[test]
757 fn test_widget_children() {
758 let plot = ClusterPlot::default();
759 assert!(plot.children().is_empty());
760 }
761
762 #[test]
763 fn test_widget_children_mut() {
764 let mut plot = ClusterPlot::default();
765 assert!(plot.children_mut().is_empty());
766 }
767
768 #[test]
769 fn test_cluster_algorithm_default() {
770 let algo = ClusterAlgorithm::default();
771 assert!(matches!(algo, ClusterAlgorithm::KMeans { k: 3 }));
772 }
773
774 #[test]
775 fn test_cluster_count_all_noise() {
776 let points = vec![(0.0, 0.0), (1.0, 1.0)];
777 let labels = vec![-1, -1]; let plot = ClusterPlot::new(points, labels);
779 assert_eq!(plot.cluster_count(), 0);
780 }
781
782 #[test]
783 fn test_cluster_count_duplicates() {
784 let points = vec![(0.0, 0.0); 10];
786 let labels = vec![0, 0, 0, 1, 1, 2, 2, 2, 2, 0];
787 let plot = ClusterPlot::new(points, labels);
788 assert_eq!(plot.cluster_count(), 3);
789 }
790
791 #[test]
792 fn test_paint_all_algorithms_legend() {
793 let points = vec![(0.0, 0.0), (1.0, 1.0)];
795 let labels = vec![0, 0];
796
797 let algorithms = vec![
798 ClusterAlgorithm::KMeans { k: 3 },
799 ClusterAlgorithm::DBSCAN {
800 eps: 0.5,
801 min_samples: 5,
802 },
803 ClusterAlgorithm::Hierarchical { n_clusters: 3 },
804 ClusterAlgorithm::HDBSCAN {
805 min_cluster_size: 5,
806 },
807 ];
808
809 for algo in algorithms {
810 let mut plot = ClusterPlot::new(points.clone(), labels.clone()).with_algorithm(algo);
811 plot.bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
812
813 let mut buffer = CellBuffer::new(60, 20);
814 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
815 plot.paint(&mut canvas);
816 }
817 }
818
819 #[test]
820 fn test_paint_missing_label() {
821 let points = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0)];
823 let labels = vec![0]; let mut plot = ClusterPlot::new(points, labels);
825 plot.bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
826
827 let mut buffer = CellBuffer::new(60, 20);
828 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
829 plot.paint(&mut canvas); }
831
832 #[test]
833 fn test_clone() {
834 let plot = ClusterPlot::new(vec![(0.0, 0.0)], vec![0])
835 .with_centroids(vec![(0.5, 0.5)])
836 .with_algorithm(ClusterAlgorithm::DBSCAN {
837 eps: 0.3,
838 min_samples: 2,
839 })
840 .with_show_centroids(true)
841 .with_colors(vec![Color::RED]);
842
843 let cloned = plot;
844 assert_eq!(cloned.points.len(), 1);
845 assert_eq!(cloned.centroids.len(), 1);
846 assert!(cloned.show_centroids);
847 }
848
849 #[test]
850 fn test_debug() {
851 let plot = ClusterPlot::default();
852 let debug_str = format!("{:?}", plot);
853 assert!(debug_str.contains("ClusterPlot"));
854 }
855
856 #[test]
857 fn test_algorithm_debug() {
858 let algo = ClusterAlgorithm::KMeans { k: 5 };
859 let debug_str = format!("{:?}", algo);
860 assert!(debug_str.contains("KMeans"));
861 }
862
863 #[test]
864 fn test_algorithm_clone() {
865 let algo = ClusterAlgorithm::DBSCAN {
866 eps: 0.5,
867 min_samples: 3,
868 };
869 let cloned = algo;
870 assert!(matches!(
871 cloned,
872 ClusterAlgorithm::DBSCAN {
873 eps: _,
874 min_samples: 3
875 }
876 ));
877 }
878}