Skip to main content

presentar_terminal/widgets/
cluster_plot.rs

1//! Cluster plot widget for K-Means, DBSCAN, and other clustering visualizations.
2//!
3//! Implements SPEC-024 Section 26.3.
4
5use 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/// Clustering algorithm type.
13#[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/// Cluster plot widget.
28#[derive(Debug, Clone)]
29pub struct ClusterPlot {
30    /// Data points (x, y).
31    points: Vec<(f64, f64)>,
32    /// Cluster labels for each point (-1 = noise).
33    labels: Vec<i32>,
34    /// Cluster centroids.
35    centroids: Vec<(f64, f64)>,
36    /// Algorithm used.
37    algorithm: ClusterAlgorithm,
38    /// Show centroids.
39    show_centroids: bool,
40    /// Cluster colors.
41    colors: Vec<Color>,
42    /// Cached bounds.
43    bounds: Rect,
44}
45
46impl ClusterPlot {
47    /// Create a new cluster plot.
48    #[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    /// Set centroids.
63    #[must_use]
64    pub fn with_centroids(mut self, centroids: Vec<(f64, f64)>) -> Self {
65        self.centroids = centroids;
66        self
67    }
68
69    /// Set algorithm.
70    #[must_use]
71    pub fn with_algorithm(mut self, algorithm: ClusterAlgorithm) -> Self {
72        self.algorithm = algorithm;
73        self
74    }
75
76    /// Toggle centroid display.
77    #[must_use]
78    pub fn with_show_centroids(mut self, show: bool) -> Self {
79        self.show_centroids = show;
80        self
81    }
82
83    /// Set custom colors.
84    #[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), // Blue
93            Color::new(1.0, 0.5, 0.05, 1.0),   // Orange
94            Color::new(0.17, 0.63, 0.17, 1.0), // Green
95            Color::new(0.84, 0.15, 0.16, 1.0), // Red
96            Color::new(0.58, 0.4, 0.74, 1.0),  // Purple
97            Color::new(0.55, 0.34, 0.29, 1.0), // Brown
98            Color::new(0.89, 0.47, 0.76, 1.0), // Pink
99            Color::new(0.5, 0.5, 0.5, 1.0),    // Gray
100            Color::new(0.74, 0.74, 0.13, 1.0), // Olive
101            Color::new(0.09, 0.75, 0.81, 1.0), // Cyan
102        ]
103    }
104
105    fn get_cluster_color(&self, label: i32) -> Color {
106        if label < 0 {
107            // Noise points
108            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    /// Get unique cluster count.
153    #[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        // Draw points
206        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        // Draw centroids
243        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        // Draw legend
281        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        // Check labels consistency
348        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]; // 3 clusters + noise
401        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]); // Mismatch
450        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    // =========================================================================
467    // Additional coverage tests
468    // =========================================================================
469
470    #[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        // Noise color should be grayish with low alpha
491        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        // Different clusters should have different colors
500        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        // Should wrap around
508        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        // Should have 10% padding
527        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        // Should ignore NaN
537        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); // Too narrow
571
572        let mut buffer = CellBuffer::new(5, 20);
573        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
574        plot.paint(&mut canvas); // Should not crash
575    }
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); // Too short
581
582        let mut buffer = CellBuffer::new(60, 3);
583        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
584        plot.paint(&mut canvas); // Should not crash
585    }
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), // Noise point
593        ];
594        let labels = vec![0, 0, -1]; // -1 = noise
595        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); // Should skip NaN centroids
628    }
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); // Should skip NaN points
640    }
641
642    #[test]
643    fn test_paint_single_point() {
644        // Single point means x_min == x_max, y_min == y_max
645        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        // Set bounds that result in negative plot_width/height after margin
659        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); // Should handle gracefully
664    }
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); // Too small
693        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]; // All noise
778        let plot = ClusterPlot::new(points, labels);
779        assert_eq!(plot.cluster_count(), 0);
780    }
781
782    #[test]
783    fn test_cluster_count_duplicates() {
784        // Labels with duplicates should count unique clusters
785        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        // Test legend rendering for each algorithm type
794        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        // More points than labels
822        let points = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0)];
823        let labels = vec![0]; // Only 1 label for 3 points
824        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); // Should handle gracefully (default to -1)
830    }
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}