scirs2_metrics/
dashboard.rs

1//! Interactive visualization dashboard for metrics
2//!
3//! This module provides a web-based interactive dashboard for visualizing
4//! machine learning metrics in real-time, with export capabilities and
5//! customizable visualizations.
6//!
7//! # HTTP Server Support
8//!
9//! When the `dashboard_server` feature is enabled, this module provides
10//! a real HTTP server implementation using tokio. To use it:
11//!
12//! ```no_run
13//! # #[cfg(feature = "dashboard_server")]
14//! # {
15//! use scirs2_metrics::dashboard::{InteractiveDashboard, DashboardConfig};
16//! use scirs2_metrics::dashboard::server::start_http_server;
17//!
18//! let dashboard = InteractiveDashboard::default();
19//! dashboard.add_metric("accuracy", 0.95).expect("Operation failed");
20//!
21//! // Start the HTTP server
22//! let server = start_http_server(dashboard).expect("Operation failed");
23//! # }
24//! ```
25
26use 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// Include dashboard server implementation if tokio is available
35#[cfg(feature = "dashboard_server")]
36pub mod server;
37
38/// Configuration for the interactive dashboard
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct DashboardConfig {
41    /// Server listening address
42    pub address: SocketAddr,
43    /// Auto-refresh interval in seconds
44    pub refresh_interval: u64,
45    /// Maximum number of data points to keep in memory
46    pub max_data_points: usize,
47    /// Enable real-time updates
48    pub enable_realtime: bool,
49    /// Dashboard title
50    pub title: String,
51    /// Theme configuration
52    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/// Theme configuration for the dashboard
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct DashboardTheme {
71    /// Primary color (hex)
72    pub primary_color: String,
73    /// Background color (hex)
74    pub background_color: String,
75    /// Text color (hex)
76    pub text_color: String,
77    /// Chart colors
78    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/// Metric data point for dashboard visualization
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct MetricDataPoint {
102    /// Timestamp of the measurement
103    pub timestamp: u64,
104    /// Metric name
105    pub name: String,
106    /// Metric value
107    pub value: f64,
108    /// Optional metadata
109    pub metadata: HashMap<String, String>,
110}
111
112impl MetricDataPoint {
113    /// Create a new metric data point
114    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    /// Create a new metric data point with metadata
129    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/// Dashboard data storage and management
137#[derive(Debug, Clone)]
138pub struct DashboardData {
139    /// Stored metric data points
140    data_points: Arc<Mutex<Vec<MetricDataPoint>>>,
141    /// Configuration
142    config: DashboardConfig,
143}
144
145impl DashboardData {
146    /// Create new dashboard data storage
147    pub fn new(config: DashboardConfig) -> Self {
148        Self {
149            data_points: Arc::new(Mutex::new(Vec::new())),
150            config,
151        }
152    }
153
154    /// Add a metric data point
155    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        // Keep only the most recent data points
164        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    /// Add multiple metric data points
173    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    /// Get all metric data points
181    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    /// Get metric data points by name
191    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    /// Get metric names
207    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    /// Clear all data
221    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    /// Get data points within time range
232    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/// Interactive dashboard server
253#[derive(Debug, Clone)]
254pub struct InteractiveDashboard {
255    /// Dashboard data
256    data: DashboardData,
257    /// Server configuration
258    config: DashboardConfig,
259}
260
261impl InteractiveDashboard {
262    /// Create new interactive dashboard
263    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    /// Add metric measurement to dashboard
278    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    /// Add metric measurement with metadata
284    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    /// Add batch of metrics from arrays
295    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    /// Start the dashboard server
316    pub fn start_server(&self) -> Result<DashboardServer> {
317        #[cfg(feature = "dashboard_server")]
318        {
319            // Use actual HTTP server when feature is enabled
320            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            // Return mock server when feature is not enabled
340            Ok(DashboardServer {
341                address: self.config.address,
342                is_running: true,
343            })
344        }
345    }
346
347    /// Export data to JSON
348    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    /// Export data to CSV
355    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    /// Get all metric data points
377    pub fn get_all_metrics(&self) -> Result<Vec<MetricDataPoint>> {
378        self.data.get_all_metrics()
379    }
380
381    /// Get metric data points by name
382    pub fn get_metrics_by_name(&self, name: &str) -> Result<Vec<MetricDataPoint>> {
383        self.data.get_metrics_by_name(name)
384    }
385
386    /// Get metric names
387    pub fn get_metric_names(&self) -> Result<Vec<String>> {
388        self.data.get_metric_names()
389    }
390
391    /// Get data points within time range
392    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    /// Clear all data
401    pub fn clear_data(&self) -> Result<()> {
402        self.data.clear()
403    }
404
405    /// Get dashboard statistics
406    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    /// Generate HTML dashboard (basic implementation)
440    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            // Show only first 100 points
548            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/// Dashboard server handle (placeholder for actual server)
580#[derive(Debug)]
581pub struct DashboardServer {
582    /// Server address
583    pub address: SocketAddr,
584    /// Server running status
585    pub is_running: bool,
586}
587
588impl DashboardServer {
589    /// Stop the server
590    pub fn stop(&mut self) {
591        self.is_running = false;
592        println!("Dashboard server stopped");
593    }
594
595    /// Check if server is running
596    pub fn is_running(&self) -> bool {
597        self.is_running
598    }
599}
600
601/// Dashboard statistics
602#[derive(Debug, Clone, Serialize, Deserialize)]
603pub struct DashboardStatistics {
604    /// Total number of data points
605    pub total_data_points: usize,
606    /// Number of unique metrics
607    pub unique_metrics: usize,
608    /// Count of data points per metric
609    pub metric_counts: HashMap<String, usize>,
610    /// Latest value for each metric
611    pub latest_values: HashMap<String, f64>,
612    /// Time range (start, end) timestamps
613    pub time_range: (u64, u64),
614}
615
616/// Dashboard widget for embedding metrics
617#[derive(Debug, Clone)]
618pub struct DashboardWidget {
619    /// Widget identifier
620    pub id: String,
621    /// Widget title
622    pub title: String,
623    /// Metric names to display
624    pub metrics: Vec<String>,
625    /// Widget type
626    pub widget_type: WidgetType,
627    /// Configuration options
628    pub config: HashMap<String, String>,
629}
630
631/// Types of dashboard widgets
632#[derive(Debug, Clone, Serialize, Deserialize)]
633pub enum WidgetType {
634    /// Line chart for time series data
635    LineChart,
636    /// Bar chart for categorical data
637    BarChart,
638    /// Gauge for single value metrics
639    Gauge,
640    /// Table for tabular data
641    Table,
642    /// Heatmap for correlation matrices
643    Heatmap,
644    /// Confusion matrix visualization
645    ConfusionMatrix,
646    /// ROC curve
647    RocCurve,
648    /// Custom widget type
649    Custom(String),
650}
651
652impl DashboardWidget {
653    /// Create new line chart widget
654    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    /// Create new gauge widget
665    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    /// Create new table widget
676    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    /// Add configuration option
687    pub fn with_config(mut self, key: String, value: String) -> Self {
688        self.config.insert(key, value);
689        self
690    }
691}
692
693/// Utility functions for dashboard creation
694pub mod utils {
695    use super::*;
696
697    /// Create a dashboard from classification metrics
698    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    /// Create a dashboard from regression metrics
715    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    /// Create a dashboard from clustering metrics
732    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    /// Export dashboard data to file
747    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    /// Export formats
765    #[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}