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)]
425pub struct DashboardServer {
426 port: u16,
428 plots: HashMap<String, String>, is_running: bool,
430}
431
432impl DashboardServer {
433 pub fn plots(&self) -> &HashMap<String, String> {
435 &self.plots
436 }
437
438 pub fn intended_port(&self) -> u16 {
440 self.port
441 }
442
443 pub fn is_active(&self) -> bool {
445 self.is_running
446 }
447}
448
449fn html_attribute_escape(text: &str) -> String {
451 let mut out = String::with_capacity(text.len());
452 for ch in text.chars() {
453 match ch {
454 '&' => out.push_str("&"),
455 '<' => out.push_str("<"),
456 '>' => out.push_str(">"),
457 '"' => out.push_str("""),
458 '\'' => out.push_str("'"),
459 _ => out.push(ch),
460 }
461 }
462 out
463}
464
465#[derive(Debug, Clone)]
467pub struct CachedPlot {
468 pub content: String,
469 pub hash: u64,
470 pub creation_time: DateTime<Utc>,
471 pub access_count: u64,
472}
473
474impl ModernPlottingEngine {
475 pub fn new(config: ModernPlottingConfig) -> Self {
477 std::fs::create_dir_all(&config.output_directory).ok();
478
479 Self {
480 config,
481 active_plots: HashMap::new(),
482 dashboard_server: None,
483 plot_cache: HashMap::new(),
484 }
485 }
486
487 pub async fn create_interactive_line_plot(
489 &mut self,
490 plot_data: InteractivePlotData,
491 plot_id: Option<String>,
492 ) -> Result<String> {
493 let id = plot_id.unwrap_or_else(|| format!("line_plot_{}", Utc::now().timestamp()));
494
495 let html_content = self.generate_plotly_line_plot(&plot_data)?;
496 let file_path = self.save_plot_to_file(&id, &html_content).await?;
497
498 let instance = PlotInstance {
499 id: id.clone(),
500 plot_type: InteractivePlotType::InteractiveLinePlot,
501 data: plot_data,
502 creation_time: Utc::now(),
503 last_update: Utc::now(),
504 file_path: Some(file_path),
505 is_realtime: false,
506 update_count: 0,
507 };
508
509 self.active_plots.insert(id.clone(), instance);
510
511 if self.config.enable_web_dashboard {
512 self.add_plot_to_dashboard(&id, &html_content).await?;
513 }
514
515 Ok(id)
516 }
517
518 pub async fn create_interactive_scatter_plot(
520 &mut self,
521 x_values: &[f64],
522 y_values: &[f64],
523 labels: Option<&[String]>,
524 title: &str,
525 plot_id: Option<String>,
526 ) -> Result<String> {
527 let id = plot_id.unwrap_or_else(|| format!("scatter_plot_{}", Utc::now().timestamp()));
528
529 let plot_data = InteractivePlotData {
530 plot_data: PlotData {
531 x_values: x_values.to_vec(),
532 y_values: y_values.to_vec(),
533 labels: labels.map(|l| l.to_vec()).unwrap_or_else(|| vec!["Series 1".to_string()]),
534 title: title.to_string(),
535 x_label: "X".to_string(),
536 y_label: "Y".to_string(),
537 },
538 interactive_config: InteractiveConfig::default(),
539 styling: PlotStyling::default(),
540 animation_config: None,
541 realtime_config: None,
542 };
543
544 let html_content = self.generate_plotly_scatter_plot(&plot_data)?;
545 let file_path = self.save_plot_to_file(&id, &html_content).await?;
546
547 let instance = PlotInstance {
548 id: id.clone(),
549 plot_type: InteractivePlotType::InteractiveScatterPlot,
550 data: plot_data,
551 creation_time: Utc::now(),
552 last_update: Utc::now(),
553 file_path: Some(file_path),
554 is_realtime: false,
555 update_count: 0,
556 };
557
558 self.active_plots.insert(id.clone(), instance);
559
560 if self.config.enable_web_dashboard {
561 self.add_plot_to_dashboard(&id, &html_content).await?;
562 }
563
564 Ok(id)
565 }
566
567 pub async fn create_interactive_heatmap(
569 &mut self,
570 values: &[Vec<f64>],
571 x_labels: Option<&[String]>,
572 y_labels: Option<&[String]>,
573 title: &str,
574 plot_id: Option<String>,
575 ) -> Result<String> {
576 let id = plot_id.unwrap_or_else(|| format!("heatmap_{}", Utc::now().timestamp()));
577
578 let default_x_labels: Vec<String> = (0..values.first().map_or(0, |row| row.len()))
579 .map(|i| format!("Col_{}", i))
580 .collect();
581 let default_y_labels: Vec<String> =
582 (0..values.len()).map(|i| format!("Row_{}", i)).collect();
583
584 let heatmap_data = HeatmapData {
585 values: values.to_vec(),
586 x_labels: x_labels.map(|l| l.to_vec()).unwrap_or(default_x_labels),
587 y_labels: y_labels.map(|l| l.to_vec()).unwrap_or(default_y_labels),
588 title: title.to_string(),
589 color_bar_label: "Value".to_string(),
590 };
591
592 let html_content = self.generate_plotly_heatmap(&heatmap_data)?;
593 let file_path = self.save_plot_to_file(&id, &html_content).await?;
594
595 let plot_data = InteractivePlotData {
596 plot_data: PlotData {
597 x_values: vec![],
598 y_values: vec![],
599 labels: vec![],
600 title: title.to_string(),
601 x_label: "X".to_string(),
602 y_label: "Y".to_string(),
603 },
604 interactive_config: InteractiveConfig::default(),
605 styling: PlotStyling::default(),
606 animation_config: None,
607 realtime_config: None,
608 };
609
610 let instance = PlotInstance {
611 id: id.clone(),
612 plot_type: InteractivePlotType::InteractiveHeatmap,
613 data: plot_data,
614 creation_time: Utc::now(),
615 last_update: Utc::now(),
616 file_path: Some(file_path),
617 is_realtime: false,
618 update_count: 0,
619 };
620
621 self.active_plots.insert(id.clone(), instance);
622
623 if self.config.enable_web_dashboard {
624 self.add_plot_to_dashboard(&id, &html_content).await?;
625 }
626
627 Ok(id)
628 }
629
630 pub async fn create_realtime_plot(
632 &mut self,
633 title: &str,
634 plot_id: Option<String>,
635 realtime_config: RealtimeConfig,
636 ) -> Result<String> {
637 let id = plot_id.unwrap_or_else(|| format!("realtime_plot_{}", Utc::now().timestamp()));
638
639 let plot_data = InteractivePlotData {
640 plot_data: PlotData {
641 x_values: vec![],
642 y_values: vec![],
643 labels: vec!["Real-time Data".to_string()],
644 title: title.to_string(),
645 x_label: "Time".to_string(),
646 y_label: "Value".to_string(),
647 },
648 interactive_config: InteractiveConfig::default(),
649 styling: PlotStyling::default(),
650 animation_config: None,
651 realtime_config: Some(realtime_config),
652 };
653
654 let html_content = self.generate_realtime_plot(&plot_data)?;
655 let file_path = self.save_plot_to_file(&id, &html_content).await?;
656
657 let instance = PlotInstance {
658 id: id.clone(),
659 plot_type: InteractivePlotType::RealtimeStreamingPlot,
660 data: plot_data,
661 creation_time: Utc::now(),
662 last_update: Utc::now(),
663 file_path: Some(file_path),
664 is_realtime: true,
665 update_count: 0,
666 };
667
668 self.active_plots.insert(id.clone(), instance);
669
670 if self.config.enable_web_dashboard {
671 self.add_plot_to_dashboard(&id, &html_content).await?;
672 }
673
674 Ok(id)
675 }
676
677 pub async fn create_animated_training_plot(
679 &mut self,
680 training_data: &[f64],
681 validation_data: &[f64],
682 epochs: &[u32],
683 title: &str,
684 plot_id: Option<String>,
685 ) -> Result<String> {
686 let id = plot_id.unwrap_or_else(|| format!("animated_training_{}", Utc::now().timestamp()));
687
688 let animation_config = AnimationConfig {
689 animation_type: AnimationType::TrainingProgress,
690 duration_ms: 5000,
691 easing: EasingFunction::EaseInOut,
692 frames: epochs.len() as u32,
693 loop_animation: false,
694 auto_start: true,
695 };
696
697 let plot_data = InteractivePlotData {
698 plot_data: PlotData {
699 x_values: epochs.iter().map(|&e| e as f64).collect(),
700 y_values: training_data.to_vec(),
701 labels: vec!["Training Loss".to_string(), "Validation Loss".to_string()],
702 title: title.to_string(),
703 x_label: "Epoch".to_string(),
704 y_label: "Loss".to_string(),
705 },
706 interactive_config: InteractiveConfig::default(),
707 styling: PlotStyling::default(),
708 animation_config: Some(animation_config),
709 realtime_config: None,
710 };
711
712 let html_content = self.generate_animated_training_plot(&plot_data, validation_data)?;
713 let file_path = self.save_plot_to_file(&id, &html_content).await?;
714
715 let instance = PlotInstance {
716 id: id.clone(),
717 plot_type: InteractivePlotType::AnimatedTrainingPlot,
718 data: plot_data,
719 creation_time: Utc::now(),
720 last_update: Utc::now(),
721 file_path: Some(file_path),
722 is_realtime: false,
723 update_count: 0,
724 };
725
726 self.active_plots.insert(id.clone(), instance);
727
728 if self.config.enable_web_dashboard {
729 self.add_plot_to_dashboard(&id, &html_content).await?;
730 }
731
732 Ok(id)
733 }
734
735 pub async fn create_dashboard(&mut self, plot_ids: &[String], title: &str) -> Result<String> {
737 let dashboard_id = format!("dashboard_{}", Utc::now().timestamp());
738
739 let mut dashboard_html = self.generate_dashboard_template(title)?;
740
741 for plot_id in plot_ids {
742 if let Some(plot_instance) = self.active_plots.get(plot_id) {
743 let plot_html = self.get_plot_html_content(plot_instance)?;
744 dashboard_html =
745 self.embed_plot_in_dashboard(&dashboard_html, plot_id, &plot_html)?;
746 }
747 }
748
749 dashboard_html = self.finalize_dashboard_html(&dashboard_html)?;
750
751 let dashboard_path =
752 Path::new(&self.config.output_directory).join(format!("{}.html", dashboard_id));
753 tokio::fs::write(&dashboard_path, &dashboard_html).await?;
754
755 if self.config.enable_web_dashboard {
756 self.start_dashboard_server().await?;
757 }
758
759 Ok(dashboard_path.to_string_lossy().to_string())
760 }
761
762 pub async fn update_realtime_plot(
764 &mut self,
765 plot_id: &str,
766 new_x: f64,
767 new_y: f64,
768 ) -> Result<()> {
769 let should_update_dashboard = self.config.enable_web_dashboard;
770 let mut plot_data_for_dashboard = None;
771
772 if let Some(plot_instance) = self.active_plots.get_mut(plot_id) {
773 if plot_instance.is_realtime {
774 plot_instance.data.plot_data.x_values.push(new_x);
776 plot_instance.data.plot_data.y_values.push(new_y);
777
778 if let Some(ref realtime_config) = plot_instance.data.realtime_config {
780 let buffer_size = realtime_config.buffer_size;
781 if plot_instance.data.plot_data.x_values.len() > buffer_size {
782 plot_instance.data.plot_data.x_values.remove(0);
783 plot_instance.data.plot_data.y_values.remove(0);
784 }
785 }
786
787 plot_instance.last_update = Utc::now();
788 plot_instance.update_count += 1;
789
790 if should_update_dashboard {
792 plot_data_for_dashboard = Some(plot_instance.data.clone());
793 }
794 }
795 }
796
797 if let Some(data) = plot_data_for_dashboard {
799 self.update_plot_in_dashboard(plot_id, &data).await?;
800 }
801
802 Ok(())
803 }
804
805 pub fn get_plot_statistics(&self, plot_id: &str) -> Option<PlotStatistics> {
807 self.active_plots.get(plot_id).map(|instance| PlotStatistics {
808 plot_id: plot_id.to_string(),
809 plot_type: instance.plot_type.clone(),
810 creation_time: instance.creation_time,
811 last_update: instance.last_update,
812 update_count: instance.update_count,
813 data_points: instance.data.plot_data.x_values.len(),
814 is_realtime: instance.is_realtime,
815 file_size_bytes: instance
816 .file_path
817 .as_ref()
818 .and_then(|path| std::fs::metadata(path).ok())
819 .map(|metadata| metadata.len())
820 .unwrap_or(0),
821 })
822 }
823
824 pub fn list_active_plots(&self) -> Vec<String> {
826 self.active_plots.keys().cloned().collect()
827 }
828
829 pub async fn remove_plot(&mut self, plot_id: &str) -> Result<()> {
831 if let Some(instance) = self.active_plots.remove(plot_id) {
832 if let Some(file_path) = instance.file_path {
834 tokio::fs::remove_file(file_path).await.ok();
835 }
836
837 if self.config.enable_web_dashboard {
839 self.remove_plot_from_dashboard(plot_id).await?;
840 }
841 }
842
843 Ok(())
844 }
845
846 fn generate_plotly_line_plot(&self, data: &InteractivePlotData) -> Result<String> {
849 let plot_data = &data.plot_data;
850 let styling = &data.styling;
851
852 let mut html = String::from(
853 r#"
854<!DOCTYPE html>
855<html>
856<head>
857 <script src="https://cdn.plot.ly/plotly-2.26.0.min.js"></script>
858 <title>Interactive Line Plot</title>
859</head>
860<body>
861 <div id="plotDiv" style="width:100%;height:600px;"></div>
862 <script>
863 var trace = {
864 x: ["#,
865 );
866
867 html.push_str(
869 &plot_data.x_values.iter().map(|x| x.to_string()).collect::<Vec<_>>().join(", "),
870 );
871
872 html.push_str(
873 r#"],
874 y: ["#,
875 );
876
877 html.push_str(
879 &plot_data.y_values.iter().map(|y| y.to_string()).collect::<Vec<_>>().join(", "),
880 );
881
882 html.push_str(&format!(
883 r#"],
884 type: 'scatter',
885 mode: 'lines+markers',
886 name: '{}',
887 line: {{
888 color: '{}',
889 width: 2
890 }},
891 marker: {{
892 size: 6,
893 color: '{}'
894 }}
895 }};
896
897 var layout = {{
898 title: '{}',
899 xaxis: {{
900 title: '{}',
901 showgrid: {},
902 gridcolor: '{}'
903 }},
904 yaxis: {{
905 title: '{}',
906 showgrid: {},
907 gridcolor: '{}'
908 }},
909 font: {{
910 family: '{}',
911 size: {},
912 color: '{}'
913 }},
914 plot_bgcolor: '{}',
915 paper_bgcolor: '{}'
916 }};
917
918 var config = {{
919 responsive: true,
920 displayModeBar: true,
921 modeBarButtonsToAdd: ['pan2d', 'select2d', 'lasso2d', 'resetScale2d'],
922 toImageButtonOptions: {{
923 format: 'png',
924 filename: 'debug_plot',
925 height: 600,
926 width: 800,
927 scale: 1
928 }}
929 }};
930
931 Plotly.newPlot('plotDiv', [trace], layout, config);
932 </script>
933</body>
934</html>"#,
935 plot_data.labels.first().unwrap_or(&"Series 1".to_string()),
936 styling.color_palette.first().unwrap_or(&"#1f77b4".to_string()),
937 styling.color_palette.first().unwrap_or(&"#1f77b4".to_string()),
938 plot_data.title,
939 plot_data.x_label,
940 styling.grid_config.show_x_grid,
941 styling.grid_config.grid_color,
942 plot_data.y_label,
943 styling.grid_config.show_y_grid,
944 styling.grid_config.grid_color,
945 styling.font_config.family,
946 styling.font_config.size,
947 styling.font_config.color,
948 styling.background_color,
949 styling.background_color
950 ));
951
952 Ok(html)
953 }
954
955 fn generate_plotly_scatter_plot(&self, data: &InteractivePlotData) -> Result<String> {
956 let plot_data = &data.plot_data;
957 let styling = &data.styling;
958
959 let html = format!(
960 r#"
961<!DOCTYPE html>
962<html>
963<head>
964 <script src="https://cdn.plot.ly/plotly-2.26.0.min.js"></script>
965 <title>Interactive Scatter Plot</title>
966</head>
967<body>
968 <div id="plotDiv" style="width:100%;height:600px;"></div>
969 <script>
970 var trace = {{
971 x: [{}],
972 y: [{}],
973 mode: 'markers',
974 type: 'scatter',
975 name: '{}',
976 marker: {{
977 size: 8,
978 color: '{}',
979 opacity: 0.7,
980 line: {{
981 color: '{}',
982 width: 1
983 }}
984 }}
985 }};
986
987 var layout = {{
988 title: '{}',
989 xaxis: {{
990 title: '{}',
991 showgrid: true
992 }},
993 yaxis: {{
994 title: '{}',
995 showgrid: true
996 }},
997 hovermode: 'closest'
998 }};
999
1000 var config = {{
1001 responsive: true,
1002 displayModeBar: true
1003 }};
1004
1005 Plotly.newPlot('plotDiv', [trace], layout, config);
1006 </script>
1007</body>
1008</html>"#,
1009 plot_data.x_values.iter().map(|x| x.to_string()).collect::<Vec<_>>().join(", "),
1010 plot_data.y_values.iter().map(|y| y.to_string()).collect::<Vec<_>>().join(", "),
1011 plot_data.labels.first().unwrap_or(&"Series 1".to_string()),
1012 styling.color_palette.first().unwrap_or(&"#1f77b4".to_string()),
1013 styling.color_palette.first().unwrap_or(&"#1f77b4".to_string()),
1014 plot_data.title,
1015 plot_data.x_label,
1016 plot_data.y_label
1017 );
1018
1019 Ok(html)
1020 }
1021
1022 fn generate_plotly_heatmap(&self, data: &HeatmapData) -> Result<String> {
1023 let values_json = serde_json::to_string(&data.values)?;
1024 let x_labels_json = serde_json::to_string(&data.x_labels)?;
1025 let y_labels_json = serde_json::to_string(&data.y_labels)?;
1026
1027 let html = format!(
1028 r#"
1029<!DOCTYPE html>
1030<html>
1031<head>
1032 <script src="https://cdn.plot.ly/plotly-2.26.0.min.js"></script>
1033 <title>Interactive Heatmap</title>
1034</head>
1035<body>
1036 <div id="plotDiv" style="width:100%;height:600px;"></div>
1037 <script>
1038 var data = [{{
1039 z: {},
1040 x: {},
1041 y: {},
1042 type: 'heatmap',
1043 colorscale: 'Viridis',
1044 showscale: true,
1045 colorbar: {{
1046 title: '{}'
1047 }}
1048 }}];
1049
1050 var layout = {{
1051 title: '{}',
1052 xaxis: {{
1053 title: 'Features'
1054 }},
1055 yaxis: {{
1056 title: 'Samples'
1057 }}
1058 }};
1059
1060 var config = {{
1061 responsive: true,
1062 displayModeBar: true
1063 }};
1064
1065 Plotly.newPlot('plotDiv', data, layout, config);
1066 </script>
1067</body>
1068</html>"#,
1069 values_json, x_labels_json, y_labels_json, data.color_bar_label, data.title
1070 );
1071
1072 Ok(html)
1073 }
1074
1075 fn generate_realtime_plot(&self, data: &InteractivePlotData) -> Result<String> {
1076 let realtime_config = data
1077 .realtime_config
1078 .as_ref()
1079 .ok_or_else(|| anyhow::anyhow!("Realtime config is required for realtime plots"))?;
1080
1081 let html = format!(
1082 r#"
1083<!DOCTYPE html>
1084<html>
1085<head>
1086 <script src="https://cdn.plot.ly/plotly-2.26.0.min.js"></script>
1087 <title>Real-time Plot</title>
1088</head>
1089<body>
1090 <div id="plotDiv" style="width:100%;height:600px;"></div>
1091 <script>
1092 var trace = {{
1093 x: [],
1094 y: [],
1095 mode: 'lines',
1096 type: 'scatter',
1097 name: '{}'
1098 }};
1099
1100 var layout = {{
1101 title: '{}',
1102 xaxis: {{
1103 title: '{}',
1104 range: [0, {}]
1105 }},
1106 yaxis: {{
1107 title: '{}'
1108 }}
1109 }};
1110
1111 var config = {{
1112 responsive: true,
1113 displayModeBar: true
1114 }};
1115
1116 Plotly.newPlot('plotDiv', [trace], layout, config);
1117
1118 // Live-update hook. The page does NOT generate its own data: call
1119 // `trustformersPushPoint(x, y)` from whatever feed supplies real
1120 // samples. A previous version of this template ran a setInterval that
1121 // appended `Math.sin(cnt * 0.1) + Math.random() * 0.1` to the chart, so
1122 // an unattended page filled up with invented measurements.
1123 var trustformersBufferSize = {};
1124 window.trustformersPushPoint = function(x, y) {{
1125 Plotly.extendTraces('plotDiv', {{ x: [[x]], y: [[y]] }}, [0]);
1126 if (trace.x.length > trustformersBufferSize) {{
1127 Plotly.relayout('plotDiv', {{
1128 'xaxis.range': [
1129 trace.x[trace.x.length - trustformersBufferSize],
1130 trace.x[trace.x.length - 1]
1131 ]
1132 }});
1133 }}
1134 }};
1135 </script>
1136</body>
1137</html>"#,
1138 data.plot_data.labels.first().unwrap_or(&"Real-time Data".to_string()),
1139 data.plot_data.title,
1140 data.plot_data.x_label,
1141 realtime_config.time_window_seconds,
1142 data.plot_data.y_label,
1143 realtime_config.buffer_size,
1144 );
1145
1146 Ok(html)
1147 }
1148
1149 fn generate_animated_training_plot(
1150 &self,
1151 data: &InteractivePlotData,
1152 validation_data: &[f64],
1153 ) -> Result<String> {
1154 let training_json = serde_json::to_string(&data.plot_data.y_values)?;
1155 let validation_json = serde_json::to_string(validation_data)?;
1156 let epochs_json = serde_json::to_string(&data.plot_data.x_values)?;
1157
1158 let html = format!(
1159 r#"
1160<!DOCTYPE html>
1161<html>
1162<head>
1163 <script src="https://cdn.plot.ly/plotly-2.26.0.min.js"></script>
1164 <title>Animated Training Plot</title>
1165</head>
1166<body>
1167 <div id="plotDiv" style="width:100%;height:600px;"></div>
1168 <div id="controls">
1169 <button onclick="animateTraining()">Start Animation</button>
1170 <button onclick="resetAnimation()">Reset</button>
1171 </div>
1172 <script>
1173 var trainingData = {};
1174 var validationData = {};
1175 var epochs = {};
1176 var currentFrame = 0;
1177
1178 var trace1 = {{
1179 x: [],
1180 y: [],
1181 mode: 'lines+markers',
1182 type: 'scatter',
1183 name: 'Training Loss',
1184 line: {{color: '#1f77b4', width: 3}},
1185 marker: {{size: 6}}
1186 }};
1187
1188 var trace2 = {{
1189 x: [],
1190 y: [],
1191 mode: 'lines+markers',
1192 type: 'scatter',
1193 name: 'Validation Loss',
1194 line: {{color: '#ff7f0e', width: 3}},
1195 marker: {{size: 6}}
1196 }};
1197
1198 var layout = {{
1199 title: '{}',
1200 xaxis: {{title: '{}'}},
1201 yaxis: {{title: '{}'}},
1202 showlegend: true
1203 }};
1204
1205 Plotly.newPlot('plotDiv', [trace1, trace2], layout);
1206
1207 function animateTraining() {{
1208 var interval = setInterval(function() {{
1209 if (currentFrame >= trainingData.length) {{
1210 clearInterval(interval);
1211 return;
1212 }}
1213
1214 trace1.x.push(epochs[currentFrame]);
1215 trace1.y.push(trainingData[currentFrame]);
1216 trace2.x.push(epochs[currentFrame]);
1217 trace2.y.push(validationData[currentFrame]);
1218
1219 Plotly.redraw('plotDiv');
1220 currentFrame++;
1221 }}, 200);
1222 }}
1223
1224 function resetAnimation() {{
1225 currentFrame = 0;
1226 trace1.x = [];
1227 trace1.y = [];
1228 trace2.x = [];
1229 trace2.y = [];
1230 Plotly.redraw('plotDiv');
1231 }}
1232 </script>
1233</body>
1234</html>"#,
1235 training_json,
1236 validation_json,
1237 epochs_json,
1238 data.plot_data.title,
1239 data.plot_data.x_label,
1240 data.plot_data.y_label
1241 );
1242
1243 Ok(html)
1244 }
1245
1246 fn generate_dashboard_template(&self, title: &str) -> Result<String> {
1247 let html = format!(
1248 r#"
1249<!DOCTYPE html>
1250<html>
1251<head>
1252 <meta charset="utf-8">
1253 <title>{}</title>
1254 <script src="https://cdn.plot.ly/plotly-2.26.0.min.js"></script>
1255 <style>
1256 body {{
1257 font-family: Arial, sans-serif;
1258 margin: 20px;
1259 background-color: #f5f5f5;
1260 }}
1261 .dashboard-header {{
1262 text-align: center;
1263 margin-bottom: 30px;
1264 padding: 20px;
1265 background-color: white;
1266 border-radius: 10px;
1267 box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1268 }}
1269 .plot-container {{
1270 display: inline-block;
1271 width: 48%;
1272 margin: 1%;
1273 background-color: white;
1274 border-radius: 10px;
1275 box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1276 padding: 10px;
1277 }}
1278 .plot-container.full-width {{
1279 width: 98%;
1280 }}
1281 .controls {{
1282 text-align: center;
1283 margin: 20px 0;
1284 }}
1285 button {{
1286 padding: 10px 20px;
1287 margin: 0 10px;
1288 background-color: #007bff;
1289 color: white;
1290 border: none;
1291 border-radius: 5px;
1292 cursor: pointer;
1293 }}
1294 button:hover {{
1295 background-color: #0056b3;
1296 }}
1297 </style>
1298</head>
1299<body>
1300 <div class="dashboard-header">
1301 <h1>{}</h1>
1302 <p>Real-time debugging dashboard</p>
1303 </div>
1304 <div class="controls">
1305 <button onclick="refreshAll()">Refresh All</button>
1306 <button onclick="exportDashboard()">Export</button>
1307 <button onclick="toggleAutoRefresh()">Toggle Auto-refresh</button>
1308 </div>
1309 <div id="plots-container">"#,
1310 title, title
1311 );
1312
1313 Ok(html)
1314 }
1315
1316 fn embed_plot_in_dashboard(
1328 &self,
1329 dashboard_html: &str,
1330 plot_id: &str,
1331 plot_html: &str,
1332 ) -> Result<String> {
1333 let plot_div = format!(
1334 r#"<div class="plot-container" id="container-{}"><iframe title="{}" style="width:100%;height:640px;border:0" srcdoc="{}"></iframe></div>"#,
1335 html_attribute_escape(plot_id),
1336 html_attribute_escape(plot_id),
1337 html_attribute_escape(plot_html),
1338 );
1339
1340 let updated_html = dashboard_html.replace(
1341 r#"<div id="plots-container">"#,
1342 &format!(r#"<div id="plots-container">{}"#, plot_div),
1343 );
1344
1345 Ok(updated_html)
1346 }
1347
1348 fn finalize_dashboard_html(&self, html: &str) -> Result<String> {
1349 let finalized = format!(
1350 r#"{}
1351 </div>
1352 <script>
1353 function refreshAll() {{
1354 location.reload();
1355 }}
1356
1357 function exportDashboard() {{
1358 // Export functionality
1359 alert('Export functionality would be implemented here');
1360 }}
1361
1362 var autoRefresh = false;
1363 function toggleAutoRefresh() {{
1364 autoRefresh = !autoRefresh;
1365 if (autoRefresh) {{
1366 setInterval(refreshAll, 30000); // Refresh every 30 seconds
1367 }}
1368 }}
1369 </script>
1370</body>
1371</html>"#,
1372 html
1373 );
1374
1375 Ok(finalized)
1376 }
1377
1378 async fn save_plot_to_file(&self, plot_id: &str, content: &str) -> Result<PathBuf> {
1379 let file_path = Path::new(&self.config.output_directory).join(format!("{}.html", plot_id));
1380 tokio::fs::write(&file_path, content).await?;
1381 Ok(file_path)
1382 }
1383
1384 async fn add_plot_to_dashboard(&mut self, plot_id: &str, content: &str) -> Result<()> {
1385 if self.dashboard_server.is_none() {
1386 self.dashboard_server = Some(DashboardServer {
1387 port: self.config.dashboard_port,
1388 plots: HashMap::new(),
1389 is_running: false,
1390 });
1391 }
1392
1393 if let Some(ref mut server) = self.dashboard_server {
1394 server.plots.insert(plot_id.to_string(), content.to_string());
1395 }
1396
1397 Ok(())
1398 }
1399
1400 async fn start_dashboard_server(&mut self) -> Result<()> {
1410 if let Some(ref mut server) = self.dashboard_server {
1411 if !server.is_running {
1412 server.is_running = true;
1413 tracing::debug!(
1414 port = server.port,
1415 "in-memory plot dashboard activated (no socket is bound)"
1416 );
1417 }
1418 }
1419 Ok(())
1420 }
1421
1422 async fn update_plot_in_dashboard(
1423 &mut self,
1424 plot_id: &str,
1425 data: &InteractivePlotData,
1426 ) -> Result<()> {
1427 let updated_content = self.generate_plotly_line_plot(data)?;
1429
1430 if let Some(ref mut server) = self.dashboard_server {
1431 server.plots.insert(plot_id.to_string(), updated_content);
1432 }
1433 Ok(())
1434 }
1435
1436 async fn remove_plot_from_dashboard(&mut self, plot_id: &str) -> Result<()> {
1437 if let Some(ref mut server) = self.dashboard_server {
1438 server.plots.remove(plot_id);
1439 }
1440 Ok(())
1441 }
1442
1443 fn get_plot_html_content(&self, instance: &PlotInstance) -> Result<String> {
1444 match instance.plot_type {
1446 InteractivePlotType::InteractiveLinePlot => {
1447 self.generate_plotly_line_plot(&instance.data)
1448 },
1449 InteractivePlotType::InteractiveScatterPlot => {
1450 self.generate_plotly_scatter_plot(&instance.data)
1451 },
1452 InteractivePlotType::RealtimeStreamingPlot => {
1453 self.generate_realtime_plot(&instance.data)
1454 },
1455 _ => Ok("Plot content not available".to_string()),
1456 }
1457 }
1458}
1459
1460#[derive(Debug, Clone, Serialize, Deserialize)]
1462pub struct PlotStatistics {
1463 pub plot_id: String,
1464 pub plot_type: InteractivePlotType,
1465 pub creation_time: DateTime<Utc>,
1466 pub last_update: DateTime<Utc>,
1467 pub update_count: u64,
1468 pub data_points: usize,
1469 pub is_realtime: bool,
1470 pub file_size_bytes: u64,
1471}
1472
1473#[cfg(test)]
1474mod tests {
1475 use super::*;
1476
1477 #[tokio::test]
1478 async fn test_modern_plotting_engine_creation() {
1479 let config = ModernPlottingConfig::default();
1480 let engine = ModernPlottingEngine::new(config);
1481 assert_eq!(engine.active_plots.len(), 0);
1482 }
1483
1484 #[tokio::test]
1485 async fn test_create_interactive_line_plot() {
1486 let config = ModernPlottingConfig::default();
1487 let mut engine = ModernPlottingEngine::new(config);
1488
1489 let plot_data = InteractivePlotData {
1490 plot_data: PlotData {
1491 x_values: vec![1.0, 2.0, 3.0, 4.0, 5.0],
1492 y_values: vec![1.0, 4.0, 2.0, 3.0, 5.0],
1493 labels: vec!["Test Data".to_string()],
1494 title: "Test Plot".to_string(),
1495 x_label: "X Axis".to_string(),
1496 y_label: "Y Axis".to_string(),
1497 },
1498 interactive_config: InteractiveConfig::default(),
1499 styling: PlotStyling::default(),
1500 animation_config: None,
1501 realtime_config: None,
1502 };
1503
1504 let result = engine.create_interactive_line_plot(plot_data, None).await;
1505 assert!(result.is_ok());
1506
1507 let plot_id = result.expect("operation failed in test");
1508 assert!(engine.active_plots.contains_key(&plot_id));
1509 }
1510
1511 #[tokio::test]
1512 async fn test_create_interactive_scatter_plot() {
1513 let config = ModernPlottingConfig::default();
1514 let mut engine = ModernPlottingEngine::new(config);
1515
1516 let x_values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1517 let y_values = vec![2.0, 3.0, 1.0, 4.0, 5.0];
1518
1519 let result = engine
1520 .create_interactive_scatter_plot(&x_values, &y_values, None, "Test Scatter Plot", None)
1521 .await;
1522
1523 assert!(result.is_ok());
1524
1525 let plot_id = result.expect("operation failed in test");
1526 assert!(engine.active_plots.contains_key(&plot_id));
1527 }
1528 #[test]
1531 fn test_modern_plotting_config_default() {
1532 let cfg = ModernPlottingConfig::default();
1533 assert!(cfg.enable_interactive);
1534 assert!(cfg.enable_realtime);
1535 assert_eq!(cfg.dashboard_port, 8888);
1536 assert_eq!(cfg.max_data_points, 10000);
1537 assert_eq!(cfg.animation_fps, 30);
1538 }
1539
1540 #[test]
1541 fn test_modern_plotting_engine_new_empty() {
1542 let mut cfg = ModernPlottingConfig::default();
1543 cfg.output_directory =
1544 std::env::temp_dir().join("trustformers_test_mp").to_string_lossy().into_owned();
1545 cfg.enable_web_dashboard = false;
1546 let engine = ModernPlottingEngine::new(cfg);
1547 assert_eq!(engine.active_plots.len(), 0);
1548 assert_eq!(engine.list_active_plots().len(), 0);
1549 }
1550
1551 #[test]
1552 fn test_modern_plotting_engine_statistics_missing() {
1553 let mut cfg = ModernPlottingConfig::default();
1554 cfg.output_directory = std::env::temp_dir()
1555 .join("trustformers_test_mp2")
1556 .to_string_lossy()
1557 .into_owned();
1558 cfg.enable_web_dashboard = false;
1559 let engine = ModernPlottingEngine::new(cfg);
1560 assert!(engine.get_plot_statistics("nonexistent").is_none());
1561 }
1562
1563 #[test]
1564 fn test_plotting_backend_variants() {
1565 let _ = PlottingBackend::PlotlyJS;
1566 let _ = PlottingBackend::D3JS;
1567 let _ = PlottingBackend::ChartJS;
1568 let _ = PlottingBackend::ThreeJS;
1569 let _ = PlottingBackend::WebGL;
1570 }
1571
1572 #[test]
1573 fn test_interactive_config_default() {
1574 let ic = InteractiveConfig::default();
1575 assert!(ic.enable_zoom);
1576 assert!(ic.enable_pan);
1577 assert!(ic.enable_hover);
1578 assert!(!ic.enable_brush);
1579 }
1580
1581 #[test]
1582 fn test_plot_styling_default() {
1583 let styling = PlotStyling::default();
1584 assert_eq!(styling.color_palette.len(), 10);
1585 assert!(styling.custom_css.is_none());
1586 }
1587
1588 #[test]
1589 fn test_font_config_default() {
1590 let fc = FontConfig::default();
1591 assert_eq!(fc.size, 12);
1592 assert_eq!(fc.color, "#000000");
1593 }
1594
1595 #[test]
1596 fn test_font_weight_variants() {
1597 let _ = FontWeight::Normal;
1598 let _ = FontWeight::Bold;
1599 let _ = FontWeight::Light;
1600 let _ = FontWeight::ExtraBold;
1601 }
1602
1603 #[test]
1604 fn test_line_style_variants() {
1605 let _ = [
1606 LineStyle::Solid,
1607 LineStyle::Dashed,
1608 LineStyle::Dotted,
1609 LineStyle::DashDot,
1610 ];
1611 }
1612
1613 #[test]
1614 fn test_marker_style_variants() {
1615 let _ = [
1616 MarkerStyle::Circle,
1617 MarkerStyle::Square,
1618 MarkerStyle::Triangle,
1619 MarkerStyle::Diamond,
1620 MarkerStyle::Cross,
1621 MarkerStyle::Plus,
1622 MarkerStyle::Star,
1623 ];
1624 }
1625
1626 #[test]
1627 fn test_interactive_plot_type_variants() {
1628 let _ = InteractivePlotType::InteractiveLinePlot;
1629 let _ = InteractivePlotType::InteractiveScatterPlot;
1630 let _ = InteractivePlotType::InteractiveHeatmap;
1631 let _ = InteractivePlotType::RealtimeStreamingPlot;
1632 let _ = InteractivePlotType::MultiPlotDashboard;
1633 }
1634
1635 #[test]
1636 fn test_data_source_variants() {
1637 let _ = DataSource::MemoryBuffer {
1638 buffer_id: "buf1".to_string(),
1639 };
1640 let _ = DataSource::WebSocket {
1641 url: "ws://localhost".to_string(),
1642 };
1643 let _ = DataSource::FileWatching {
1644 path: "/tmp/test".to_string(),
1645 };
1646 }
1647
1648 #[test]
1649 fn test_animation_type_variants() {
1650 let _ = AnimationType::FadeIn;
1651 let _ = AnimationType::SlideIn;
1652 let _ = AnimationType::Grow;
1653 let _ = AnimationType::TrainingProgress;
1654 let _ = AnimationType::GradientFlow;
1655 }
1656
1657 #[test]
1658 fn test_easing_function_variants() {
1659 let _ = EasingFunction::Linear;
1660 let _ = EasingFunction::EaseIn;
1661 let _ = EasingFunction::EaseOut;
1662 let _ = EasingFunction::EaseInOut;
1663 let _ = EasingFunction::Bounce;
1664 let _ = EasingFunction::Elastic;
1665 }
1666
1667 #[test]
1668 fn test_legend_position_variants() {
1669 let _ = LegendPosition::TopRight;
1670 let _ = LegendPosition::TopLeft;
1671 let _ = LegendPosition::BottomRight;
1672 let _ = LegendPosition::Bottom;
1673 let _ = LegendPosition::Left;
1674 let _ = LegendPosition::Right;
1675 }
1676}