1use anyhow::Result;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11
12use super::types::*;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ModernPlottingConfig {
17 pub enable_interactive: bool,
19 pub enable_realtime: bool,
21 pub enable_web_dashboard: bool,
23 pub backend: PlottingBackend,
25 pub output_directory: String,
27 pub dashboard_port: u16,
29 pub max_data_points: usize,
31 pub refresh_interval_ms: u64,
33 pub enable_animations: bool,
35 pub animation_fps: u32,
37}
38
39impl Default for ModernPlottingConfig {
40 fn default() -> Self {
41 Self {
42 enable_interactive: true,
43 enable_realtime: true,
44 enable_web_dashboard: true,
45 backend: PlottingBackend::PlotlyJS,
46 output_directory: "./modern_debug_plots".to_string(),
47 dashboard_port: 8888,
48 max_data_points: 10000,
49 refresh_interval_ms: 1000,
50 enable_animations: true,
51 animation_fps: 30,
52 }
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub enum PlottingBackend {
59 PlotlyJS,
61 D3JS,
63 ChartJS,
65 ThreeJS,
67 Matplotlib,
69 Bokeh,
71 WebGL,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub enum InteractivePlotType {
78 InteractiveLinePlot,
80 InteractiveScatterPlot,
82 InteractiveHeatmap,
84 Interactive3DSurface,
86 RealtimeStreamingPlot,
88 AnimatedTrainingPlot,
90 InteractiveNetworkDiagram,
92 MultiPlotDashboard,
94 InteractiveHistogram,
96 ParallelCoordinatesPlot,
98 InteractiveCorrelationMatrix,
100 TimeSeriesWithRangeSelector,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct InteractivePlotData {
107 pub plot_data: PlotData,
109 pub interactive_config: InteractiveConfig,
111 pub styling: PlotStyling,
113 pub animation_config: Option<AnimationConfig>,
115 pub realtime_config: Option<RealtimeConfig>,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct InteractiveConfig {
122 pub enable_zoom: bool,
124 pub enable_pan: bool,
126 pub enable_hover: bool,
128 pub enable_selection: bool,
130 pub enable_brush: bool,
132 pub enable_crossfilter: bool,
134 pub event_handlers: HashMap<String, String>,
136}
137
138impl Default for InteractiveConfig {
139 fn default() -> Self {
140 Self {
141 enable_zoom: true,
142 enable_pan: true,
143 enable_hover: true,
144 enable_selection: true,
145 enable_brush: false,
146 enable_crossfilter: false,
147 event_handlers: HashMap::new(),
148 }
149 }
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct PlotStyling {
155 pub color_palette: Vec<String>,
157 pub font_config: FontConfig,
159 pub line_styles: Vec<LineStyle>,
161 pub marker_styles: Vec<MarkerStyle>,
163 pub background_color: String,
165 pub grid_config: GridConfig,
167 pub legend_config: LegendConfig,
169 pub custom_css: Option<String>,
171}
172
173impl Default for PlotStyling {
174 fn default() -> Self {
175 Self {
176 color_palette: vec![
177 "#1f77b4".to_string(),
178 "#ff7f0e".to_string(),
179 "#2ca02c".to_string(),
180 "#d62728".to_string(),
181 "#9467bd".to_string(),
182 "#8c564b".to_string(),
183 "#e377c2".to_string(),
184 "#7f7f7f".to_string(),
185 "#bcbd22".to_string(),
186 "#17becf".to_string(),
187 ],
188 font_config: FontConfig::default(),
189 line_styles: vec![LineStyle::Solid, LineStyle::Dashed, LineStyle::Dotted],
190 marker_styles: vec![
191 MarkerStyle::Circle,
192 MarkerStyle::Square,
193 MarkerStyle::Triangle,
194 ],
195 background_color: "#ffffff".to_string(),
196 grid_config: GridConfig::default(),
197 legend_config: LegendConfig::default(),
198 custom_css: None,
199 }
200 }
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct FontConfig {
206 pub family: String,
207 pub size: u32,
208 pub weight: FontWeight,
209 pub color: String,
210}
211
212impl Default for FontConfig {
213 fn default() -> Self {
214 Self {
215 family: "Arial, sans-serif".to_string(),
216 size: 12,
217 weight: FontWeight::Normal,
218 color: "#000000".to_string(),
219 }
220 }
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
225pub enum FontWeight {
226 Normal,
227 Bold,
228 Light,
229 ExtraBold,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234pub enum LineStyle {
235 Solid,
236 Dashed,
237 Dotted,
238 DashDot,
239 None,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244pub enum MarkerStyle {
245 Circle,
246 Square,
247 Triangle,
248 Diamond,
249 Cross,
250 Plus,
251 Star,
252 None,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct GridConfig {
258 pub show_x_grid: bool,
259 pub show_y_grid: bool,
260 pub grid_color: String,
261 pub grid_alpha: f64,
262 pub grid_width: f64,
263}
264
265impl Default for GridConfig {
266 fn default() -> Self {
267 Self {
268 show_x_grid: true,
269 show_y_grid: true,
270 grid_color: "#cccccc".to_string(),
271 grid_alpha: 0.5,
272 grid_width: 1.0,
273 }
274 }
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct LegendConfig {
280 pub show_legend: bool,
281 pub position: LegendPosition,
282 pub background_color: String,
283 pub border_color: String,
284 pub border_width: f64,
285}
286
287impl Default for LegendConfig {
288 fn default() -> Self {
289 Self {
290 show_legend: true,
291 position: LegendPosition::TopRight,
292 background_color: "rgba(255, 255, 255, 0.8)".to_string(),
293 border_color: "#cccccc".to_string(),
294 border_width: 1.0,
295 }
296 }
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize)]
301pub enum LegendPosition {
302 TopLeft,
303 TopRight,
304 BottomLeft,
305 BottomRight,
306 Top,
307 Bottom,
308 Left,
309 Right,
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct AnimationConfig {
315 pub animation_type: AnimationType,
317 pub duration_ms: u64,
319 pub easing: EasingFunction,
321 pub frames: u32,
323 pub loop_animation: bool,
325 pub auto_start: bool,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
331pub enum AnimationType {
332 FadeIn,
334 SlideIn,
336 Grow,
338 TrainingProgress,
340 GradientFlow,
342 LossLandscapeFlythrough,
344 Custom(String),
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
350pub enum EasingFunction {
351 Linear,
352 EaseIn,
353 EaseOut,
354 EaseInOut,
355 Bounce,
356 Elastic,
357 Back,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
362pub struct RealtimeConfig {
363 pub buffer_size: usize,
365 pub update_frequency_ms: u64,
367 pub streaming_mode: bool,
369 pub data_source: DataSource,
371 pub auto_scroll: bool,
373 pub time_window_seconds: f64,
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize)]
379pub enum DataSource {
380 WebSocket { url: String },
382 HttpPolling { url: String, interval_ms: u64 },
384 FileWatching { path: String },
386 MemoryBuffer { buffer_id: String },
388 CustomFunction { function_name: String },
390}
391
392#[derive(Debug)]
394pub struct ModernPlottingEngine {
395 config: ModernPlottingConfig,
396 active_plots: HashMap<String, PlotInstance>,
397 dashboard_server: Option<DashboardServer>,
398 #[allow(dead_code)]
399 plot_cache: HashMap<String, CachedPlot>,
400}
401
402#[derive(Debug, Clone)]
404pub struct PlotInstance {
405 pub id: String,
406 pub plot_type: InteractivePlotType,
407 pub data: InteractivePlotData,
408 pub creation_time: DateTime<Utc>,
409 pub last_update: DateTime<Utc>,
410 pub file_path: Option<PathBuf>,
411 pub is_realtime: bool,
412 pub update_count: u64,
413}
414
415#[derive(Debug)]
417pub struct DashboardServer {
418 port: u16,
419 plots: HashMap<String, String>, is_running: bool,
421}
422
423#[derive(Debug, Clone)]
425pub struct CachedPlot {
426 pub content: String,
427 pub hash: u64,
428 pub creation_time: DateTime<Utc>,
429 pub access_count: u64,
430}
431
432impl ModernPlottingEngine {
433 pub fn new(config: ModernPlottingConfig) -> Self {
435 std::fs::create_dir_all(&config.output_directory).ok();
436
437 Self {
438 config,
439 active_plots: HashMap::new(),
440 dashboard_server: None,
441 plot_cache: HashMap::new(),
442 }
443 }
444
445 pub async fn create_interactive_line_plot(
447 &mut self,
448 plot_data: InteractivePlotData,
449 plot_id: Option<String>,
450 ) -> Result<String> {
451 let id = plot_id.unwrap_or_else(|| format!("line_plot_{}", Utc::now().timestamp()));
452
453 let html_content = self.generate_plotly_line_plot(&plot_data)?;
454 let file_path = self.save_plot_to_file(&id, &html_content).await?;
455
456 let instance = PlotInstance {
457 id: id.clone(),
458 plot_type: InteractivePlotType::InteractiveLinePlot,
459 data: plot_data,
460 creation_time: Utc::now(),
461 last_update: Utc::now(),
462 file_path: Some(file_path),
463 is_realtime: false,
464 update_count: 0,
465 };
466
467 self.active_plots.insert(id.clone(), instance);
468
469 if self.config.enable_web_dashboard {
470 self.add_plot_to_dashboard(&id, &html_content).await?;
471 }
472
473 Ok(id)
474 }
475
476 pub async fn create_interactive_scatter_plot(
478 &mut self,
479 x_values: &[f64],
480 y_values: &[f64],
481 labels: Option<&[String]>,
482 title: &str,
483 plot_id: Option<String>,
484 ) -> Result<String> {
485 let id = plot_id.unwrap_or_else(|| format!("scatter_plot_{}", Utc::now().timestamp()));
486
487 let plot_data = InteractivePlotData {
488 plot_data: PlotData {
489 x_values: x_values.to_vec(),
490 y_values: y_values.to_vec(),
491 labels: labels.map(|l| l.to_vec()).unwrap_or_else(|| vec!["Series 1".to_string()]),
492 title: title.to_string(),
493 x_label: "X".to_string(),
494 y_label: "Y".to_string(),
495 },
496 interactive_config: InteractiveConfig::default(),
497 styling: PlotStyling::default(),
498 animation_config: None,
499 realtime_config: None,
500 };
501
502 let html_content = self.generate_plotly_scatter_plot(&plot_data)?;
503 let file_path = self.save_plot_to_file(&id, &html_content).await?;
504
505 let instance = PlotInstance {
506 id: id.clone(),
507 plot_type: InteractivePlotType::InteractiveScatterPlot,
508 data: plot_data,
509 creation_time: Utc::now(),
510 last_update: Utc::now(),
511 file_path: Some(file_path),
512 is_realtime: false,
513 update_count: 0,
514 };
515
516 self.active_plots.insert(id.clone(), instance);
517
518 if self.config.enable_web_dashboard {
519 self.add_plot_to_dashboard(&id, &html_content).await?;
520 }
521
522 Ok(id)
523 }
524
525 pub async fn create_interactive_heatmap(
527 &mut self,
528 values: &[Vec<f64>],
529 x_labels: Option<&[String]>,
530 y_labels: Option<&[String]>,
531 title: &str,
532 plot_id: Option<String>,
533 ) -> Result<String> {
534 let id = plot_id.unwrap_or_else(|| format!("heatmap_{}", Utc::now().timestamp()));
535
536 let default_x_labels: Vec<String> = (0..values.first().map_or(0, |row| row.len()))
537 .map(|i| format!("Col_{}", i))
538 .collect();
539 let default_y_labels: Vec<String> =
540 (0..values.len()).map(|i| format!("Row_{}", i)).collect();
541
542 let heatmap_data = HeatmapData {
543 values: values.to_vec(),
544 x_labels: x_labels.map(|l| l.to_vec()).unwrap_or(default_x_labels),
545 y_labels: y_labels.map(|l| l.to_vec()).unwrap_or(default_y_labels),
546 title: title.to_string(),
547 color_bar_label: "Value".to_string(),
548 };
549
550 let html_content = self.generate_plotly_heatmap(&heatmap_data)?;
551 let file_path = self.save_plot_to_file(&id, &html_content).await?;
552
553 let plot_data = InteractivePlotData {
554 plot_data: PlotData {
555 x_values: vec![],
556 y_values: vec![],
557 labels: vec![],
558 title: title.to_string(),
559 x_label: "X".to_string(),
560 y_label: "Y".to_string(),
561 },
562 interactive_config: InteractiveConfig::default(),
563 styling: PlotStyling::default(),
564 animation_config: None,
565 realtime_config: None,
566 };
567
568 let instance = PlotInstance {
569 id: id.clone(),
570 plot_type: InteractivePlotType::InteractiveHeatmap,
571 data: plot_data,
572 creation_time: Utc::now(),
573 last_update: Utc::now(),
574 file_path: Some(file_path),
575 is_realtime: false,
576 update_count: 0,
577 };
578
579 self.active_plots.insert(id.clone(), instance);
580
581 if self.config.enable_web_dashboard {
582 self.add_plot_to_dashboard(&id, &html_content).await?;
583 }
584
585 Ok(id)
586 }
587
588 pub async fn create_realtime_plot(
590 &mut self,
591 title: &str,
592 plot_id: Option<String>,
593 realtime_config: RealtimeConfig,
594 ) -> Result<String> {
595 let id = plot_id.unwrap_or_else(|| format!("realtime_plot_{}", Utc::now().timestamp()));
596
597 let plot_data = InteractivePlotData {
598 plot_data: PlotData {
599 x_values: vec![],
600 y_values: vec![],
601 labels: vec!["Real-time Data".to_string()],
602 title: title.to_string(),
603 x_label: "Time".to_string(),
604 y_label: "Value".to_string(),
605 },
606 interactive_config: InteractiveConfig::default(),
607 styling: PlotStyling::default(),
608 animation_config: None,
609 realtime_config: Some(realtime_config),
610 };
611
612 let html_content = self.generate_realtime_plot(&plot_data)?;
613 let file_path = self.save_plot_to_file(&id, &html_content).await?;
614
615 let instance = PlotInstance {
616 id: id.clone(),
617 plot_type: InteractivePlotType::RealtimeStreamingPlot,
618 data: plot_data,
619 creation_time: Utc::now(),
620 last_update: Utc::now(),
621 file_path: Some(file_path),
622 is_realtime: true,
623 update_count: 0,
624 };
625
626 self.active_plots.insert(id.clone(), instance);
627
628 if self.config.enable_web_dashboard {
629 self.add_plot_to_dashboard(&id, &html_content).await?;
630 }
631
632 Ok(id)
633 }
634
635 pub async fn create_animated_training_plot(
637 &mut self,
638 training_data: &[f64],
639 validation_data: &[f64],
640 epochs: &[u32],
641 title: &str,
642 plot_id: Option<String>,
643 ) -> Result<String> {
644 let id = plot_id.unwrap_or_else(|| format!("animated_training_{}", Utc::now().timestamp()));
645
646 let animation_config = AnimationConfig {
647 animation_type: AnimationType::TrainingProgress,
648 duration_ms: 5000,
649 easing: EasingFunction::EaseInOut,
650 frames: epochs.len() as u32,
651 loop_animation: false,
652 auto_start: true,
653 };
654
655 let plot_data = InteractivePlotData {
656 plot_data: PlotData {
657 x_values: epochs.iter().map(|&e| e as f64).collect(),
658 y_values: training_data.to_vec(),
659 labels: vec!["Training Loss".to_string(), "Validation Loss".to_string()],
660 title: title.to_string(),
661 x_label: "Epoch".to_string(),
662 y_label: "Loss".to_string(),
663 },
664 interactive_config: InteractiveConfig::default(),
665 styling: PlotStyling::default(),
666 animation_config: Some(animation_config),
667 realtime_config: None,
668 };
669
670 let html_content = self.generate_animated_training_plot(&plot_data, validation_data)?;
671 let file_path = self.save_plot_to_file(&id, &html_content).await?;
672
673 let instance = PlotInstance {
674 id: id.clone(),
675 plot_type: InteractivePlotType::AnimatedTrainingPlot,
676 data: plot_data,
677 creation_time: Utc::now(),
678 last_update: Utc::now(),
679 file_path: Some(file_path),
680 is_realtime: false,
681 update_count: 0,
682 };
683
684 self.active_plots.insert(id.clone(), instance);
685
686 if self.config.enable_web_dashboard {
687 self.add_plot_to_dashboard(&id, &html_content).await?;
688 }
689
690 Ok(id)
691 }
692
693 pub async fn create_dashboard(&mut self, plot_ids: &[String], title: &str) -> Result<String> {
695 let dashboard_id = format!("dashboard_{}", Utc::now().timestamp());
696
697 let mut dashboard_html = self.generate_dashboard_template(title)?;
698
699 for plot_id in plot_ids {
700 if let Some(plot_instance) = self.active_plots.get(plot_id) {
701 let plot_html = self.get_plot_html_content(plot_instance)?;
702 dashboard_html =
703 self.embed_plot_in_dashboard(&dashboard_html, plot_id, &plot_html)?;
704 }
705 }
706
707 dashboard_html = self.finalize_dashboard_html(&dashboard_html)?;
708
709 let dashboard_path =
710 Path::new(&self.config.output_directory).join(format!("{}.html", dashboard_id));
711 tokio::fs::write(&dashboard_path, &dashboard_html).await?;
712
713 if self.config.enable_web_dashboard {
714 self.start_dashboard_server().await?;
715 }
716
717 Ok(dashboard_path.to_string_lossy().to_string())
718 }
719
720 pub async fn update_realtime_plot(
722 &mut self,
723 plot_id: &str,
724 new_x: f64,
725 new_y: f64,
726 ) -> Result<()> {
727 let should_update_dashboard = self.config.enable_web_dashboard;
728 let mut plot_data_for_dashboard = None;
729
730 if let Some(plot_instance) = self.active_plots.get_mut(plot_id) {
731 if plot_instance.is_realtime {
732 plot_instance.data.plot_data.x_values.push(new_x);
734 plot_instance.data.plot_data.y_values.push(new_y);
735
736 if let Some(ref realtime_config) = plot_instance.data.realtime_config {
738 let buffer_size = realtime_config.buffer_size;
739 if plot_instance.data.plot_data.x_values.len() > buffer_size {
740 plot_instance.data.plot_data.x_values.remove(0);
741 plot_instance.data.plot_data.y_values.remove(0);
742 }
743 }
744
745 plot_instance.last_update = Utc::now();
746 plot_instance.update_count += 1;
747
748 if should_update_dashboard {
750 plot_data_for_dashboard = Some(plot_instance.data.clone());
751 }
752 }
753 }
754
755 if let Some(data) = plot_data_for_dashboard {
757 self.update_plot_in_dashboard(plot_id, &data).await?;
758 }
759
760 Ok(())
761 }
762
763 pub fn get_plot_statistics(&self, plot_id: &str) -> Option<PlotStatistics> {
765 self.active_plots.get(plot_id).map(|instance| PlotStatistics {
766 plot_id: plot_id.to_string(),
767 plot_type: instance.plot_type.clone(),
768 creation_time: instance.creation_time,
769 last_update: instance.last_update,
770 update_count: instance.update_count,
771 data_points: instance.data.plot_data.x_values.len(),
772 is_realtime: instance.is_realtime,
773 file_size_bytes: instance
774 .file_path
775 .as_ref()
776 .and_then(|path| std::fs::metadata(path).ok())
777 .map(|metadata| metadata.len())
778 .unwrap_or(0),
779 })
780 }
781
782 pub fn list_active_plots(&self) -> Vec<String> {
784 self.active_plots.keys().cloned().collect()
785 }
786
787 pub async fn remove_plot(&mut self, plot_id: &str) -> Result<()> {
789 if let Some(instance) = self.active_plots.remove(plot_id) {
790 if let Some(file_path) = instance.file_path {
792 tokio::fs::remove_file(file_path).await.ok();
793 }
794
795 if self.config.enable_web_dashboard {
797 self.remove_plot_from_dashboard(plot_id).await?;
798 }
799 }
800
801 Ok(())
802 }
803
804 fn generate_plotly_line_plot(&self, data: &InteractivePlotData) -> Result<String> {
807 let plot_data = &data.plot_data;
808 let styling = &data.styling;
809
810 let mut html = String::from(
811 r#"
812<!DOCTYPE html>
813<html>
814<head>
815 <script src="https://cdn.plot.ly/plotly-2.26.0.min.js"></script>
816 <title>Interactive Line Plot</title>
817</head>
818<body>
819 <div id="plotDiv" style="width:100%;height:600px;"></div>
820 <script>
821 var trace = {
822 x: ["#,
823 );
824
825 html.push_str(
827 &plot_data.x_values.iter().map(|x| x.to_string()).collect::<Vec<_>>().join(", "),
828 );
829
830 html.push_str(
831 r#"],
832 y: ["#,
833 );
834
835 html.push_str(
837 &plot_data.y_values.iter().map(|y| y.to_string()).collect::<Vec<_>>().join(", "),
838 );
839
840 html.push_str(&format!(
841 r#"],
842 type: 'scatter',
843 mode: 'lines+markers',
844 name: '{}',
845 line: {{
846 color: '{}',
847 width: 2
848 }},
849 marker: {{
850 size: 6,
851 color: '{}'
852 }}
853 }};
854
855 var layout = {{
856 title: '{}',
857 xaxis: {{
858 title: '{}',
859 showgrid: {},
860 gridcolor: '{}'
861 }},
862 yaxis: {{
863 title: '{}',
864 showgrid: {},
865 gridcolor: '{}'
866 }},
867 font: {{
868 family: '{}',
869 size: {},
870 color: '{}'
871 }},
872 plot_bgcolor: '{}',
873 paper_bgcolor: '{}'
874 }};
875
876 var config = {{
877 responsive: true,
878 displayModeBar: true,
879 modeBarButtonsToAdd: ['pan2d', 'select2d', 'lasso2d', 'resetScale2d'],
880 toImageButtonOptions: {{
881 format: 'png',
882 filename: 'debug_plot',
883 height: 600,
884 width: 800,
885 scale: 1
886 }}
887 }};
888
889 Plotly.newPlot('plotDiv', [trace], layout, config);
890 </script>
891</body>
892</html>"#,
893 plot_data.labels.first().unwrap_or(&"Series 1".to_string()),
894 styling.color_palette.first().unwrap_or(&"#1f77b4".to_string()),
895 styling.color_palette.first().unwrap_or(&"#1f77b4".to_string()),
896 plot_data.title,
897 plot_data.x_label,
898 styling.grid_config.show_x_grid,
899 styling.grid_config.grid_color,
900 plot_data.y_label,
901 styling.grid_config.show_y_grid,
902 styling.grid_config.grid_color,
903 styling.font_config.family,
904 styling.font_config.size,
905 styling.font_config.color,
906 styling.background_color,
907 styling.background_color
908 ));
909
910 Ok(html)
911 }
912
913 fn generate_plotly_scatter_plot(&self, data: &InteractivePlotData) -> Result<String> {
914 let plot_data = &data.plot_data;
915 let styling = &data.styling;
916
917 let html = format!(
918 r#"
919<!DOCTYPE html>
920<html>
921<head>
922 <script src="https://cdn.plot.ly/plotly-2.26.0.min.js"></script>
923 <title>Interactive Scatter Plot</title>
924</head>
925<body>
926 <div id="plotDiv" style="width:100%;height:600px;"></div>
927 <script>
928 var trace = {{
929 x: [{}],
930 y: [{}],
931 mode: 'markers',
932 type: 'scatter',
933 name: '{}',
934 marker: {{
935 size: 8,
936 color: '{}',
937 opacity: 0.7,
938 line: {{
939 color: '{}',
940 width: 1
941 }}
942 }}
943 }};
944
945 var layout = {{
946 title: '{}',
947 xaxis: {{
948 title: '{}',
949 showgrid: true
950 }},
951 yaxis: {{
952 title: '{}',
953 showgrid: true
954 }},
955 hovermode: 'closest'
956 }};
957
958 var config = {{
959 responsive: true,
960 displayModeBar: true
961 }};
962
963 Plotly.newPlot('plotDiv', [trace], layout, config);
964 </script>
965</body>
966</html>"#,
967 plot_data.x_values.iter().map(|x| x.to_string()).collect::<Vec<_>>().join(", "),
968 plot_data.y_values.iter().map(|y| y.to_string()).collect::<Vec<_>>().join(", "),
969 plot_data.labels.first().unwrap_or(&"Series 1".to_string()),
970 styling.color_palette.first().unwrap_or(&"#1f77b4".to_string()),
971 styling.color_palette.first().unwrap_or(&"#1f77b4".to_string()),
972 plot_data.title,
973 plot_data.x_label,
974 plot_data.y_label
975 );
976
977 Ok(html)
978 }
979
980 fn generate_plotly_heatmap(&self, data: &HeatmapData) -> Result<String> {
981 let values_json = serde_json::to_string(&data.values)?;
982 let x_labels_json = serde_json::to_string(&data.x_labels)?;
983 let y_labels_json = serde_json::to_string(&data.y_labels)?;
984
985 let html = format!(
986 r#"
987<!DOCTYPE html>
988<html>
989<head>
990 <script src="https://cdn.plot.ly/plotly-2.26.0.min.js"></script>
991 <title>Interactive Heatmap</title>
992</head>
993<body>
994 <div id="plotDiv" style="width:100%;height:600px;"></div>
995 <script>
996 var data = [{{
997 z: {},
998 x: {},
999 y: {},
1000 type: 'heatmap',
1001 colorscale: 'Viridis',
1002 showscale: true,
1003 colorbar: {{
1004 title: '{}'
1005 }}
1006 }}];
1007
1008 var layout = {{
1009 title: '{}',
1010 xaxis: {{
1011 title: 'Features'
1012 }},
1013 yaxis: {{
1014 title: 'Samples'
1015 }}
1016 }};
1017
1018 var config = {{
1019 responsive: true,
1020 displayModeBar: true
1021 }};
1022
1023 Plotly.newPlot('plotDiv', data, layout, config);
1024 </script>
1025</body>
1026</html>"#,
1027 values_json, x_labels_json, y_labels_json, data.color_bar_label, data.title
1028 );
1029
1030 Ok(html)
1031 }
1032
1033 fn generate_realtime_plot(&self, data: &InteractivePlotData) -> Result<String> {
1034 let realtime_config = data
1035 .realtime_config
1036 .as_ref()
1037 .ok_or_else(|| anyhow::anyhow!("Realtime config is required for realtime plots"))?;
1038
1039 let html = format!(
1040 r#"
1041<!DOCTYPE html>
1042<html>
1043<head>
1044 <script src="https://cdn.plot.ly/plotly-2.26.0.min.js"></script>
1045 <title>Real-time Plot</title>
1046</head>
1047<body>
1048 <div id="plotDiv" style="width:100%;height:600px;"></div>
1049 <script>
1050 var trace = {{
1051 x: [],
1052 y: [],
1053 mode: 'lines',
1054 type: 'scatter',
1055 name: '{}'
1056 }};
1057
1058 var layout = {{
1059 title: '{}',
1060 xaxis: {{
1061 title: '{}',
1062 range: [0, {}]
1063 }},
1064 yaxis: {{
1065 title: '{}'
1066 }}
1067 }};
1068
1069 var config = {{
1070 responsive: true,
1071 displayModeBar: true
1072 }};
1073
1074 Plotly.newPlot('plotDiv', [trace], layout, config);
1075
1076 // Simulate real-time updates
1077 var cnt = 0;
1078 var interval = setInterval(function() {{
1079 var time = new Date().getTime();
1080 var y = Math.sin(cnt * 0.1) + Math.random() * 0.1;
1081
1082 Plotly.extendTraces('plotDiv', {{
1083 x: [[time]],
1084 y: [[y]]
1085 }}, [0]);
1086
1087 // Keep only last {} points
1088 if (trace.x.length > {}) {{
1089 Plotly.relayout('plotDiv', {{
1090 'xaxis.range': [trace.x[trace.x.length - {}], trace.x[trace.x.length - 1]]
1091 }});
1092 }}
1093
1094 cnt++;
1095 }}, {});
1096 </script>
1097</body>
1098</html>"#,
1099 data.plot_data.labels.first().unwrap_or(&"Real-time Data".to_string()),
1100 data.plot_data.title,
1101 data.plot_data.x_label,
1102 realtime_config.time_window_seconds,
1103 data.plot_data.y_label,
1104 realtime_config.buffer_size,
1105 realtime_config.buffer_size,
1106 realtime_config.buffer_size,
1107 realtime_config.update_frequency_ms
1108 );
1109
1110 Ok(html)
1111 }
1112
1113 fn generate_animated_training_plot(
1114 &self,
1115 data: &InteractivePlotData,
1116 validation_data: &[f64],
1117 ) -> Result<String> {
1118 let training_json = serde_json::to_string(&data.plot_data.y_values)?;
1119 let validation_json = serde_json::to_string(validation_data)?;
1120 let epochs_json = serde_json::to_string(&data.plot_data.x_values)?;
1121
1122 let html = format!(
1123 r#"
1124<!DOCTYPE html>
1125<html>
1126<head>
1127 <script src="https://cdn.plot.ly/plotly-2.26.0.min.js"></script>
1128 <title>Animated Training Plot</title>
1129</head>
1130<body>
1131 <div id="plotDiv" style="width:100%;height:600px;"></div>
1132 <div id="controls">
1133 <button onclick="animateTraining()">Start Animation</button>
1134 <button onclick="resetAnimation()">Reset</button>
1135 </div>
1136 <script>
1137 var trainingData = {};
1138 var validationData = {};
1139 var epochs = {};
1140 var currentFrame = 0;
1141
1142 var trace1 = {{
1143 x: [],
1144 y: [],
1145 mode: 'lines+markers',
1146 type: 'scatter',
1147 name: 'Training Loss',
1148 line: {{color: '#1f77b4', width: 3}},
1149 marker: {{size: 6}}
1150 }};
1151
1152 var trace2 = {{
1153 x: [],
1154 y: [],
1155 mode: 'lines+markers',
1156 type: 'scatter',
1157 name: 'Validation Loss',
1158 line: {{color: '#ff7f0e', width: 3}},
1159 marker: {{size: 6}}
1160 }};
1161
1162 var layout = {{
1163 title: '{}',
1164 xaxis: {{title: '{}'}},
1165 yaxis: {{title: '{}'}},
1166 showlegend: true
1167 }};
1168
1169 Plotly.newPlot('plotDiv', [trace1, trace2], layout);
1170
1171 function animateTraining() {{
1172 var interval = setInterval(function() {{
1173 if (currentFrame >= trainingData.length) {{
1174 clearInterval(interval);
1175 return;
1176 }}
1177
1178 trace1.x.push(epochs[currentFrame]);
1179 trace1.y.push(trainingData[currentFrame]);
1180 trace2.x.push(epochs[currentFrame]);
1181 trace2.y.push(validationData[currentFrame]);
1182
1183 Plotly.redraw('plotDiv');
1184 currentFrame++;
1185 }}, 200);
1186 }}
1187
1188 function resetAnimation() {{
1189 currentFrame = 0;
1190 trace1.x = [];
1191 trace1.y = [];
1192 trace2.x = [];
1193 trace2.y = [];
1194 Plotly.redraw('plotDiv');
1195 }}
1196 </script>
1197</body>
1198</html>"#,
1199 training_json,
1200 validation_json,
1201 epochs_json,
1202 data.plot_data.title,
1203 data.plot_data.x_label,
1204 data.plot_data.y_label
1205 );
1206
1207 Ok(html)
1208 }
1209
1210 fn generate_dashboard_template(&self, title: &str) -> Result<String> {
1211 let html = format!(
1212 r#"
1213<!DOCTYPE html>
1214<html>
1215<head>
1216 <meta charset="utf-8">
1217 <title>{}</title>
1218 <script src="https://cdn.plot.ly/plotly-2.26.0.min.js"></script>
1219 <style>
1220 body {{
1221 font-family: Arial, sans-serif;
1222 margin: 20px;
1223 background-color: #f5f5f5;
1224 }}
1225 .dashboard-header {{
1226 text-align: center;
1227 margin-bottom: 30px;
1228 padding: 20px;
1229 background-color: white;
1230 border-radius: 10px;
1231 box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1232 }}
1233 .plot-container {{
1234 display: inline-block;
1235 width: 48%;
1236 margin: 1%;
1237 background-color: white;
1238 border-radius: 10px;
1239 box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1240 padding: 10px;
1241 }}
1242 .plot-container.full-width {{
1243 width: 98%;
1244 }}
1245 .controls {{
1246 text-align: center;
1247 margin: 20px 0;
1248 }}
1249 button {{
1250 padding: 10px 20px;
1251 margin: 0 10px;
1252 background-color: #007bff;
1253 color: white;
1254 border: none;
1255 border-radius: 5px;
1256 cursor: pointer;
1257 }}
1258 button:hover {{
1259 background-color: #0056b3;
1260 }}
1261 </style>
1262</head>
1263<body>
1264 <div class="dashboard-header">
1265 <h1>{}</h1>
1266 <p>Real-time debugging dashboard</p>
1267 </div>
1268 <div class="controls">
1269 <button onclick="refreshAll()">Refresh All</button>
1270 <button onclick="exportDashboard()">Export</button>
1271 <button onclick="toggleAutoRefresh()">Toggle Auto-refresh</button>
1272 </div>
1273 <div id="plots-container">"#,
1274 title, title
1275 );
1276
1277 Ok(html)
1278 }
1279
1280 fn embed_plot_in_dashboard(
1281 &self,
1282 dashboard_html: &str,
1283 plot_id: &str,
1284 plot_html: &str,
1285 ) -> Result<String> {
1286 let plot_div = format!(
1288 r#"<div class="plot-container" id="container-{}"></div>"#,
1289 plot_id
1290 );
1291
1292 let mut updated_html = dashboard_html.replace(
1293 r#"<div id="plots-container">"#,
1294 &format!(r#"<div id="plots-container">{}"#, plot_div),
1295 );
1296
1297 updated_html.push_str(&format!(
1299 r#"
1300 <script>
1301 // Plot {} initialization would go here
1302 // Extracted from: {}
1303 </script>"#,
1304 plot_id,
1305 plot_html.len()
1306 ));
1307
1308 Ok(updated_html)
1309 }
1310
1311 fn finalize_dashboard_html(&self, html: &str) -> Result<String> {
1312 let finalized = format!(
1313 r#"{}
1314 </div>
1315 <script>
1316 function refreshAll() {{
1317 location.reload();
1318 }}
1319
1320 function exportDashboard() {{
1321 // Export functionality
1322 alert('Export functionality would be implemented here');
1323 }}
1324
1325 var autoRefresh = false;
1326 function toggleAutoRefresh() {{
1327 autoRefresh = !autoRefresh;
1328 if (autoRefresh) {{
1329 setInterval(refreshAll, 30000); // Refresh every 30 seconds
1330 }}
1331 }}
1332 </script>
1333</body>
1334</html>"#,
1335 html
1336 );
1337
1338 Ok(finalized)
1339 }
1340
1341 async fn save_plot_to_file(&self, plot_id: &str, content: &str) -> Result<PathBuf> {
1342 let file_path = Path::new(&self.config.output_directory).join(format!("{}.html", plot_id));
1343 tokio::fs::write(&file_path, content).await?;
1344 Ok(file_path)
1345 }
1346
1347 async fn add_plot_to_dashboard(&mut self, plot_id: &str, content: &str) -> Result<()> {
1348 if self.dashboard_server.is_none() {
1349 self.dashboard_server = Some(DashboardServer {
1350 port: self.config.dashboard_port,
1351 plots: HashMap::new(),
1352 is_running: false,
1353 });
1354 }
1355
1356 if let Some(ref mut server) = self.dashboard_server {
1357 server.plots.insert(plot_id.to_string(), content.to_string());
1358 }
1359
1360 Ok(())
1361 }
1362
1363 async fn start_dashboard_server(&mut self) -> Result<()> {
1364 if let Some(ref mut server) = self.dashboard_server {
1365 if !server.is_running {
1366 server.is_running = true;
1368 tracing::info!("Dashboard server started on port {}", server.port);
1369 }
1370 }
1371 Ok(())
1372 }
1373
1374 async fn update_plot_in_dashboard(
1375 &mut self,
1376 plot_id: &str,
1377 data: &InteractivePlotData,
1378 ) -> Result<()> {
1379 let updated_content = self.generate_plotly_line_plot(data)?;
1381
1382 if let Some(ref mut server) = self.dashboard_server {
1383 server.plots.insert(plot_id.to_string(), updated_content);
1384 }
1385 Ok(())
1386 }
1387
1388 async fn remove_plot_from_dashboard(&mut self, plot_id: &str) -> Result<()> {
1389 if let Some(ref mut server) = self.dashboard_server {
1390 server.plots.remove(plot_id);
1391 }
1392 Ok(())
1393 }
1394
1395 fn get_plot_html_content(&self, instance: &PlotInstance) -> Result<String> {
1396 match instance.plot_type {
1398 InteractivePlotType::InteractiveLinePlot => {
1399 self.generate_plotly_line_plot(&instance.data)
1400 },
1401 InteractivePlotType::InteractiveScatterPlot => {
1402 self.generate_plotly_scatter_plot(&instance.data)
1403 },
1404 InteractivePlotType::RealtimeStreamingPlot => {
1405 self.generate_realtime_plot(&instance.data)
1406 },
1407 _ => Ok("Plot content not available".to_string()),
1408 }
1409 }
1410}
1411
1412#[derive(Debug, Clone, Serialize, Deserialize)]
1414pub struct PlotStatistics {
1415 pub plot_id: String,
1416 pub plot_type: InteractivePlotType,
1417 pub creation_time: DateTime<Utc>,
1418 pub last_update: DateTime<Utc>,
1419 pub update_count: u64,
1420 pub data_points: usize,
1421 pub is_realtime: bool,
1422 pub file_size_bytes: u64,
1423}
1424
1425#[cfg(test)]
1426mod tests {
1427 use super::*;
1428
1429 #[tokio::test]
1430 async fn test_modern_plotting_engine_creation() {
1431 let config = ModernPlottingConfig::default();
1432 let engine = ModernPlottingEngine::new(config);
1433 assert_eq!(engine.active_plots.len(), 0);
1434 }
1435
1436 #[tokio::test]
1437 async fn test_create_interactive_line_plot() {
1438 let config = ModernPlottingConfig::default();
1439 let mut engine = ModernPlottingEngine::new(config);
1440
1441 let plot_data = InteractivePlotData {
1442 plot_data: PlotData {
1443 x_values: vec![1.0, 2.0, 3.0, 4.0, 5.0],
1444 y_values: vec![1.0, 4.0, 2.0, 3.0, 5.0],
1445 labels: vec!["Test Data".to_string()],
1446 title: "Test Plot".to_string(),
1447 x_label: "X Axis".to_string(),
1448 y_label: "Y Axis".to_string(),
1449 },
1450 interactive_config: InteractiveConfig::default(),
1451 styling: PlotStyling::default(),
1452 animation_config: None,
1453 realtime_config: None,
1454 };
1455
1456 let result = engine.create_interactive_line_plot(plot_data, None).await;
1457 assert!(result.is_ok());
1458
1459 let plot_id = result.expect("operation failed in test");
1460 assert!(engine.active_plots.contains_key(&plot_id));
1461 }
1462
1463 #[tokio::test]
1464 async fn test_create_interactive_scatter_plot() {
1465 let config = ModernPlottingConfig::default();
1466 let mut engine = ModernPlottingEngine::new(config);
1467
1468 let x_values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1469 let y_values = vec![2.0, 3.0, 1.0, 4.0, 5.0];
1470
1471 let result = engine
1472 .create_interactive_scatter_plot(&x_values, &y_values, None, "Test Scatter Plot", None)
1473 .await;
1474
1475 assert!(result.is_ok());
1476
1477 let plot_id = result.expect("operation failed in test");
1478 assert!(engine.active_plots.contains_key(&plot_id));
1479 }
1480}