1use crate::error::{MetricsError, Result};
27use scirs2_core::ndarray::{Array1, Array2};
28use serde::{Deserialize, Serialize};
29use std::collections::HashMap;
30use std::net::SocketAddr;
31use std::sync::{Arc, Mutex};
32use std::time::{Duration, SystemTime, UNIX_EPOCH};
33
34#[cfg(feature = "dashboard_server")]
36pub mod server;
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct DashboardConfig {
41 pub address: SocketAddr,
43 pub refresh_interval: u64,
45 pub max_data_points: usize,
47 pub enable_realtime: bool,
49 pub title: String,
51 pub theme: DashboardTheme,
53}
54
55impl Default for DashboardConfig {
56 fn default() -> Self {
57 Self {
58 address: "127.0.0.1:8080".parse().expect("Operation failed"),
59 refresh_interval: 5,
60 max_data_points: 1000,
61 enable_realtime: true,
62 title: "ML Metrics Dashboard".to_string(),
63 theme: DashboardTheme::default(),
64 }
65 }
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct DashboardTheme {
71 pub primary_color: String,
73 pub background_color: String,
75 pub text_color: String,
77 pub chart_colors: Vec<String>,
79}
80
81impl Default for DashboardTheme {
82 fn default() -> Self {
83 Self {
84 primary_color: "#2563eb".to_string(),
85 background_color: "#ffffff".to_string(),
86 text_color: "#1f2937".to_string(),
87 chart_colors: vec![
88 "#2563eb".to_string(),
89 "#dc2626".to_string(),
90 "#059669".to_string(),
91 "#d97706".to_string(),
92 "#7c3aed".to_string(),
93 "#db2777".to_string(),
94 ],
95 }
96 }
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct MetricDataPoint {
102 pub timestamp: u64,
104 pub name: String,
106 pub value: f64,
108 pub metadata: HashMap<String, String>,
110}
111
112impl MetricDataPoint {
113 pub fn new(name: String, value: f64) -> Self {
115 let timestamp = SystemTime::now()
116 .duration_since(UNIX_EPOCH)
117 .unwrap_or(Duration::from_secs(0))
118 .as_secs();
119
120 Self {
121 timestamp,
122 name,
123 value,
124 metadata: HashMap::new(),
125 }
126 }
127
128 pub fn with_metadata(name: String, value: f64, metadata: HashMap<String, String>) -> Self {
130 let mut point = Self::new(name, value);
131 point.metadata = metadata;
132 point
133 }
134}
135
136#[derive(Debug, Clone)]
138pub struct DashboardData {
139 data_points: Arc<Mutex<Vec<MetricDataPoint>>>,
141 config: DashboardConfig,
143}
144
145impl DashboardData {
146 pub fn new(config: DashboardConfig) -> Self {
148 Self {
149 data_points: Arc::new(Mutex::new(Vec::new())),
150 config,
151 }
152 }
153
154 pub fn add_metric(&self, point: MetricDataPoint) -> Result<()> {
156 let mut data = self
157 .data_points
158 .lock()
159 .map_err(|_| MetricsError::InvalidInput("Failed to acquire data lock".to_string()))?;
160
161 data.push(point);
162
163 if data.len() > self.config.max_data_points {
165 let excess = data.len() - self.config.max_data_points;
166 data.drain(0..excess);
167 }
168
169 Ok(())
170 }
171
172 pub fn add_metrics(&self, points: Vec<MetricDataPoint>) -> Result<()> {
174 for point in points {
175 self.add_metric(point)?;
176 }
177 Ok(())
178 }
179
180 pub fn get_all_metrics(&self) -> Result<Vec<MetricDataPoint>> {
182 let data = self
183 .data_points
184 .lock()
185 .map_err(|_| MetricsError::InvalidInput("Failed to acquire data lock".to_string()))?;
186
187 Ok(data.clone())
188 }
189
190 pub fn get_metrics_by_name(&self, name: &str) -> Result<Vec<MetricDataPoint>> {
192 let data = self
193 .data_points
194 .lock()
195 .map_err(|_| MetricsError::InvalidInput("Failed to acquire data lock".to_string()))?;
196
197 let filtered: Vec<MetricDataPoint> = data
198 .iter()
199 .filter(|point| point.name == name)
200 .cloned()
201 .collect();
202
203 Ok(filtered)
204 }
205
206 pub fn get_metric_names(&self) -> Result<Vec<String>> {
208 let data = self
209 .data_points
210 .lock()
211 .map_err(|_| MetricsError::InvalidInput("Failed to acquire data lock".to_string()))?;
212
213 let mut names: Vec<String> = data.iter().map(|point| point.name.clone()).collect();
214 names.sort();
215 names.dedup();
216
217 Ok(names)
218 }
219
220 pub fn clear(&self) -> Result<()> {
222 let mut data = self
223 .data_points
224 .lock()
225 .map_err(|_| MetricsError::InvalidInput("Failed to acquire data lock".to_string()))?;
226
227 data.clear();
228 Ok(())
229 }
230
231 pub fn get_metrics_in_range(
233 &self,
234 start_time: u64,
235 end_time: u64,
236 ) -> Result<Vec<MetricDataPoint>> {
237 let data = self
238 .data_points
239 .lock()
240 .map_err(|_| MetricsError::InvalidInput("Failed to acquire data lock".to_string()))?;
241
242 let filtered: Vec<MetricDataPoint> = data
243 .iter()
244 .filter(|point| point.timestamp >= start_time && point.timestamp <= end_time)
245 .cloned()
246 .collect();
247
248 Ok(filtered)
249 }
250}
251
252#[derive(Debug, Clone)]
254pub struct InteractiveDashboard {
255 data: DashboardData,
257 config: DashboardConfig,
259}
260
261impl InteractiveDashboard {
262 pub fn new(config: DashboardConfig) -> Self {
264 let data = DashboardData::new(config.clone());
265
266 Self { data, config }
267 }
268}
269
270impl Default for InteractiveDashboard {
271 fn default() -> Self {
272 Self::new(DashboardConfig::default())
273 }
274}
275
276impl InteractiveDashboard {
277 pub fn add_metric(&self, name: &str, value: f64) -> Result<()> {
279 let point = MetricDataPoint::new(name.to_string(), value);
280 self.data.add_metric(point)
281 }
282
283 pub fn add_metric_with_metadata(
285 &self,
286 name: &str,
287 value: f64,
288 metadata: HashMap<String, String>,
289 ) -> Result<()> {
290 let point = MetricDataPoint::with_metadata(name.to_string(), value, metadata);
291 self.data.add_metric(point)
292 }
293
294 pub fn add_metrics_from_arrays(
296 &self,
297 metric_names: &[String],
298 values: &Array1<f64>,
299 ) -> Result<()> {
300 if metric_names.len() != values.len() {
301 return Err(MetricsError::InvalidInput(
302 "Metric _names and values must have same length".to_string(),
303 ));
304 }
305
306 let points: Vec<MetricDataPoint> = metric_names
307 .iter()
308 .zip(values.iter())
309 .map(|(name, &value)| MetricDataPoint::new(name.clone(), value))
310 .collect();
311
312 self.data.add_metrics(points)
313 }
314
315 pub fn start_server(&self) -> Result<DashboardServer> {
317 #[cfg(feature = "dashboard_server")]
318 {
319 let _http_server = server::start_http_server(self.clone())?;
321
322 Ok(DashboardServer {
323 address: self.config.address,
324 is_running: true,
325 })
326 }
327
328 #[cfg(not(feature = "dashboard_server"))]
329 {
330 println!(
331 "Dashboard server feature not enabled. Starting mock server at http://{}",
332 self.config.address
333 );
334 println!("Dashboard title: {}", self.config.title);
335 println!("Refresh interval: {} seconds", self.config.refresh_interval);
336 println!("Float-time updates: {}", self.config.enable_realtime);
337 println!("To use the real HTTP server, enable the 'dashboard_server' feature");
338
339 Ok(DashboardServer {
341 address: self.config.address,
342 is_running: true,
343 })
344 }
345 }
346
347 pub fn export_to_json(&self) -> Result<String> {
349 let data = self.data.get_all_metrics()?;
350 serde_json::to_string_pretty(&data)
351 .map_err(|e| MetricsError::InvalidInput(format!("Failed to serialize data: {e}")))
352 }
353
354 pub fn export_to_csv(&self) -> Result<String> {
356 let data = self.data.get_all_metrics()?;
357
358 let mut csv = "timestamp,name,value,metadata\n".to_string();
359
360 for point in data {
361 let metadata_str = if point.metadata.is_empty() {
362 String::new()
363 } else {
364 serde_json::to_string(&point.metadata).unwrap_or_default()
365 };
366
367 csv.push_str(&format!(
368 "{},{},{},{}\n",
369 point.timestamp, point.name, point.value, metadata_str
370 ));
371 }
372
373 Ok(csv)
374 }
375
376 pub fn get_all_metrics(&self) -> Result<Vec<MetricDataPoint>> {
378 self.data.get_all_metrics()
379 }
380
381 pub fn get_metrics_by_name(&self, name: &str) -> Result<Vec<MetricDataPoint>> {
383 self.data.get_metrics_by_name(name)
384 }
385
386 pub fn get_metric_names(&self) -> Result<Vec<String>> {
388 self.data.get_metric_names()
389 }
390
391 pub fn get_metrics_in_range(
393 &self,
394 start_time: u64,
395 end_time: u64,
396 ) -> Result<Vec<MetricDataPoint>> {
397 self.data.get_metrics_in_range(start_time, end_time)
398 }
399
400 pub fn clear_data(&self) -> Result<()> {
402 self.data.clear()
403 }
404
405 pub fn get_statistics(&self) -> Result<DashboardStatistics> {
407 let data = self.data.get_all_metrics()?;
408 let metric_names = self.data.get_metric_names()?;
409
410 let mut metric_counts = HashMap::new();
411 let mut latest_values = HashMap::new();
412
413 for point in &data {
414 *metric_counts.entry(point.name.clone()).or_insert(0) += 1;
415 latest_values.insert(point.name.clone(), point.value);
416 }
417
418 let total_points = data.len();
419 let unique_metrics = metric_names.len();
420 let time_range = if data.is_empty() {
421 (0, 0)
422 } else {
423 let timestamps: Vec<u64> = data.iter().map(|p| p.timestamp).collect();
424 (
425 *timestamps.iter().min().expect("Operation failed"),
426 *timestamps.iter().max().expect("Operation failed"),
427 )
428 };
429
430 Ok(DashboardStatistics {
431 total_data_points: total_points,
432 unique_metrics,
433 metric_counts,
434 latest_values,
435 time_range,
436 })
437 }
438
439 pub fn generate_html(&self) -> Result<String> {
441 let stats = self.get_statistics()?;
442 let data = self.data.get_all_metrics()?;
443
444 let html = format!(
445 r#"
446<!DOCTYPE html>
447<html lang="en">
448<head>
449 <meta charset="UTF-8">
450 <meta name="viewport" content="width=device-width, initial-scale=1.0">
451 <title>{}</title>
452 <style>
453 body {{
454 font-family: Arial, sans-serif;
455 background-color: {};
456 color: {};
457 margin: 0;
458 padding: 20px;
459 }}
460 .header {{
461 background-color: {};
462 color: white;
463 padding: 20px;
464 border-radius: 8px;
465 margin-bottom: 20px;
466 }}
467 .stats {{
468 display: grid;
469 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
470 gap: 20px;
471 margin-bottom: 20px;
472 }}
473 .stat-card {{
474 background: white;
475 padding: 15px;
476 border-radius: 8px;
477 box-shadow: 0 2px 4px rgba(0,0,0,0.1);
478 }}
479 .data-table {{
480 background: white;
481 border-radius: 8px;
482 overflow: hidden;
483 box-shadow: 0 2px 4px rgba(0,0,0,0.1);
484 }}
485 table {{
486 width: 100%;
487 border-collapse: collapse;
488 }}
489 th, td {{
490 padding: 12px;
491 text-align: left;
492 border-bottom: 1px solid #ddd;
493 }}
494 th {{
495 background-color: {};
496 color: white;
497 }}
498 </style>
499</head>
500<body>
501 <div class="header">
502 <h1>{}</h1>
503 <p>Interactive Machine Learning Metrics Dashboard</p>
504 </div>
505
506 <div class="stats">
507 <div class="stat-card">
508 <h3>Total Data Points</h3>
509 <p style="font-size: 24px; margin: 0;">{}</p>
510 </div>
511 <div class="stat-card">
512 <h3>Unique Metrics</h3>
513 <p style="font-size: 24px; margin: 0;">{}</p>
514 </div>
515 <div class="stat-card">
516 <h3>Time Range</h3>
517 <p style="font-size: 14px; margin: 0;">{} - {}</p>
518 </div>
519 </div>
520
521 <div class="data-table">
522 <table>
523 <thead>
524 <tr>
525 <th>Timestamp</th>
526 <th>Metric Name</th>
527 <th>Value</th>
528 <th>Metadata</th>
529 </tr>
530 </thead>
531 <tbody>
532"#,
533 self.config.title,
534 self.config.theme.background_color,
535 self.config.theme.text_color,
536 self.config.theme.primary_color,
537 self.config.theme.primary_color,
538 self.config.title,
539 stats.total_data_points,
540 stats.unique_metrics,
541 stats.time_range.0,
542 stats.time_range.1
543 );
544
545 let mut rows = String::new();
546 for point in data.iter().take(100) {
547 let metadata_display = if point.metadata.is_empty() {
549 "-".to_string()
550 } else {
551 format!("{} keys", point.metadata.len())
552 };
553
554 rows.push_str(&format!(
555 "<tr><td>{}</td><td>{}</td><td>{:.6}</td><td>{}</td></tr>\n",
556 point.timestamp, point.name, point.value, metadata_display
557 ));
558 }
559
560 let footer = r#"
561 </tbody>
562 </table>
563 </div>
564
565 <script>
566 // Auto-refresh functionality (placeholder)
567 setInterval(() => {
568 console.log('Refreshing dashboard data...');
569 }, 5000);
570 </script>
571</body>
572</html>
573"#;
574
575 Ok(format!("{html}{rows}{footer}"))
576 }
577}
578
579#[derive(Debug)]
581pub struct DashboardServer {
582 pub address: SocketAddr,
584 pub is_running: bool,
586}
587
588impl DashboardServer {
589 pub fn stop(&mut self) {
591 self.is_running = false;
592 println!("Dashboard server stopped");
593 }
594
595 pub fn is_running(&self) -> bool {
597 self.is_running
598 }
599}
600
601#[derive(Debug, Clone, Serialize, Deserialize)]
603pub struct DashboardStatistics {
604 pub total_data_points: usize,
606 pub unique_metrics: usize,
608 pub metric_counts: HashMap<String, usize>,
610 pub latest_values: HashMap<String, f64>,
612 pub time_range: (u64, u64),
614}
615
616#[derive(Debug, Clone)]
618pub struct DashboardWidget {
619 pub id: String,
621 pub title: String,
623 pub metrics: Vec<String>,
625 pub widget_type: WidgetType,
627 pub config: HashMap<String, String>,
629}
630
631#[derive(Debug, Clone, Serialize, Deserialize)]
633pub enum WidgetType {
634 LineChart,
636 BarChart,
638 Gauge,
640 Table,
642 Heatmap,
644 ConfusionMatrix,
646 RocCurve,
648 Custom(String),
650}
651
652impl DashboardWidget {
653 pub fn line_chart(id: String, title: String, metrics: Vec<String>) -> Self {
655 Self {
656 id,
657 title,
658 metrics,
659 widget_type: WidgetType::LineChart,
660 config: HashMap::new(),
661 }
662 }
663
664 pub fn gauge(id: String, title: String, metric: String) -> Self {
666 Self {
667 id,
668 title,
669 metrics: vec![metric],
670 widget_type: WidgetType::Gauge,
671 config: HashMap::new(),
672 }
673 }
674
675 pub fn table(id: String, title: String, metrics: Vec<String>) -> Self {
677 Self {
678 id,
679 title,
680 metrics,
681 widget_type: WidgetType::Table,
682 config: HashMap::new(),
683 }
684 }
685
686 pub fn with_config(mut self, key: String, value: String) -> Self {
688 self.config.insert(key, value);
689 self
690 }
691}
692
693pub mod utils {
695 use super::*;
696
697 pub fn create_classification_dashboard(
699 accuracy: f64,
700 precision: f64,
701 recall: f64,
702 f1_score: f64,
703 ) -> Result<InteractiveDashboard> {
704 let dashboard = InteractiveDashboard::default();
705
706 dashboard.add_metric("accuracy", accuracy)?;
707 dashboard.add_metric("precision", precision)?;
708 dashboard.add_metric("recall", recall)?;
709 dashboard.add_metric("f1_score", f1_score)?;
710
711 Ok(dashboard)
712 }
713
714 pub fn create_regression_dashboard(
716 mse: f64,
717 rmse: f64,
718 mae: f64,
719 r2: f64,
720 ) -> Result<InteractiveDashboard> {
721 let dashboard = InteractiveDashboard::default();
722
723 dashboard.add_metric("mse", mse)?;
724 dashboard.add_metric("rmse", rmse)?;
725 dashboard.add_metric("mae", mae)?;
726 dashboard.add_metric("r2", r2)?;
727
728 Ok(dashboard)
729 }
730
731 pub fn create_clustering_dashboard(
733 silhouette_score: f64,
734 davies_bouldin: f64,
735 calinski_harabasz: f64,
736 ) -> Result<InteractiveDashboard> {
737 let dashboard = InteractiveDashboard::default();
738
739 dashboard.add_metric("silhouette_score", silhouette_score)?;
740 dashboard.add_metric("davies_bouldin", davies_bouldin)?;
741 dashboard.add_metric("calinski_harabasz", calinski_harabasz)?;
742
743 Ok(dashboard)
744 }
745
746 pub fn export_dashboard_to_file(
748 dashboard: &InteractiveDashboard,
749 file_path: &str,
750 format: ExportFormat,
751 ) -> Result<()> {
752 let content = match format {
753 ExportFormat::Json => dashboard.export_to_json()?,
754 ExportFormat::Csv => dashboard.export_to_csv()?,
755 ExportFormat::Html => dashboard.generate_html()?,
756 };
757
758 std::fs::write(file_path, content)
759 .map_err(|e| MetricsError::InvalidInput(format!("Failed to write file: {e}")))?;
760
761 Ok(())
762 }
763
764 #[derive(Debug, Clone)]
766 pub enum ExportFormat {
767 Json,
768 Csv,
769 Html,
770 }
771}
772
773#[cfg(test)]
774mod tests {
775 use super::*;
776
777 #[test]
778 fn test_dashboard_creation() {
779 let config = DashboardConfig::default();
780 let dashboard = InteractiveDashboard::new(config);
781
782 assert!(dashboard.add_metric("accuracy", 0.95).is_ok());
783 assert!(dashboard.add_metric("precision", 0.92).is_ok());
784 assert!(dashboard.add_metric("recall", 0.88).is_ok());
785 }
786
787 #[test]
788 fn test_metric_data_point() {
789 let point = MetricDataPoint::new("accuracy".to_string(), 0.95);
790 assert_eq!(point.name, "accuracy");
791 assert_eq!(point.value, 0.95);
792 assert!(point.timestamp > 0);
793 }
794
795 #[test]
796 fn test_dashboard_data() {
797 let config = DashboardConfig::default();
798 let data = DashboardData::new(config);
799
800 let point1 = MetricDataPoint::new("accuracy".to_string(), 0.95);
801 let point2 = MetricDataPoint::new("precision".to_string(), 0.92);
802
803 assert!(data.add_metric(point1).is_ok());
804 assert!(data.add_metric(point2).is_ok());
805
806 let all_metrics = data.get_all_metrics().expect("Operation failed");
807 assert_eq!(all_metrics.len(), 2);
808
809 let accuracy_metrics = data
810 .get_metrics_by_name("accuracy")
811 .expect("Operation failed");
812 assert_eq!(accuracy_metrics.len(), 1);
813 assert_eq!(accuracy_metrics[0].value, 0.95);
814 }
815
816 #[test]
817 fn test_dashboard_statistics() {
818 let dashboard = InteractiveDashboard::default();
819
820 assert!(dashboard.add_metric("accuracy", 0.95).is_ok());
821 assert!(dashboard.add_metric("precision", 0.92).is_ok());
822 assert!(dashboard.add_metric("accuracy", 0.97).is_ok());
823
824 let stats = dashboard.get_statistics().expect("Operation failed");
825 assert_eq!(stats.total_data_points, 3);
826 assert_eq!(stats.unique_metrics, 2);
827 assert_eq!(stats.metric_counts["accuracy"], 2);
828 assert_eq!(stats.metric_counts["precision"], 1);
829 }
830
831 #[test]
832 fn test_export_functions() {
833 let dashboard = InteractiveDashboard::default();
834
835 assert!(dashboard.add_metric("accuracy", 0.95).is_ok());
836 assert!(dashboard.add_metric("precision", 0.92).is_ok());
837
838 let json_export = dashboard.export_to_json();
839 assert!(json_export.is_ok());
840 assert!(json_export.expect("Operation failed").contains("accuracy"));
841
842 let csv_export = dashboard.export_to_csv();
843 assert!(csv_export.is_ok());
844 assert!(csv_export
845 .expect("Operation failed")
846 .contains("timestamp,name,value"));
847
848 let html_export = dashboard.generate_html();
849 assert!(html_export.is_ok());
850 assert!(html_export
851 .expect("Operation failed")
852 .contains("<!DOCTYPE html>"));
853 }
854
855 #[test]
856 fn test_dashboard_widgets() {
857 let widget = DashboardWidget::line_chart(
858 "accuracy_chart".to_string(),
859 "Model Accuracy".to_string(),
860 vec!["accuracy".to_string()],
861 );
862
863 assert_eq!(widget.id, "accuracy_chart");
864 assert_eq!(widget.title, "Model Accuracy");
865 assert!(matches!(widget.widget_type, WidgetType::LineChart));
866 }
867
868 #[test]
869 fn test_utility_functions() {
870 let dashboard = utils::create_classification_dashboard(0.95, 0.92, 0.88, 0.90)
871 .expect("Operation failed");
872 let stats = dashboard.get_statistics().expect("Operation failed");
873
874 assert_eq!(stats.total_data_points, 4);
875 assert_eq!(stats.unique_metrics, 4);
876 assert!(stats.latest_values.contains_key("accuracy"));
877 }
878}