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