1use scirs2_core::ndarray::{Array1, Array2, ArrayView2};
8use scirs2_core::numeric::{Float, FromPrimitive};
9use std::collections::HashMap;
10use std::fmt::Debug;
11
12use serde::{Deserialize, Serialize};
13
14use super::{ColorScheme, ScatterPlot3D, VisualizationConfig};
15use crate::error::{ClusteringError, Result};
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct InteractiveConfig {
20 pub enable_camera_controls: bool,
22 pub enable_point_selection: bool,
24 pub enable_cluster_manipulation: bool,
26 pub show_axes: bool,
28 pub show_grid: bool,
30 pub show_realtime_stats: bool,
32 pub multi_view: bool,
34 pub view_count: usize,
36 pub enable_vr_mode: bool,
38 pub stereoscopic: bool,
40 pub field_of_view: f32,
42 pub camera_sensitivity: f32,
44 pub highlight_on_hover: bool,
46 pub show_3d_boundaries: bool,
48 pub temporal_view: bool,
50}
51
52impl Default for InteractiveConfig {
53 fn default() -> Self {
54 Self {
55 enable_camera_controls: true,
56 enable_point_selection: true,
57 enable_cluster_manipulation: false,
58 show_axes: true,
59 show_grid: true,
60 show_realtime_stats: true,
61 multi_view: false,
62 view_count: 1,
63 enable_vr_mode: false,
64 stereoscopic: false,
65 field_of_view: 60.0,
66 camera_sensitivity: 1.0,
67 highlight_on_hover: true,
68 show_3d_boundaries: true,
69 temporal_view: false,
70 }
71 }
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct CameraState {
77 pub position: (f64, f64, f64),
79 pub target: (f64, f64, f64),
81 pub up: (f64, f64, f64),
83 pub fov: f32,
85 pub near: f64,
87 pub far: f64,
89 pub rotation: (f64, f64, f64),
91 pub zoom: f64,
93}
94
95impl Default for CameraState {
96 fn default() -> Self {
97 Self {
98 position: (10.0, 10.0, 10.0),
99 target: (0.0, 0.0, 0.0),
100 up: (0.0, 1.0, 0.0),
101 fov: 60.0,
102 near: 0.1,
103 far: 1000.0,
104 rotation: (0.0, 0.0, 0.0),
105 zoom: 1.0,
106 }
107 }
108}
109
110#[derive(Debug, Clone)]
112pub struct InteractiveState {
113 pub camera: CameraState,
115 pub selected_points: Vec<usize>,
117 pub highlighted_points: Vec<usize>,
119 pub active_cluster: Option<i32>,
121 pub input_state: InputState,
123 pub view_bounds: (f64, f64, f64, f64, f64, f64),
125 pub current_time: f64,
127 pub animation_playing: bool,
129 pub view_mode: ViewMode,
131}
132
133#[derive(Debug, Clone)]
135pub struct InputState {
136 pub mouse_position: (f64, f64),
138 pub prev_mouse_position: (f64, f64),
140 pub mouse_buttons: Vec<MouseButton>,
142 pub keys_pressed: Vec<KeyCode>,
144 pub touch_points: Vec<TouchPoint>,
146 pub gesture_state: GestureState,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum MouseButton {
153 Left,
154 Right,
155 Middle,
156 Other(u8),
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum KeyCode {
162 Space,
163 Enter,
164 Escape,
165 ArrowUp,
166 ArrowDown,
167 ArrowLeft,
168 ArrowRight,
169 Shift,
170 Ctrl,
171 Alt,
172 Key(char),
173}
174
175#[derive(Debug, Clone)]
177pub struct TouchPoint {
178 pub id: u64,
179 pub position: (f64, f64),
180 pub pressure: f64,
181}
182
183#[derive(Debug, Clone)]
185pub struct GestureState {
186 pub is_pinching: bool,
187 pub pinch_scale: f64,
188 pub is_rotating: bool,
189 pub rotation_angle: f64,
190 pub is_panning: bool,
191 pub pan_delta: (f64, f64),
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
196pub enum ViewMode {
197 Perspective,
199 Orthographic,
201 FirstPerson,
203 BirdsEye,
205 Side,
207 Front,
209 Top,
211 SplitScreen,
213 VRStereo,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct ClusterStats {
220 pub cluster_id: i32,
222 pub point_count: usize,
224 pub centroid: Array1<f64>,
226 pub diameter: f64,
228 pub avg_distance_to_centroid: f64,
230 pub density: f64,
232 pub bounding_box: (f64, f64, f64, f64, f64, f64),
234 pub color: String,
236}
237
238pub struct InteractiveVisualizer {
240 config: InteractiveConfig,
241 state: InteractiveState,
242 cluster_stats: HashMap<i32, ClusterStats>,
243 last_update: std::time::Instant,
244 point_positions: Vec<[f64; 3]>,
248}
249
250impl InteractiveVisualizer {
251 pub fn new(config: InteractiveConfig) -> Self {
253 Self {
254 config,
255 state: InteractiveState {
256 camera: CameraState::default(),
257 selected_points: Vec::new(),
258 highlighted_points: Vec::new(),
259 active_cluster: None,
260 input_state: InputState {
261 mouse_position: (0.0, 0.0),
262 prev_mouse_position: (0.0, 0.0),
263 mouse_buttons: Vec::new(),
264 keys_pressed: Vec::new(),
265 touch_points: Vec::new(),
266 gesture_state: GestureState {
267 is_pinching: false,
268 pinch_scale: 1.0,
269 is_rotating: false,
270 rotation_angle: 0.0,
271 is_panning: false,
272 pan_delta: (0.0, 0.0),
273 },
274 },
275 view_bounds: (-10.0, 10.0, -10.0, 10.0, -10.0, 10.0),
276 current_time: 0.0,
277 animation_playing: false,
278 view_mode: ViewMode::Perspective,
279 },
280 cluster_stats: HashMap::new(),
281 last_update: std::time::Instant::now(),
282 point_positions: Vec::new(),
283 }
284 }
285
286 pub fn update_data<F: Float + FromPrimitive + Debug>(
288 &mut self,
289 data: ArrayView2<F>,
290 labels: &Array1<i32>,
291 centroids: Option<&Array2<F>>,
292 ) -> Result<()> {
293 self.calculate_cluster_stats(data, labels, centroids)?;
295
296 self.point_positions = data
299 .rows()
300 .into_iter()
301 .map(|row| {
302 let mut pos = [0.0_f64; 3];
303 for (axis, slot) in pos.iter_mut().enumerate() {
304 if let Some(v) = row.get(axis) {
305 *slot = v.to_f64().unwrap_or(0.0);
306 }
307 }
308 pos
309 })
310 .collect();
311
312 self.update_view_bounds(data);
314
315 self.validate_selections(data.nrows());
317
318 Ok(())
319 }
320
321 pub fn handle_mouse_input(&mut self, button: MouseButton, position: (f64, f64), pressed: bool) {
323 self.state.input_state.prev_mouse_position = self.state.input_state.mouse_position;
324 self.state.input_state.mouse_position = position;
325
326 if pressed {
327 if !self.state.input_state.mouse_buttons.contains(&button) {
328 self.state.input_state.mouse_buttons.push(button);
329 }
330 } else {
331 self.state
332 .input_state
333 .mouse_buttons
334 .retain(|&b| b != button);
335 }
336
337 if self.config.enable_camera_controls {
339 self.handle_camera_input();
340 }
341 }
342
343 pub fn handle_keyboard_input(&mut self, key: KeyCode, pressed: bool) {
345 if pressed {
346 if !self.state.input_state.keys_pressed.contains(&key) {
347 self.state.input_state.keys_pressed.push(key);
348 }
349 } else {
350 self.state.input_state.keys_pressed.retain(|&k| k != key);
351 }
352
353 self.handle_keyboard_shortcuts(key, pressed);
355 }
356
357 pub fn handle_touch_input(&mut self, touchpoints: Vec<TouchPoint>) {
359 let prev_touch_count = self.state.input_state.touch_points.len();
360 self.state.input_state.touch_points = touchpoints;
361 let current_touch_count = self.state.input_state.touch_points.len();
362
363 self.update_gesture_state(prev_touch_count, current_touch_count);
365
366 if self.config.enable_camera_controls {
368 self.handle_touch_gestures();
369 }
370 }
371
372 pub fn select_points_in_region(&mut self, region: BoundingBox3D) -> Vec<usize> {
379 let (min_x, min_y, min_z) = region.min;
380 let (max_x, max_y, max_z) = region.max;
381 let (lo_x, hi_x) = (min_x.min(max_x), min_x.max(max_x));
383 let (lo_y, hi_y) = (min_y.min(max_y), min_y.max(max_y));
384 let (lo_z, hi_z) = (min_z.min(max_z), min_z.max(max_z));
385
386 let selected: Vec<usize> = self
387 .point_positions
388 .iter()
389 .enumerate()
390 .filter(|(_, p)| {
391 p[0] >= lo_x
392 && p[0] <= hi_x
393 && p[1] >= lo_y
394 && p[1] <= hi_y
395 && p[2] >= lo_z
396 && p[2] <= hi_z
397 })
398 .map(|(i, _)| i)
399 .collect();
400
401 self.state.selected_points = selected.clone();
402 selected
403 }
404
405 pub fn highlight_points_at(&mut self, screenpos: (f64, f64)) -> Vec<usize> {
412 let (sx, sy) = screenpos;
413 const PICK_RADIUS: f64 = 0.5;
416 let pick_radius_sq = PICK_RADIUS * PICK_RADIUS;
417
418 let mut best: Option<(usize, f64)> = None;
419 for (i, p) in self.point_positions.iter().enumerate() {
420 let dx = p[0] - sx;
421 let dy = p[1] - sy;
422 let dist_sq = dx * dx + dy * dy;
423 if dist_sq <= pick_radius_sq && best.map(|(_, b)| dist_sq < b).unwrap_or(true) {
424 best = Some((i, dist_sq));
425 }
426 }
427
428 let highlighted: Vec<usize> = best.map(|(i, _)| vec![i]).unwrap_or_default();
429 self.state.highlighted_points = highlighted.clone();
430 highlighted
431 }
432
433 pub fn get_cluster_stats(&self) -> &HashMap<i32, ClusterStats> {
435 &self.cluster_stats
436 }
437
438 pub fn get_state(&self) -> &InteractiveState {
440 &self.state
441 }
442
443 pub fn set_camera_position(&mut self, position: (f64, f64, f64)) {
445 self.state.camera.position = position;
446 }
447
448 pub fn set_camera_target(&mut self, target: (f64, f64, f64)) {
450 self.state.camera.target = target;
451 }
452
453 pub fn set_view_mode(&mut self, mode: ViewMode) {
455 self.state.view_mode = mode;
456
457 match mode {
459 ViewMode::BirdsEye => {
460 self.state.camera.position = (0.0, 20.0, 0.0);
461 self.state.camera.target = (0.0, 0.0, 0.0);
462 self.state.camera.up = (0.0, 0.0, -1.0);
463 }
464 ViewMode::Side => {
465 self.state.camera.position = (20.0, 0.0, 0.0);
466 self.state.camera.target = (0.0, 0.0, 0.0);
467 self.state.camera.up = (0.0, 1.0, 0.0);
468 }
469 ViewMode::Front => {
470 self.state.camera.position = (0.0, 0.0, 20.0);
471 self.state.camera.target = (0.0, 0.0, 0.0);
472 self.state.camera.up = (0.0, 1.0, 0.0);
473 }
474 ViewMode::Top => {
475 self.state.camera.position = (0.0, 20.0, 0.0);
476 self.state.camera.target = (0.0, 0.0, 0.0);
477 self.state.camera.up = (0.0, 0.0, -1.0);
478 }
479 _ => {
480 }
482 }
483 }
484
485 pub fn set_animation_playing(&mut self, playing: bool) {
487 self.state.animation_playing = playing;
488 }
489
490 pub fn set_current_time(&mut self, time: f64) {
492 self.state.current_time = time;
493 }
494
495 pub fn export_view_state(&self) -> Result<String> {
497 #[cfg(feature = "serde")]
498 {
499 let export_data = InteractiveViewExport {
500 camera: self.state.camera.clone(),
501 view_mode: self.state.view_mode,
502 cluster_stats: self.cluster_stats.clone(),
503 view_bounds: self.state.view_bounds,
504 current_time: self.state.current_time,
505 };
506
507 return serde_json::to_string_pretty(&export_data)
508 .map_err(|e| ClusteringError::ComputationError(format!("Export failed: {}", e)));
509 }
510
511 #[cfg(not(feature = "serde"))]
512 {
513 Err(ClusteringError::ComputationError(
514 "Export requires 'serde' feature".to_string(),
515 ))
516 }
517 }
518
519 fn calculate_cluster_stats<F: Float + FromPrimitive + Debug>(
521 &mut self,
522 data: ArrayView2<F>,
523 labels: &Array1<i32>,
524 centroids: Option<&Array2<F>>,
525 ) -> Result<()> {
526 self.cluster_stats.clear();
527
528 let mut unique_labels: Vec<i32> = labels.iter().cloned().collect();
530 unique_labels.sort_unstable();
531 unique_labels.dedup();
532
533 for &cluster_id in &unique_labels {
534 let cluster_points: Vec<usize> = labels
536 .iter()
537 .enumerate()
538 .filter(|(_, &label)| label == cluster_id)
539 .map(|(idx_, _)| idx_)
540 .collect();
541
542 if cluster_points.is_empty() {
543 continue;
544 }
545
546 let centroid = if let Some(cents) = centroids {
548 if cluster_id >= 0 && (cluster_id as usize) < cents.nrows() {
549 cents
550 .row(cluster_id as usize)
551 .mapv(|x| x.to_f64().unwrap_or(0.0))
552 } else {
553 self.calculate_centroid_from_points(data, &cluster_points)?
555 }
556 } else {
557 self.calculate_centroid_from_points(data, &cluster_points)?
558 };
559
560 let (diameter, avg_distance, density, bounding_box) =
562 self.calculate_cluster_metrics(data, &cluster_points, ¢roid)?;
563
564 let stats = ClusterStats {
565 cluster_id,
566 point_count: cluster_points.len(),
567 centroid,
568 diameter,
569 avg_distance_to_centroid: avg_distance,
570 density,
571 bounding_box,
572 color: format!("#{:06x}", (cluster_id.unsigned_abs() * 123456) % 0xFFFFFF),
573 };
574
575 self.cluster_stats.insert(cluster_id, stats);
576 }
577
578 Ok(())
579 }
580
581 fn calculate_centroid_from_points<F: Float + FromPrimitive + Debug>(
583 &self,
584 data: ArrayView2<F>,
585 point_indices: &[usize],
586 ) -> Result<Array1<f64>> {
587 let n_features = data.ncols();
588 let mut centroid = Array1::zeros(n_features);
589
590 for &idx in point_indices {
591 for j in 0..n_features {
592 centroid[j] += data[[idx, j]].to_f64().unwrap_or(0.0);
593 }
594 }
595
596 let count = point_indices.len() as f64;
597 if count > 0.0 {
598 centroid.mapv_inplace(|x| x / count);
599 }
600
601 Ok(centroid)
602 }
603
604 fn calculate_cluster_metrics<F: Float + FromPrimitive + Debug>(
606 &self,
607 data: ArrayView2<F>,
608 point_indices: &[usize],
609 centroid: &Array1<f64>,
610 ) -> Result<(f64, f64, f64, (f64, f64, f64, f64, f64, f64))> {
611 let n_features = data.ncols();
612
613 let mut max_distance = 0.0;
614 let mut total_distance = 0.0;
615 let mut min_coords = vec![f64::INFINITY; n_features];
616 let mut max_coords = vec![f64::NEG_INFINITY; n_features];
617
618 for &idx in point_indices {
620 let mut distance_to_centroid = 0.0;
621
622 for j in 0..n_features {
623 let coord = data[[idx, j]].to_f64().unwrap_or(0.0);
624 let diff = coord - centroid[j];
625 distance_to_centroid += diff * diff;
626
627 min_coords[j] = min_coords[j].min(coord);
628 max_coords[j] = max_coords[j].max(coord);
629 }
630
631 distance_to_centroid = distance_to_centroid.sqrt();
632 total_distance += distance_to_centroid;
633 }
634
635 for i in 0..point_indices.len() {
637 for j in (i + 1)..point_indices.len() {
638 let mut distance = 0.0;
639 for k in 0..n_features {
640 let diff = data[[point_indices[i], k]].to_f64().unwrap_or(0.0)
641 - data[[point_indices[j], k]].to_f64().unwrap_or(0.0);
642 distance += diff * diff;
643 }
644 distance = distance.sqrt();
645 max_distance = max_distance.max(distance);
646 }
647 }
648
649 let avg_distance = if point_indices.is_empty() {
650 0.0
651 } else {
652 total_distance / point_indices.len() as f64
653 };
654
655 let volume = if n_features >= 3 {
657 (max_coords[0] - min_coords[0])
658 * (max_coords[1] - min_coords[1])
659 * (max_coords[2] - min_coords[2])
660 } else if n_features >= 2 {
661 (max_coords[0] - min_coords[0]) * (max_coords[1] - min_coords[1])
662 } else {
663 max_coords[0] - min_coords[0]
664 };
665
666 let density = if volume > 0.0 {
667 point_indices.len() as f64 / volume
668 } else {
669 0.0
670 };
671
672 let bounding_box = (
673 min_coords.first().copied().unwrap_or(0.0),
674 max_coords.first().copied().unwrap_or(0.0),
675 min_coords.get(1).copied().unwrap_or(0.0),
676 max_coords.get(1).copied().unwrap_or(0.0),
677 min_coords.get(2).copied().unwrap_or(0.0),
678 max_coords.get(2).copied().unwrap_or(0.0),
679 );
680
681 Ok((max_distance, avg_distance, density, bounding_box))
682 }
683
684 fn update_view_bounds<F: Float + FromPrimitive + Debug>(&mut self, data: ArrayView2<F>) {
686 let n_features = data.ncols();
687
688 if n_features == 0 || data.nrows() == 0 {
689 return;
690 }
691
692 let mut min_vals = vec![f64::INFINITY; n_features];
693 let mut max_vals = vec![f64::NEG_INFINITY; n_features];
694
695 for i in 0..data.nrows() {
696 for j in 0..n_features {
697 let val = data[[i, j]].to_f64().unwrap_or(0.0);
698 min_vals[j] = min_vals[j].min(val);
699 max_vals[j] = max_vals[j].max(val);
700 }
701 }
702
703 let padding = 0.1;
705 for j in 0..n_features {
706 let range = max_vals[j] - min_vals[j];
707 min_vals[j] -= range * padding;
708 max_vals[j] += range * padding;
709 }
710
711 self.state.view_bounds = (
712 min_vals.first().copied().unwrap_or(-10.0),
713 max_vals.first().copied().unwrap_or(10.0),
714 min_vals.get(1).copied().unwrap_or(-10.0),
715 max_vals.get(1).copied().unwrap_or(10.0),
716 min_vals.get(2).copied().unwrap_or(-10.0),
717 max_vals.get(2).copied().unwrap_or(10.0),
718 );
719 }
720
721 fn validate_selections(&mut self, npoints: usize) {
723 self.state.selected_points.retain(|&idx| idx < npoints);
724 self.state.highlighted_points.retain(|&idx| idx < npoints);
725 }
726
727 fn handle_camera_input(&mut self) {
729 let mouse_delta = (
730 self.state.input_state.mouse_position.0 - self.state.input_state.prev_mouse_position.0,
731 self.state.input_state.mouse_position.1 - self.state.input_state.prev_mouse_position.1,
732 );
733
734 let sensitivity = self.config.camera_sensitivity as f64;
735
736 if self
738 .state
739 .input_state
740 .mouse_buttons
741 .contains(&MouseButton::Left)
742 {
743 self.state.camera.rotation.0 += mouse_delta.1 * sensitivity * 0.01;
744 self.state.camera.rotation.1 += mouse_delta.0 * sensitivity * 0.01;
745 }
746
747 if self
749 .state
750 .input_state
751 .mouse_buttons
752 .contains(&MouseButton::Right)
753 {
754 self.state.camera.zoom *= 1.0 + mouse_delta.1 * sensitivity * 0.01;
755 self.state.camera.zoom = self.state.camera.zoom.clamp(0.1, 10.0);
756 }
757
758 if self
760 .state
761 .input_state
762 .mouse_buttons
763 .contains(&MouseButton::Middle)
764 {
765 }
767 }
768
769 fn handle_keyboard_shortcuts(&mut self, key: KeyCode, pressed: bool) {
771 if !pressed {
772 return;
773 }
774
775 match key {
776 KeyCode::Space => {
777 self.state.animation_playing = !self.state.animation_playing;
778 }
779 KeyCode::Key('1') => self.set_view_mode(ViewMode::Perspective),
780 KeyCode::Key('2') => self.set_view_mode(ViewMode::Orthographic),
781 KeyCode::Key('3') => self.set_view_mode(ViewMode::BirdsEye),
782 KeyCode::Key('4') => self.set_view_mode(ViewMode::Side),
783 KeyCode::Key('5') => self.set_view_mode(ViewMode::Front),
784 KeyCode::Key('6') => self.set_view_mode(ViewMode::Top),
785 KeyCode::Escape => {
786 self.state.selected_points.clear();
787 self.state.highlighted_points.clear();
788 }
789 _ => {}
790 }
791 }
792
793 fn update_gesture_state(&mut self, prev_touch_count: usize, current_touchcount: usize) {
795 if current_touchcount == 2 {
797 let touch1 = &self.state.input_state.touch_points[0];
798 let touch2 = &self.state.input_state.touch_points[1];
799
800 let distance = ((touch1.position.0 - touch2.position.0).powi(2)
801 + (touch1.position.1 - touch2.position.1).powi(2))
802 .sqrt();
803
804 if !self.state.input_state.gesture_state.is_pinching {
805 self.state.input_state.gesture_state.is_pinching = true;
806 self.state.input_state.gesture_state.pinch_scale = distance;
807 } else {
808 let scale_factor = distance / self.state.input_state.gesture_state.pinch_scale;
809 self.state.camera.zoom *= scale_factor;
810 self.state.input_state.gesture_state.pinch_scale = distance;
811 }
812 } else {
813 self.state.input_state.gesture_state.is_pinching = false;
814 }
815 }
816
817 fn handle_touch_gestures(&mut self) {
819 }
821}
822
823#[derive(Debug, Clone)]
825pub struct BoundingBox3D {
826 pub min: (f64, f64, f64),
827 pub max: (f64, f64, f64),
828}
829
830#[derive(Debug, Clone, Serialize, Deserialize)]
832struct InteractiveViewExport {
833 camera: CameraState,
834 view_mode: ViewMode,
835 cluster_stats: HashMap<i32, ClusterStats>,
836 view_bounds: (f64, f64, f64, f64, f64, f64),
837 current_time: f64,
838}
839
840#[cfg(test)]
841mod tests {
842 use super::*;
843 use scirs2_core::ndarray::Array2;
844
845 #[test]
846 fn test_interactive_visualizer_creation() {
847 let config = InteractiveConfig::default();
848 let visualizer = InteractiveVisualizer::new(config);
849
850 assert_eq!(visualizer.state.view_mode, ViewMode::Perspective);
851 assert!(visualizer.cluster_stats.is_empty());
852 }
853
854 #[test]
855 fn test_camera_controls() {
856 let config = InteractiveConfig::default();
857 let mut visualizer = InteractiveVisualizer::new(config);
858
859 visualizer.set_camera_position((5.0, 5.0, 5.0));
860 assert_eq!(visualizer.state.camera.position, (5.0, 5.0, 5.0));
861
862 visualizer.set_camera_target((1.0, 1.0, 1.0));
863 assert_eq!(visualizer.state.camera.target, (1.0, 1.0, 1.0));
864 }
865
866 #[test]
867 fn test_view_mode_switching() {
868 let config = InteractiveConfig::default();
869 let mut visualizer = InteractiveVisualizer::new(config);
870
871 visualizer.set_view_mode(ViewMode::BirdsEye);
872 assert_eq!(visualizer.state.view_mode, ViewMode::BirdsEye);
873 assert_eq!(visualizer.state.camera.position, (0.0, 20.0, 0.0));
874 }
875
876 #[test]
877 fn test_cluster_stats_calculation() {
878 let config = InteractiveConfig::default();
879 let mut visualizer = InteractiveVisualizer::new(config);
880
881 let data = Array2::from_shape_vec(
882 (4, 3),
883 vec![1.0, 2.0, 3.0, 1.1, 2.1, 3.1, 5.0, 6.0, 7.0, 5.1, 6.1, 7.1],
884 )
885 .expect("Operation failed");
886
887 let labels = Array1::from_vec(vec![0, 0, 1, 1]);
888
889 visualizer
890 .update_data(data.view(), &labels, None)
891 .expect("Operation failed");
892
893 let stats = visualizer.get_cluster_stats();
894 assert_eq!(stats.len(), 2);
895 assert!(stats.contains_key(&0));
896 assert!(stats.contains_key(&1));
897
898 let cluster_0_stats = &stats[&0];
899 assert_eq!(cluster_0_stats.point_count, 2);
900 }
901}