1use std::collections::HashMap;
12use std::sync::{Arc, Mutex};
13
14#[derive(Debug, Clone, PartialEq)]
16pub struct Viewport {
17 pub x: f64,
19 pub y: f64,
21 pub scale: f64,
23 pub width: f64,
25 pub height: f64,
27 min_x: Option<f64>,
29 max_x: Option<f64>,
31 min_y: Option<f64>,
33 max_y: Option<f64>,
35}
36
37impl Viewport {
38 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 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 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 self.scale = self.scale.max(0.1).min(10.0);
68
69 let world_center_x = (center_x - self.x) / old_scale;
72 let world_center_y = (center_y - self.y) / old_scale;
73
74 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 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 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 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 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#[derive(Debug, Clone)]
121pub struct Tooltip {
122 pub content: String,
124 pub position: (f64, f64),
126 pub visible: bool,
128 pub style: TooltipStyle,
130}
131
132#[derive(Debug, Clone)]
133pub struct TooltipStyle {
134 pub background_color: String,
136 pub text_color: String,
138 pub border_color: String,
140 pub font_size: f64,
142 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 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 pub fn show(&mut self) {
171 self.visible = true;
172 }
173
174 pub fn hide(&mut self) {
176 self.visible = false;
177 }
178
179 pub fn update_position(&mut self, x: f64, y: f64) {
181 self.position = (x, y);
182 }
183
184 pub fn auto_position(&mut self, viewport: &Viewport) {
186 let (x, y) = self.position;
187 let tooltip_width = 200.0; let tooltip_height = 100.0; 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 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#[derive(Debug, Clone)]
221pub struct TooltipData {
222 pub title: String,
224 pub values: Vec<(String, String)>,
226 pub timestamp: String,
228}
229
230#[derive(Debug, Clone, PartialEq)]
232pub struct BrushSelection {
233 pub x1: f64,
235 pub y1: f64,
237 pub x2: f64,
239 pub y2: f64,
241}
242
243impl BrushSelection {
244 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 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 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 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 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 pub fn is_empty(&self) -> bool {
292 self.x1 == self.x2 && self.y1 == self.y2
293 }
294
295 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#[derive(Debug)]
306pub struct CrossFilter {
307 pub charts: Vec<String>,
309 pub active_filters: HashMap<String, BrushSelection>,
311 filter_events: Vec<FilterEvent>,
313}
314
315#[derive(Debug, Clone)]
316pub struct FilterEvent {
317 pub target_chart: String,
319 pub filter: Option<BrushSelection>,
321}
322
323impl CrossFilter {
324 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 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 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 pub fn remove_filter(&mut self, chart_id: &str) {
351 self.active_filters.remove(chart_id);
352
353 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 pub fn has_filter(&self, chart_id: &str) -> bool {
366 self.active_filters.contains_key(chart_id)
367 }
368
369 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 pub fn clear_all(&mut self) {
378 self.active_filters.clear();
379 self.filter_events.clear();
380 }
381
382 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 for brush in self.active_filters.values() {
389 filtered_data = brush.filter_data(&filtered_data);
390 }
391
392 filtered_data
393 }
394}
395
396#[derive(Debug, Clone, PartialEq)]
398pub struct DataPoint {
399 pub x: f64,
400 pub y: f64,
401 pub value: f64,
402}
403
404#[derive(Debug)]
406pub struct InteractiveChart {
407 pub viewport: Viewport,
409 pub tooltip: Tooltip,
411 pub brush: BrushSelection,
413 pub cross_filter: Option<CrossFilter>,
415 pub data: Vec<DataPoint>,
417 pub shared_data: Option<Arc<Mutex<Vec<DataPoint>>>>,
419}
420
421impl InteractiveChart {
422 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 pub fn set_data(&mut self, data: Vec<DataPoint>) {
436 self.data = data;
437 }
438
439 pub fn set_shared_data(&mut self, shared_data: Arc<Mutex<Vec<DataPoint>>>) {
441 self.shared_data = Some(shared_data);
442 }
443
444 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 pub fn pan(&mut self, dx: f64, dy: f64) {
451 self.viewport.pan(dx, dy);
452 }
453
454 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 pub fn start_brush_selection(&mut self, x: f64, y: f64) {
463 self.brush = BrushSelection::new((x, y), (x, y));
464 }
465
466 pub fn update_brush_selection(&mut self, x: f64, y: f64) {
468 self.brush.x2 = x;
469 self.brush.y2 = y;
470 }
471
472 pub fn finish_brush_selection(&mut self) {
474 }
476
477 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 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 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 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); 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 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}