leptos_helios/
interactivity.rs

1//! Core Interactivity Features for leptos-helios
2//!
3//! This module implements the core interactivity features identified in the feature gap analysis:
4//! - Zoom & Pan functionality
5//! - Rich tooltips with contextual information
6//! - Brush selection for data filtering
7//! - Cross-filtering between charts
8//!
9//! Following TDD methodology: Red -> Green -> Refactor
10
11use std::collections::HashMap;
12use std::sync::{Arc, Mutex};
13
14/// Viewport management for zoom and pan operations
15#[derive(Debug, Clone, PartialEq)]
16pub struct Viewport {
17    /// X offset in world coordinates
18    pub x: f64,
19    /// Y offset in world coordinates
20    pub y: f64,
21    /// Scale factor (1.0 = no zoom)
22    pub scale: f64,
23    /// Viewport width in screen coordinates
24    pub width: f64,
25    /// Viewport height in screen coordinates
26    pub height: f64,
27    /// Minimum X bound
28    min_x: Option<f64>,
29    /// Maximum X bound
30    max_x: Option<f64>,
31    /// Minimum Y bound
32    min_y: Option<f64>,
33    /// Maximum Y bound
34    max_y: Option<f64>,
35}
36
37impl Viewport {
38    /// Create a new viewport with default values
39    pub fn new(width: f64, height: f64) -> Self {
40        Self {
41            x: 0.0,
42            y: 0.0,
43            scale: 1.0,
44            width,
45            height,
46            min_x: None,
47            max_x: None,
48            min_y: None,
49            max_y: None,
50        }
51    }
52
53    /// Set bounds for the viewport
54    pub fn set_bounds(&mut self, min_x: f64, min_y: f64, max_x: f64, max_y: f64) {
55        self.min_x = Some(min_x);
56        self.min_y = Some(min_y);
57        self.max_x = Some(max_x);
58        self.max_y = Some(max_y);
59    }
60
61    /// Zoom the viewport by a factor at a specific center point
62    pub fn zoom(&mut self, factor: f64, center_x: f64, center_y: f64) {
63        let old_scale = self.scale;
64        self.scale *= factor;
65
66        // Clamp scale to reasonable bounds
67        self.scale = self.scale.max(0.1).min(10.0);
68
69        // Adjust position to keep zoom center fixed
70        // The center point in world coordinates should remain the same
71        let world_center_x = (center_x - self.x) / old_scale;
72        let world_center_y = (center_y - self.y) / old_scale;
73
74        // Update viewport position to keep the world center at the same screen position
75        self.x = center_x - world_center_x * self.scale;
76        self.y = center_y - world_center_y * self.scale;
77
78        self.clamp_to_bounds();
79    }
80
81    /// Pan the viewport by the given offset
82    pub fn pan(&mut self, dx: f64, dy: f64) {
83        self.x += dx;
84        self.y += dy;
85        self.clamp_to_bounds();
86    }
87
88    /// Clamp viewport position to bounds
89    fn clamp_to_bounds(&mut self) {
90        if let Some(min_x) = self.min_x {
91            self.x = self.x.max(min_x);
92        }
93        if let Some(max_x) = self.max_x {
94            self.x = self.x.min(max_x);
95        }
96        if let Some(min_y) = self.min_y {
97            self.y = self.y.max(min_y);
98        }
99        if let Some(max_y) = self.max_y {
100            self.y = self.y.min(max_y);
101        }
102    }
103
104    /// Transform screen coordinates to world coordinates
105    pub fn screen_to_world(&self, screen_x: f64, screen_y: f64) -> (f64, f64) {
106        let world_x = (screen_x - self.x) / self.scale;
107        let world_y = (screen_y - self.y) / self.scale;
108        (world_x, world_y)
109    }
110
111    /// Transform world coordinates to screen coordinates
112    pub fn world_to_screen(&self, world_x: f64, world_y: f64) -> (f64, f64) {
113        let screen_x = world_x * self.scale + self.x;
114        let screen_y = world_y * self.scale + self.y;
115        (screen_x, screen_y)
116    }
117}
118
119/// Rich tooltip with contextual information
120#[derive(Debug, Clone)]
121pub struct Tooltip {
122    /// Tooltip content
123    pub content: String,
124    /// Position on screen
125    pub position: (f64, f64),
126    /// Whether tooltip is visible
127    pub visible: bool,
128    /// Tooltip style
129    pub style: TooltipStyle,
130}
131
132#[derive(Debug, Clone)]
133pub struct TooltipStyle {
134    /// Background color
135    pub background_color: String,
136    /// Text color
137    pub text_color: String,
138    /// Border color
139    pub border_color: String,
140    /// Font size
141    pub font_size: f64,
142    /// Padding
143    pub padding: f64,
144}
145
146impl Default for TooltipStyle {
147    fn default() -> Self {
148        Self {
149            background_color: "rgba(0, 0, 0, 0.8)".to_string(),
150            text_color: "white".to_string(),
151            border_color: "rgba(255, 255, 255, 0.2)".to_string(),
152            font_size: 12.0,
153            padding: 8.0,
154        }
155    }
156}
157
158impl Tooltip {
159    /// Create a new tooltip
160    pub fn new(content: String, position: (f64, f64)) -> Self {
161        Self {
162            content,
163            position,
164            visible: false,
165            style: TooltipStyle::default(),
166        }
167    }
168
169    /// Show the tooltip
170    pub fn show(&mut self) {
171        self.visible = true;
172    }
173
174    /// Hide the tooltip
175    pub fn hide(&mut self) {
176        self.visible = false;
177    }
178
179    /// Update tooltip position
180    pub fn update_position(&mut self, x: f64, y: f64) {
181        self.position = (x, y);
182    }
183
184    /// Auto-position tooltip to stay within viewport bounds
185    pub fn auto_position(&mut self, viewport: &Viewport) {
186        let (x, y) = self.position;
187        let tooltip_width = 200.0; // Estimate
188        let tooltip_height = 100.0; // Estimate
189
190        let new_x = if x + tooltip_width > viewport.width {
191            x - tooltip_width
192        } else {
193            x
194        };
195
196        let new_y = if y + tooltip_height > viewport.height {
197            y - tooltip_height
198        } else {
199            y
200        };
201
202        self.position = (new_x.max(0.0), new_y.max(0.0));
203    }
204
205    /// Create tooltip from structured data
206    pub fn from_data(data: TooltipData, position: (f64, f64)) -> Self {
207        let mut content = format!("**{}**\n", data.title);
208
209        for (key, value) in &data.values {
210            content.push_str(&format!("{}: {}\n", key, value));
211        }
212
213        content.push_str(&format!("\n*{}*", data.timestamp));
214
215        Self::new(content, position)
216    }
217}
218
219/// Structured data for tooltips
220#[derive(Debug, Clone)]
221pub struct TooltipData {
222    /// Tooltip title
223    pub title: String,
224    /// Key-value pairs
225    pub values: Vec<(String, String)>,
226    /// Timestamp
227    pub timestamp: String,
228}
229
230/// Brush selection for data filtering
231#[derive(Debug, Clone, PartialEq)]
232pub struct BrushSelection {
233    /// Start X coordinate
234    pub x1: f64,
235    /// Start Y coordinate
236    pub y1: f64,
237    /// End X coordinate
238    pub x2: f64,
239    /// End Y coordinate
240    pub y2: f64,
241}
242
243impl BrushSelection {
244    /// Create a new brush selection
245    pub fn new(start: (f64, f64), end: (f64, f64)) -> Self {
246        Self {
247            x1: start.0,
248            y1: start.1,
249            x2: end.0,
250            y2: end.1,
251        }
252    }
253
254    /// Get normalized brush (ensuring x1,y1 is top-left and x2,y2 is bottom-right)
255    pub fn normalized(&self) -> Self {
256        Self {
257            x1: self.x1.min(self.x2),
258            y1: self.y1.min(self.y2),
259            x2: self.x1.max(self.x2),
260            y2: self.y1.max(self.y2),
261        }
262    }
263
264    /// Check if a point is contained within the brush
265    pub fn contains_point(&self, x: f64, y: f64) -> bool {
266        let normalized = self.normalized();
267        x >= normalized.x1 && x <= normalized.x2 && y >= normalized.y1 && y <= normalized.y2
268    }
269
270    /// Check if brush intersects with a rectangle
271    pub fn intersects_rect(&self, rect: (f64, f64, f64, f64)) -> bool {
272        let (rect_x, rect_y, rect_width, rect_height) = rect;
273        let normalized = self.normalized();
274
275        !(normalized.x2 < rect_x
276            || normalized.x1 > rect_x + rect_width
277            || normalized.y2 < rect_y
278            || normalized.y1 > rect_y + rect_height)
279    }
280
281    /// Filter data points based on brush selection
282    pub fn filter_data(&self, data_points: &[DataPoint]) -> Vec<DataPoint> {
283        data_points
284            .iter()
285            .filter(|point| self.contains_point(point.x, point.y))
286            .cloned()
287            .collect()
288    }
289
290    /// Check if brush is empty
291    pub fn is_empty(&self) -> bool {
292        self.x1 == self.x2 && self.y1 == self.y2
293    }
294
295    /// Clear the brush selection
296    pub fn clear(&mut self) {
297        self.x1 = 0.0;
298        self.y1 = 0.0;
299        self.x2 = 0.0;
300        self.y2 = 0.0;
301    }
302}
303
304/// Cross-filtering between multiple charts
305#[derive(Debug)]
306pub struct CrossFilter {
307    /// Chart IDs that participate in cross-filtering
308    pub charts: Vec<String>,
309    /// Active filters by chart ID
310    pub active_filters: HashMap<String, BrushSelection>,
311    /// Filter events to propagate
312    filter_events: Vec<FilterEvent>,
313}
314
315#[derive(Debug, Clone)]
316pub struct FilterEvent {
317    /// Target chart ID
318    pub target_chart: String,
319    /// Filter to apply
320    pub filter: Option<BrushSelection>,
321}
322
323impl CrossFilter {
324    /// Create a new cross-filter
325    pub fn new(chart_ids: Vec<String>) -> Self {
326        Self {
327            charts: chart_ids,
328            active_filters: HashMap::new(),
329            filter_events: Vec::new(),
330        }
331    }
332
333    /// Add a filter for a specific chart
334    pub fn add_filter(&mut self, chart_id: &str, brush: BrushSelection) {
335        self.active_filters
336            .insert(chart_id.to_string(), brush.clone());
337
338        // Propagate filter to other charts
339        for other_chart in &self.charts {
340            if other_chart != chart_id {
341                self.filter_events.push(FilterEvent {
342                    target_chart: other_chart.clone(),
343                    filter: Some(brush.clone()),
344                });
345            }
346        }
347    }
348
349    /// Remove filter for a specific chart
350    pub fn remove_filter(&mut self, chart_id: &str) {
351        self.active_filters.remove(chart_id);
352
353        // Propagate filter removal to other charts
354        for other_chart in &self.charts {
355            if other_chart != chart_id {
356                self.filter_events.push(FilterEvent {
357                    target_chart: other_chart.clone(),
358                    filter: None,
359                });
360            }
361        }
362    }
363
364    /// Check if a chart has an active filter
365    pub fn has_filter(&self, chart_id: &str) -> bool {
366        self.active_filters.contains_key(chart_id)
367    }
368
369    /// Get filter events to propagate
370    pub fn get_filter_events(&mut self) -> Vec<FilterEvent> {
371        let events = self.filter_events.clone();
372        self.filter_events.clear();
373        events
374    }
375
376    /// Clear all filters
377    pub fn clear_all(&mut self) {
378        self.active_filters.clear();
379        self.filter_events.clear();
380    }
381
382    /// Get filtered data based on active filters
383    pub fn get_filtered_data(&self, shared_data: &Arc<Mutex<Vec<DataPoint>>>) -> Vec<DataPoint> {
384        let data = shared_data.lock().unwrap();
385        let mut filtered_data = data.clone();
386
387        // Apply all active filters
388        for brush in self.active_filters.values() {
389            filtered_data = brush.filter_data(&filtered_data);
390        }
391
392        filtered_data
393    }
394}
395
396/// Data point for testing and filtering
397#[derive(Debug, Clone, PartialEq)]
398pub struct DataPoint {
399    pub x: f64,
400    pub y: f64,
401    pub value: f64,
402}
403
404/// Interactive chart that combines all interactivity features
405#[derive(Debug)]
406pub struct InteractiveChart {
407    /// Viewport for zoom and pan
408    pub viewport: Viewport,
409    /// Tooltip for data display
410    pub tooltip: Tooltip,
411    /// Brush selection for filtering
412    pub brush: BrushSelection,
413    /// Cross-filter for multi-chart coordination
414    pub cross_filter: Option<CrossFilter>,
415    /// Chart data
416    pub data: Vec<DataPoint>,
417    /// Shared data for cross-filtering
418    pub shared_data: Option<Arc<Mutex<Vec<DataPoint>>>>,
419}
420
421impl InteractiveChart {
422    /// Create a new interactive chart
423    pub fn new(width: f64, height: f64) -> Self {
424        Self {
425            viewport: Viewport::new(width, height),
426            tooltip: Tooltip::new(String::new(), (0.0, 0.0)),
427            brush: BrushSelection::new((0.0, 0.0), (0.0, 0.0)),
428            cross_filter: None,
429            data: Vec::new(),
430            shared_data: None,
431        }
432    }
433
434    /// Set chart data
435    pub fn set_data(&mut self, data: Vec<DataPoint>) {
436        self.data = data;
437    }
438
439    /// Set shared data for cross-filtering
440    pub fn set_shared_data(&mut self, shared_data: Arc<Mutex<Vec<DataPoint>>>) {
441        self.shared_data = Some(shared_data);
442    }
443
444    /// Zoom the chart
445    pub fn zoom(&mut self, factor: f64, center_x: f64, center_y: f64) {
446        self.viewport.zoom(factor, center_x, center_y);
447    }
448
449    /// Pan the chart
450    pub fn pan(&mut self, dx: f64, dy: f64) {
451        self.viewport.pan(dx, dy);
452    }
453
454    /// Show tooltip at position
455    pub fn show_tooltip(&mut self, x: f64, y: f64) {
456        self.tooltip.update_position(x, y);
457        self.tooltip.auto_position(&self.viewport);
458        self.tooltip.show();
459    }
460
461    /// Start brush selection
462    pub fn start_brush_selection(&mut self, x: f64, y: f64) {
463        self.brush = BrushSelection::new((x, y), (x, y));
464    }
465
466    /// Update brush selection
467    pub fn update_brush_selection(&mut self, x: f64, y: f64) {
468        self.brush.x2 = x;
469        self.brush.y2 = y;
470    }
471
472    /// Finish brush selection
473    pub fn finish_brush_selection(&mut self) {
474        // Brush selection is complete
475    }
476
477    /// Apply brush filter
478    pub fn apply_brush_filter(&mut self, brush: BrushSelection) -> Vec<DataPoint> {
479        self.brush = brush.clone();
480
481        if let Some(ref mut cross_filter) = self.cross_filter {
482            cross_filter.add_filter("current_chart", brush.clone());
483        }
484
485        // Use shared data if available, otherwise use local data
486        if let Some(ref shared_data) = self.shared_data {
487            if let Some(ref cross_filter) = self.cross_filter {
488                cross_filter.get_filtered_data(shared_data)
489            } else {
490                self.brush.filter_data(&shared_data.lock().unwrap())
491            }
492        } else {
493            self.brush.filter_data(&self.data)
494        }
495    }
496
497    /// Get filtered data
498    pub fn get_filtered_data(&self) -> Vec<DataPoint> {
499        if let Some(ref shared_data) = self.shared_data {
500            if let Some(ref cross_filter) = self.cross_filter {
501                cross_filter.get_filtered_data(shared_data)
502            } else {
503                self.brush.filter_data(&self.data)
504            }
505        } else {
506            self.brush.filter_data(&self.data)
507        }
508    }
509}
510
511#[cfg(test)]
512mod tests {
513    use super::*;
514
515    #[test]
516    fn test_viewport_initialization() {
517        let viewport = Viewport::new(800.0, 600.0);
518        assert_eq!(viewport.x, 0.0);
519        assert_eq!(viewport.y, 0.0);
520        assert_eq!(viewport.scale, 1.0);
521        assert_eq!(viewport.width, 800.0);
522        assert_eq!(viewport.height, 600.0);
523    }
524
525    #[test]
526    fn test_viewport_zoom_in() {
527        let mut viewport = Viewport::new(800.0, 600.0);
528        viewport.zoom(2.0, 400.0, 300.0);
529        assert_eq!(viewport.scale, 2.0);
530        // When zooming 2x at center (400, 300), the viewport should be positioned
531        // so that the center point remains at the same screen position
532        // world_center_x = (400 - 0) / 1.0 = 400
533        // world_center_y = (300 - 0) / 1.0 = 300
534        // new x = 400 - 400 * 2.0 = -400
535        // new y = 300 - 300 * 2.0 = -300
536        assert_eq!(viewport.x, -400.0);
537        assert_eq!(viewport.y, -300.0);
538    }
539
540    #[test]
541    fn test_viewport_zoom_out() {
542        let mut viewport = Viewport::new(800.0, 600.0);
543        viewport.scale = 2.0;
544        viewport.x = 100.0;
545        viewport.y = 100.0;
546        viewport.zoom(0.5, 400.0, 300.0);
547        assert_eq!(viewport.scale, 1.0);
548    }
549
550    #[test]
551    fn test_viewport_pan() {
552        let mut viewport = Viewport::new(800.0, 600.0);
553        viewport.pan(50.0, 30.0);
554        assert_eq!(viewport.x, 50.0);
555        assert_eq!(viewport.y, 30.0);
556    }
557
558    #[test]
559    fn test_viewport_bounds_checking() {
560        let mut viewport = Viewport::new(800.0, 600.0);
561        viewport.set_bounds(0.0, 0.0, 1600.0, 1200.0);
562        viewport.pan(-100.0, -100.0);
563        assert_eq!(viewport.x, 0.0);
564        assert_eq!(viewport.y, 0.0);
565    }
566
567    #[test]
568    fn test_viewport_coordinate_transformation() {
569        let mut viewport = Viewport::new(800.0, 600.0);
570        viewport.x = 100.0;
571        viewport.y = 50.0;
572        viewport.scale = 2.0;
573        let world_pos = viewport.screen_to_world(200.0, 150.0);
574        assert_eq!(world_pos.0, 50.0);
575        assert_eq!(world_pos.1, 50.0);
576    }
577
578    #[test]
579    fn test_viewport_world_to_screen_transformation() {
580        let mut viewport = Viewport::new(800.0, 600.0);
581        viewport.x = 100.0;
582        viewport.y = 50.0;
583        viewport.scale = 2.0;
584        let screen_pos = viewport.world_to_screen(50.0, 25.0);
585        assert_eq!(screen_pos.0, 200.0);
586        assert_eq!(screen_pos.1, 100.0);
587    }
588
589    #[test]
590    fn test_tooltip_creation() {
591        let content = "Value: 42.5\nCategory: A\nTimestamp: 2025-01-01";
592        let position = (100.0, 200.0);
593        let tooltip = Tooltip::new(content.to_string(), position);
594        assert_eq!(tooltip.content, content);
595        assert_eq!(tooltip.position, position);
596        assert_eq!(tooltip.visible, false);
597    }
598
599    #[test]
600    fn test_tooltip_show_hide() {
601        let mut tooltip = Tooltip::new("Test".to_string(), (0.0, 0.0));
602        tooltip.show();
603        assert_eq!(tooltip.visible, true);
604        tooltip.hide();
605        assert_eq!(tooltip.visible, false);
606    }
607
608    #[test]
609    fn test_tooltip_position_update() {
610        let mut tooltip = Tooltip::new("Test".to_string(), (0.0, 0.0));
611        tooltip.update_position(150.0, 250.0);
612        assert_eq!(tooltip.position, (150.0, 250.0));
613    }
614
615    #[test]
616    fn test_tooltip_auto_positioning() {
617        let mut tooltip = Tooltip::new("Test".to_string(), (100.0, 100.0));
618        let viewport = Viewport::new(800.0, 600.0);
619        tooltip.auto_position(&viewport);
620        assert!(tooltip.position.0 >= 0.0);
621        assert!(tooltip.position.1 >= 0.0);
622        assert!(tooltip.position.0 <= viewport.width);
623        assert!(tooltip.position.1 <= viewport.height);
624    }
625
626    #[test]
627    fn test_tooltip_rich_content() {
628        let data = TooltipData {
629            title: "Stock Price".to_string(),
630            values: vec![
631                ("Price".to_string(), "125.50".to_string()),
632                ("Volume".to_string(), "1,250,000".to_string()),
633                ("Change".to_string(), "+2.5%".to_string()),
634            ],
635            timestamp: "2025-01-01 12:00:00".to_string(),
636        };
637        let tooltip = Tooltip::from_data(data, (100.0, 100.0));
638        assert!(tooltip.content.contains("Stock Price"));
639        assert!(tooltip.content.contains("Price: 125.50"));
640        assert!(tooltip.content.contains("Volume: 1,250,000"));
641        assert!(tooltip.content.contains("Change: +2.5%"));
642    }
643
644    #[test]
645    fn test_brush_creation() {
646        let start = (100.0, 100.0);
647        let end = (200.0, 200.0);
648        let brush = BrushSelection::new(start, end);
649        assert_eq!(brush.x1, 100.0);
650        assert_eq!(brush.y1, 100.0);
651        assert_eq!(brush.x2, 200.0);
652        assert_eq!(brush.y2, 200.0);
653    }
654
655    #[test]
656    fn test_brush_normalization() {
657        let brush = BrushSelection::new((200.0, 200.0), (100.0, 100.0));
658        let normalized = brush.normalized();
659        assert_eq!(normalized.x1, 100.0);
660        assert_eq!(normalized.y1, 100.0);
661        assert_eq!(normalized.x2, 200.0);
662        assert_eq!(normalized.y2, 200.0);
663    }
664
665    #[test]
666    fn test_brush_contains_point() {
667        let brush = BrushSelection::new((100.0, 100.0), (200.0, 200.0));
668        let inside = brush.contains_point(150.0, 150.0);
669        let outside = brush.contains_point(50.0, 50.0);
670        assert_eq!(inside, true);
671        assert_eq!(outside, false);
672    }
673
674    #[test]
675    fn test_brush_intersects_rect() {
676        let brush = BrushSelection::new((100.0, 100.0), (200.0, 200.0));
677        let rect = (150.0, 150.0, 100.0, 100.0); // x, y, width, height
678        let intersects = brush.intersects_rect(rect);
679        assert_eq!(intersects, true);
680    }
681
682    #[test]
683    fn test_brush_data_filtering() {
684        let data_points = vec![
685            DataPoint {
686                x: 50.0,
687                y: 50.0,
688                value: 10.0,
689            },
690            DataPoint {
691                x: 150.0,
692                y: 150.0,
693                value: 20.0,
694            },
695            DataPoint {
696                x: 250.0,
697                y: 250.0,
698                value: 30.0,
699            },
700        ];
701        let brush = BrushSelection::new((100.0, 100.0), (200.0, 200.0));
702        let filtered = brush.filter_data(&data_points);
703        assert_eq!(filtered.len(), 1);
704        assert_eq!(filtered[0].value, 20.0);
705    }
706
707    #[test]
708    fn test_brush_clear() {
709        let mut brush = BrushSelection::new((100.0, 100.0), (200.0, 200.0));
710        brush.clear();
711        assert_eq!(brush.is_empty(), true);
712    }
713
714    #[test]
715    fn test_cross_filter_initialization() {
716        let chart_ids = vec!["chart1".to_string(), "chart2".to_string()];
717        let cross_filter = CrossFilter::new(chart_ids);
718        assert_eq!(cross_filter.charts.len(), 2);
719        assert_eq!(cross_filter.active_filters.len(), 0);
720    }
721
722    #[test]
723    fn test_cross_filter_add_filter() {
724        let mut cross_filter = CrossFilter::new(vec!["chart1".to_string()]);
725        let brush = BrushSelection::new((100.0, 100.0), (200.0, 200.0));
726        cross_filter.add_filter("chart1", brush);
727        assert_eq!(cross_filter.active_filters.len(), 1);
728        assert!(cross_filter.has_filter("chart1"));
729    }
730
731    #[test]
732    fn test_cross_filter_remove_filter() {
733        let mut cross_filter = CrossFilter::new(vec!["chart1".to_string()]);
734        let brush = BrushSelection::new((100.0, 100.0), (200.0, 200.0));
735        cross_filter.add_filter("chart1", brush);
736        cross_filter.remove_filter("chart1");
737        assert_eq!(cross_filter.active_filters.len(), 0);
738        assert!(!cross_filter.has_filter("chart1"));
739    }
740
741    #[test]
742    fn test_cross_filter_propagate_filter() {
743        let mut cross_filter = CrossFilter::new(vec!["chart1".to_string(), "chart2".to_string()]);
744        let brush = BrushSelection::new((100.0, 100.0), (200.0, 200.0));
745        cross_filter.add_filter("chart1", brush);
746        let events = cross_filter.get_filter_events();
747        assert_eq!(events.len(), 1);
748        assert_eq!(events[0].target_chart, "chart2");
749    }
750
751    #[test]
752    fn test_cross_filter_clear_all() {
753        let mut cross_filter = CrossFilter::new(vec!["chart1".to_string(), "chart2".to_string()]);
754        let brush1 = BrushSelection::new((100.0, 100.0), (200.0, 200.0));
755        let brush2 = BrushSelection::new((300.0, 300.0), (400.0, 400.0));
756        cross_filter.add_filter("chart1", brush1);
757        cross_filter.add_filter("chart2", brush2);
758        cross_filter.clear_all();
759        assert_eq!(cross_filter.active_filters.len(), 0);
760    }
761
762    #[test]
763    fn test_cross_filter_data_synchronization() {
764        let mut cross_filter = CrossFilter::new(vec!["chart1".to_string(), "chart2".to_string()]);
765        let shared_data = Arc::new(Mutex::new(vec![
766            DataPoint {
767                x: 50.0,
768                y: 50.0,
769                value: 10.0,
770            },
771            DataPoint {
772                x: 150.0,
773                y: 150.0,
774                value: 20.0,
775            },
776            DataPoint {
777                x: 250.0,
778                y: 250.0,
779                value: 30.0,
780            },
781        ]));
782        let brush = BrushSelection::new((100.0, 100.0), (200.0, 200.0));
783        cross_filter.add_filter("chart1", brush);
784        let filtered_data = cross_filter.get_filtered_data(&shared_data);
785        assert_eq!(filtered_data.len(), 1);
786        assert_eq!(filtered_data[0].value, 20.0);
787    }
788
789    #[test]
790    fn test_interactive_chart_workflow() {
791        let mut chart = InteractiveChart::new(800.0, 600.0);
792        let data = vec![
793            DataPoint {
794                x: 100.0,
795                y: 100.0,
796                value: 10.0,
797            },
798            DataPoint {
799                x: 200.0,
800                y: 200.0,
801                value: 20.0,
802            },
803            DataPoint {
804                x: 300.0,
805                y: 300.0,
806                value: 30.0,
807            },
808        ];
809        chart.set_data(data);
810        chart.zoom(2.0, 200.0, 200.0);
811        chart.pan(50.0, 50.0);
812        chart.show_tooltip(200.0, 200.0);
813        chart.start_brush_selection(150.0, 150.0);
814        chart.update_brush_selection(250.0, 250.0);
815        chart.finish_brush_selection();
816        assert_eq!(chart.viewport.scale, 2.0);
817        assert_eq!(chart.viewport.x, -150.0);
818        assert_eq!(chart.viewport.y, -150.0);
819        assert!(chart.tooltip.visible);
820        assert!(!chart.brush.is_empty());
821    }
822
823    #[test]
824    fn test_multi_chart_cross_filtering() {
825        let mut chart1 = InteractiveChart::new(400.0, 300.0);
826        let mut chart2 = InteractiveChart::new(400.0, 300.0);
827        let shared_data = Arc::new(Mutex::new(vec![
828            DataPoint {
829                x: 100.0,
830                y: 100.0,
831                value: 10.0,
832            },
833            DataPoint {
834                x: 200.0,
835                y: 200.0,
836                value: 20.0,
837            },
838            DataPoint {
839                x: 300.0,
840                y: 300.0,
841                value: 30.0,
842            },
843        ]));
844        chart1.set_shared_data(shared_data.clone());
845        chart2.set_shared_data(shared_data.clone());
846
847        // Set up cross-filter
848        let cross_filter = CrossFilter::new(vec!["chart1".to_string(), "chart2".to_string()]);
849        chart1.cross_filter = Some(cross_filter);
850
851        let brush = BrushSelection::new((150.0, 150.0), (250.0, 250.0));
852        let filtered = chart1.apply_brush_filter(brush);
853        assert_eq!(filtered.len(), 1);
854        assert_eq!(filtered[0].value, 20.0);
855    }
856
857    #[test]
858    fn test_performance_with_large_dataset() {
859        let mut data = Vec::new();
860        for i in 0..10000 {
861            data.push(DataPoint {
862                x: (i as f64) * 0.1,
863                y: (i as f64) * 0.1,
864                value: (i as f64) * 0.01,
865            });
866        }
867        let mut chart = InteractiveChart::new(800.0, 600.0);
868        chart.set_data(data);
869        let start = std::time::Instant::now();
870        chart.zoom(2.0, 400.0, 300.0);
871        chart.pan(100.0, 100.0);
872        let brush = BrushSelection::new((200.0, 200.0), (400.0, 400.0));
873        let filtered = chart.apply_brush_filter(brush);
874        let duration = start.elapsed();
875        assert!(duration.as_millis() < 100);
876        assert!(filtered.len() > 0);
877    }
878}