Skip to main content

oxirs_federate/
advanced_visualization.rs

1//! # Advanced Visualization & Dashboarding Module
2//!
3//! Comprehensive visualization and dashboarding capabilities:
4//! - Real-time metrics visualization
5//! - Query performance dashboards
6//! - Federation topology visualization
7//! - Security monitoring dashboards
8//! - Compliance dashboards
9//! - Customizable widget system
10//! - Alert visualization
11//! - Export capabilities (PNG, SVG, JSON)
12
13use anyhow::{anyhow, Result};
14use chrono::{DateTime, Utc};
15use dashmap::DashMap;
16use scirs2_core::ndarray_ext::Array2;
17use serde::{Deserialize, Serialize};
18use std::collections::{HashMap, VecDeque};
19use std::sync::Arc;
20use std::time::Duration;
21use tokio::sync::RwLock;
22use tracing::info;
23
24/// Main visualization and dashboarding system
25#[derive(Clone)]
26pub struct AdvancedVisualization {
27    #[allow(dead_code)]
28    config: VisualizationConfig,
29    dashboards: Arc<DashMap<String, Dashboard>>,
30    metrics_collector: Arc<MetricsCollector>,
31    chart_generator: Arc<ChartGenerator>,
32    topology_visualizer: Arc<TopologyVisualizer>,
33    alert_visualizer: Arc<AlertVisualizer>,
34}
35
36/// Visualization configuration
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct VisualizationConfig {
39    /// Enable real-time updates
40    pub enable_realtime: bool,
41    /// Update interval for real-time dashboards
42    pub update_interval: Duration,
43    /// Default chart theme
44    pub default_theme: ChartTheme,
45    /// Maximum data points to display
46    pub max_data_points: usize,
47    /// Enable export features
48    pub enable_export: bool,
49    /// Default export format
50    pub default_export_format: ExportFormat,
51}
52
53impl Default for VisualizationConfig {
54    fn default() -> Self {
55        Self {
56            enable_realtime: true,
57            update_interval: Duration::from_secs(5),
58            default_theme: ChartTheme::Dark,
59            max_data_points: 1000,
60            enable_export: true,
61            default_export_format: ExportFormat::SVG,
62        }
63    }
64}
65
66/// Chart theme
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub enum ChartTheme {
69    Light,
70    Dark,
71    HighContrast,
72    Custom(CustomTheme),
73}
74
75/// Custom theme configuration
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct CustomTheme {
78    pub background_color: String,
79    pub text_color: String,
80    pub primary_color: String,
81    pub secondary_color: String,
82    pub grid_color: String,
83}
84
85/// Export format
86#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
87pub enum ExportFormat {
88    PNG,
89    SVG,
90    JSON,
91    CSV,
92    PDF,
93}
94
95/// Dashboard
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct Dashboard {
98    pub id: String,
99    pub name: String,
100    pub description: String,
101    pub layout: DashboardLayout,
102    pub widgets: Vec<Widget>,
103    pub created_at: DateTime<Utc>,
104    pub last_updated: DateTime<Utc>,
105    pub auto_refresh: bool,
106    pub refresh_interval: Duration,
107}
108
109/// Dashboard layout
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub enum DashboardLayout {
112    Grid { rows: usize, cols: usize },
113    Flexible,
114    SingleColumn,
115    TwoColumn,
116}
117
118/// Widget
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct Widget {
121    pub id: String,
122    pub widget_type: WidgetType,
123    pub title: String,
124    pub position: WidgetPosition,
125    pub size: WidgetSize,
126    pub data_source: DataSource,
127    pub visualization_type: VisualizationType,
128    pub config: WidgetConfig,
129}
130
131/// Widget position
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct WidgetPosition {
134    pub row: usize,
135    pub col: usize,
136}
137
138/// Widget size
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct WidgetSize {
141    pub width: usize,  // Grid units
142    pub height: usize, // Grid units
143}
144
145/// Widget type
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub enum WidgetType {
148    Chart,
149    Table,
150    Map,
151    Topology,
152    Alert,
153    Metric,
154    Text,
155}
156
157/// Data source for widgets
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub enum DataSource {
160    QueryMetrics,
161    FederationTopology,
162    SecurityAlerts,
163    ComplianceStatus,
164    PerformanceMetrics,
165    ServiceHealth,
166    Custom(String),
167}
168
169/// Visualization type
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub enum VisualizationType {
172    LineChart,
173    BarChart,
174    PieChart,
175    ScatterPlot,
176    Heatmap,
177    NetworkGraph,
178    Gauge,
179    Table,
180    TreeMap,
181    Sankey,
182}
183
184/// Widget configuration
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct WidgetConfig {
187    pub show_legend: bool,
188    pub show_grid: bool,
189    pub x_axis_label: Option<String>,
190    pub y_axis_label: Option<String>,
191    pub color_scheme: Vec<String>,
192    pub custom_options: HashMap<String, serde_json::Value>,
193}
194
195impl Default for WidgetConfig {
196    fn default() -> Self {
197        Self {
198            show_legend: true,
199            show_grid: true,
200            x_axis_label: None,
201            y_axis_label: None,
202            color_scheme: vec![
203                "#1f77b4".to_string(),
204                "#ff7f0e".to_string(),
205                "#2ca02c".to_string(),
206                "#d62728".to_string(),
207                "#9467bd".to_string(),
208            ],
209            custom_options: HashMap::new(),
210        }
211    }
212}
213
214/// Metrics collector
215pub struct MetricsCollector {
216    time_series: Arc<RwLock<HashMap<String, TimeSeries>>>,
217    #[allow(dead_code)]
218    aggregations: Arc<RwLock<HashMap<String, MetricAggregation>>>,
219}
220
221impl Default for MetricsCollector {
222    fn default() -> Self {
223        Self {
224            time_series: Arc::new(RwLock::new(HashMap::new())),
225            aggregations: Arc::new(RwLock::new(HashMap::new())),
226        }
227    }
228}
229
230impl MetricsCollector {
231    pub fn new() -> Self {
232        Self::default()
233    }
234
235    /// Record metric value
236    pub async fn record_metric(&self, metric_name: &str, value: f64) -> Result<()> {
237        let mut time_series = self.time_series.write().await;
238        let series = time_series
239            .entry(metric_name.to_string())
240            .or_insert_with(|| TimeSeries {
241                name: metric_name.to_string(),
242                data_points: VecDeque::new(),
243                unit: "".to_string(),
244            });
245
246        series.data_points.push_back(DataPoint {
247            timestamp: Utc::now(),
248            value,
249        });
250
251        // Keep only recent data points (last 1000)
252        if series.data_points.len() > 1000 {
253            series.data_points.pop_front();
254        }
255
256        Ok(())
257    }
258
259    /// Get time series data
260    pub async fn get_time_series(&self, metric_name: &str) -> Option<TimeSeries> {
261        let time_series = self.time_series.read().await;
262        time_series.get(metric_name).cloned()
263    }
264
265    /// Calculate aggregation
266    pub async fn calculate_aggregation(
267        &self,
268        metric_name: &str,
269        aggregation_type: AggregationType,
270        window: Duration,
271    ) -> Result<f64> {
272        let time_series = self.time_series.read().await;
273        let series = time_series
274            .get(metric_name)
275            .ok_or_else(|| anyhow!("Metric not found: {}", metric_name))?;
276
277        let cutoff = Utc::now() - chrono::Duration::from_std(window)?;
278        let recent_values: Vec<f64> = series
279            .data_points
280            .iter()
281            .filter(|dp| dp.timestamp >= cutoff)
282            .map(|dp| dp.value)
283            .collect();
284
285        if recent_values.is_empty() {
286            return Ok(0.0);
287        }
288
289        let result = match aggregation_type {
290            AggregationType::Average => {
291                recent_values.iter().sum::<f64>() / recent_values.len() as f64
292            }
293            AggregationType::Sum => recent_values.iter().sum(),
294            AggregationType::Min => recent_values.iter().cloned().fold(f64::INFINITY, f64::min),
295            AggregationType::Max => recent_values
296                .iter()
297                .cloned()
298                .fold(f64::NEG_INFINITY, f64::max),
299            AggregationType::Count => recent_values.len() as f64,
300            AggregationType::Percentile(p) => {
301                let mut sorted = recent_values.clone();
302                sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
303                let index = ((p / 100.0) * sorted.len() as f64) as usize;
304                sorted[index.min(sorted.len() - 1)]
305            }
306        };
307
308        Ok(result)
309    }
310}
311
312/// Time series data
313#[derive(Debug, Clone)]
314pub struct TimeSeries {
315    pub name: String,
316    pub data_points: VecDeque<DataPoint>,
317    pub unit: String,
318}
319
320/// Data point
321#[derive(Debug, Clone)]
322pub struct DataPoint {
323    pub timestamp: DateTime<Utc>,
324    pub value: f64,
325}
326
327/// Aggregation type
328#[derive(Debug, Clone, Serialize, Deserialize)]
329pub enum AggregationType {
330    Average,
331    Sum,
332    Min,
333    Max,
334    Count,
335    Percentile(f64),
336}
337
338/// Metric aggregation
339#[derive(Debug, Clone)]
340pub struct MetricAggregation {
341    pub metric_name: String,
342    pub aggregation_type: AggregationType,
343    pub value: f64,
344    pub calculated_at: DateTime<Utc>,
345}
346
347/// Chart generator
348pub struct ChartGenerator {
349    #[allow(dead_code)]
350    theme: ChartTheme,
351}
352
353impl ChartGenerator {
354    pub fn new(theme: ChartTheme) -> Self {
355        Self { theme }
356    }
357
358    /// Generate line chart
359    pub async fn generate_line_chart(
360        &self,
361        data: &TimeSeries,
362        config: &WidgetConfig,
363    ) -> Result<ChartData> {
364        let points: Vec<(f64, f64)> = data
365            .data_points
366            .iter()
367            .enumerate()
368            .map(|(i, dp)| (i as f64, dp.value))
369            .collect();
370
371        Ok(ChartData {
372            chart_type: VisualizationType::LineChart,
373            series: vec![ChartSeries {
374                name: data.name.clone(),
375                data: points,
376                color: config.color_scheme.first().cloned(),
377            }],
378            x_axis_label: config.x_axis_label.clone(),
379            y_axis_label: config.y_axis_label.clone(),
380            show_legend: config.show_legend,
381            show_grid: config.show_grid,
382        })
383    }
384
385    /// Generate bar chart
386    pub async fn generate_bar_chart(
387        &self,
388        _categories: Vec<String>,
389        values: Vec<f64>,
390        config: &WidgetConfig,
391    ) -> Result<ChartData> {
392        let points: Vec<(f64, f64)> = values
393            .iter()
394            .enumerate()
395            .map(|(i, &v)| (i as f64, v))
396            .collect();
397
398        Ok(ChartData {
399            chart_type: VisualizationType::BarChart,
400            series: vec![ChartSeries {
401                name: "Values".to_string(),
402                data: points,
403                color: config.color_scheme.first().cloned(),
404            }],
405            x_axis_label: config.x_axis_label.clone(),
406            y_axis_label: config.y_axis_label.clone(),
407            show_legend: config.show_legend,
408            show_grid: config.show_grid,
409        })
410    }
411
412    /// Generate pie chart
413    pub async fn generate_pie_chart(
414        &self,
415        labels: Vec<String>,
416        values: Vec<f64>,
417        config: &WidgetConfig,
418    ) -> Result<PieChartData> {
419        let total: f64 = values.iter().sum();
420        let slices: Vec<PieSlice> = labels
421            .into_iter()
422            .zip(values.iter())
423            .enumerate()
424            .map(|(i, (label, &value))| PieSlice {
425                label,
426                value,
427                percentage: (value / total) * 100.0,
428                color: config
429                    .color_scheme
430                    .get(i % config.color_scheme.len())
431                    .cloned(),
432            })
433            .collect();
434
435        Ok(PieChartData {
436            slices,
437            show_legend: config.show_legend,
438        })
439    }
440
441    /// Generate heatmap
442    pub async fn generate_heatmap(
443        &self,
444        data: Array2<f64>,
445        row_labels: Vec<String>,
446        col_labels: Vec<String>,
447        _config: &WidgetConfig,
448    ) -> Result<HeatmapData> {
449        let (rows, cols) = data.dim();
450        let mut cells = Vec::new();
451
452        for i in 0..rows {
453            for j in 0..cols {
454                cells.push(HeatmapCell {
455                    row: i,
456                    col: j,
457                    value: data[[i, j]],
458                });
459            }
460        }
461
462        Ok(HeatmapData {
463            cells,
464            row_labels,
465            col_labels,
466            color_scale: ColorScale::Viridis,
467        })
468    }
469}
470
471/// Chart data
472#[derive(Debug, Clone, Serialize)]
473pub struct ChartData {
474    pub chart_type: VisualizationType,
475    pub series: Vec<ChartSeries>,
476    pub x_axis_label: Option<String>,
477    pub y_axis_label: Option<String>,
478    pub show_legend: bool,
479    pub show_grid: bool,
480}
481
482/// Chart series
483#[derive(Debug, Clone, Serialize)]
484pub struct ChartSeries {
485    pub name: String,
486    pub data: Vec<(f64, f64)>,
487    pub color: Option<String>,
488}
489
490/// Pie chart data
491#[derive(Debug, Clone, Serialize)]
492pub struct PieChartData {
493    pub slices: Vec<PieSlice>,
494    pub show_legend: bool,
495}
496
497/// Pie slice
498#[derive(Debug, Clone, Serialize)]
499pub struct PieSlice {
500    pub label: String,
501    pub value: f64,
502    pub percentage: f64,
503    pub color: Option<String>,
504}
505
506/// Heatmap data
507#[derive(Debug, Clone, Serialize)]
508pub struct HeatmapData {
509    pub cells: Vec<HeatmapCell>,
510    pub row_labels: Vec<String>,
511    pub col_labels: Vec<String>,
512    pub color_scale: ColorScale,
513}
514
515/// Heatmap cell
516#[derive(Debug, Clone, Serialize)]
517pub struct HeatmapCell {
518    pub row: usize,
519    pub col: usize,
520    pub value: f64,
521}
522
523/// Color scale for heatmaps
524#[derive(Debug, Clone, Serialize)]
525pub enum ColorScale {
526    Viridis,
527    Plasma,
528    Inferno,
529    Magma,
530    Grayscale,
531}
532
533/// Topology visualizer
534pub struct TopologyVisualizer {
535    nodes: Arc<RwLock<Vec<TopologyNode>>>,
536    edges: Arc<RwLock<Vec<TopologyEdge>>>,
537}
538
539impl Default for TopologyVisualizer {
540    fn default() -> Self {
541        Self {
542            nodes: Arc::new(RwLock::new(Vec::new())),
543            edges: Arc::new(RwLock::new(Vec::new())),
544        }
545    }
546}
547
548impl TopologyVisualizer {
549    pub fn new() -> Self {
550        Self::default()
551    }
552
553    /// Add node to topology
554    pub async fn add_node(&self, node: TopologyNode) -> Result<()> {
555        let mut nodes = self.nodes.write().await;
556        nodes.push(node);
557        Ok(())
558    }
559
560    /// Add edge to topology
561    pub async fn add_edge(&self, edge: TopologyEdge) -> Result<()> {
562        let mut edges = self.edges.write().await;
563        edges.push(edge);
564        Ok(())
565    }
566
567    /// Generate topology visualization
568    pub async fn generate_topology(&self) -> Result<TopologyVisualization> {
569        let nodes = self.nodes.read().await.clone();
570        let edges = self.edges.read().await.clone();
571
572        // Calculate node positions using force-directed layout (simplified)
573        let positioned_nodes = self.calculate_layout(&nodes, &edges).await?;
574
575        Ok(TopologyVisualization {
576            nodes: positioned_nodes,
577            edges,
578            layout_algorithm: LayoutAlgorithm::ForceDirected,
579        })
580    }
581
582    /// Calculate node positions using force-directed layout
583    async fn calculate_layout(
584        &self,
585        nodes: &[TopologyNode],
586        _edges: &[TopologyEdge],
587    ) -> Result<Vec<PositionedNode>> {
588        // Simplified circular layout
589        let n = nodes.len();
590        let radius = 200.0;
591        let center_x = 300.0;
592        let center_y = 300.0;
593
594        let positioned: Vec<PositionedNode> = nodes
595            .iter()
596            .enumerate()
597            .map(|(i, node)| {
598                let angle = (i as f64 / n as f64) * 2.0 * std::f64::consts::PI;
599                let x = center_x + radius * angle.cos();
600                let y = center_y + radius * angle.sin();
601
602                PositionedNode {
603                    node: node.clone(),
604                    x,
605                    y,
606                }
607            })
608            .collect();
609
610        Ok(positioned)
611    }
612}
613
614/// Topology node
615#[derive(Debug, Clone, Serialize)]
616pub struct TopologyNode {
617    pub id: String,
618    pub label: String,
619    pub node_type: NodeType,
620    pub status: NodeStatus,
621    pub metadata: HashMap<String, String>,
622}
623
624/// Node type
625#[derive(Debug, Clone, Serialize)]
626pub enum NodeType {
627    Service,
628    DataSource,
629    Gateway,
630    Cache,
631    LoadBalancer,
632}
633
634/// Node status
635#[derive(Debug, Clone, Serialize)]
636pub enum NodeStatus {
637    Healthy,
638    Degraded,
639    Unhealthy,
640    Unknown,
641}
642
643/// Topology edge
644#[derive(Debug, Clone, Serialize)]
645pub struct TopologyEdge {
646    pub source_id: String,
647    pub target_id: String,
648    pub edge_type: EdgeType,
649    pub weight: f64,
650    pub label: Option<String>,
651}
652
653/// Edge type
654#[derive(Debug, Clone, Serialize)]
655pub enum EdgeType {
656    Query,
657    DataFlow,
658    Federation,
659    Replication,
660}
661
662/// Positioned node
663#[derive(Debug, Clone, Serialize)]
664pub struct PositionedNode {
665    pub node: TopologyNode,
666    pub x: f64,
667    pub y: f64,
668}
669
670/// Topology visualization
671#[derive(Debug, Clone, Serialize)]
672pub struct TopologyVisualization {
673    pub nodes: Vec<PositionedNode>,
674    pub edges: Vec<TopologyEdge>,
675    pub layout_algorithm: LayoutAlgorithm,
676}
677
678/// Layout algorithm
679#[derive(Debug, Clone, Serialize)]
680pub enum LayoutAlgorithm {
681    ForceDirected,
682    Hierarchical,
683    Circular,
684    Grid,
685}
686
687/// Alert visualizer
688pub struct AlertVisualizer {
689    alerts: Arc<RwLock<VecDeque<Alert>>>,
690}
691
692impl Default for AlertVisualizer {
693    fn default() -> Self {
694        Self {
695            alerts: Arc::new(RwLock::new(VecDeque::new())),
696        }
697    }
698}
699
700impl AlertVisualizer {
701    pub fn new() -> Self {
702        Self::default()
703    }
704
705    /// Add alert
706    pub async fn add_alert(&self, alert: Alert) -> Result<()> {
707        let mut alerts = self.alerts.write().await;
708        alerts.push_back(alert);
709
710        // Keep only recent alerts (last 100)
711        if alerts.len() > 100 {
712            alerts.pop_front();
713        }
714
715        Ok(())
716    }
717
718    /// Get alerts by severity
719    pub async fn get_alerts_by_severity(&self, severity: AlertSeverity) -> Vec<Alert> {
720        let alerts = self.alerts.read().await;
721        alerts
722            .iter()
723            .filter(|a| a.severity == severity)
724            .cloned()
725            .collect()
726    }
727
728    /// Generate alert timeline
729    pub async fn generate_alert_timeline(&self) -> Result<AlertTimeline> {
730        let alerts = self.alerts.read().await.clone().into_iter().collect();
731
732        Ok(AlertTimeline {
733            alerts,
734            group_by: AlertGrouping::Severity,
735        })
736    }
737}
738
739/// Alert
740#[derive(Debug, Clone, Serialize)]
741pub struct Alert {
742    pub id: String,
743    pub timestamp: DateTime<Utc>,
744    pub severity: AlertSeverity,
745    pub title: String,
746    pub description: String,
747    pub source: String,
748    pub acknowledged: bool,
749}
750
751/// Alert severity
752#[derive(Debug, Clone, Serialize, PartialEq)]
753pub enum AlertSeverity {
754    Critical,
755    High,
756    Medium,
757    Low,
758    Info,
759}
760
761/// Alert timeline
762#[derive(Debug, Clone, Serialize)]
763pub struct AlertTimeline {
764    pub alerts: Vec<Alert>,
765    pub group_by: AlertGrouping,
766}
767
768/// Alert grouping
769#[derive(Debug, Clone, Serialize)]
770pub enum AlertGrouping {
771    Severity,
772    Source,
773    Time,
774}
775
776impl AdvancedVisualization {
777    /// Create new visualization system
778    pub fn new(config: VisualizationConfig) -> Self {
779        Self {
780            config: config.clone(),
781            dashboards: Arc::new(DashMap::new()),
782            metrics_collector: Arc::new(MetricsCollector::new()),
783            chart_generator: Arc::new(ChartGenerator::new(config.default_theme.clone())),
784            topology_visualizer: Arc::new(TopologyVisualizer::new()),
785            alert_visualizer: Arc::new(AlertVisualizer::new()),
786        }
787    }
788
789    /// Create new dashboard
790    pub async fn create_dashboard(&self, mut dashboard: Dashboard) -> Result<String> {
791        dashboard.created_at = Utc::now();
792        dashboard.last_updated = Utc::now();
793        let id = dashboard.id.clone();
794
795        self.dashboards.insert(id.clone(), dashboard);
796        info!("Created dashboard: {}", id);
797
798        Ok(id)
799    }
800
801    /// Get dashboard
802    pub async fn get_dashboard(&self, dashboard_id: &str) -> Option<Dashboard> {
803        self.dashboards.get(dashboard_id).map(|d| d.clone())
804    }
805
806    /// Update dashboard
807    pub async fn update_dashboard(
808        &self,
809        dashboard_id: &str,
810        mut dashboard: Dashboard,
811    ) -> Result<()> {
812        dashboard.last_updated = Utc::now();
813        self.dashboards.insert(dashboard_id.to_string(), dashboard);
814        Ok(())
815    }
816
817    /// Delete dashboard
818    pub async fn delete_dashboard(&self, dashboard_id: &str) -> Result<()> {
819        self.dashboards.remove(dashboard_id);
820        Ok(())
821    }
822
823    /// Record metric
824    pub async fn record_metric(&self, metric_name: &str, value: f64) -> Result<()> {
825        self.metrics_collector
826            .record_metric(metric_name, value)
827            .await
828    }
829
830    /// Generate chart for widget
831    pub async fn generate_widget_chart(&self, widget: &Widget) -> Result<ChartData> {
832        match widget.data_source {
833            DataSource::QueryMetrics => {
834                if let Some(series) = self
835                    .metrics_collector
836                    .get_time_series("query_latency")
837                    .await
838                {
839                    self.chart_generator
840                        .generate_line_chart(&series, &widget.config)
841                        .await
842                } else {
843                    Err(anyhow!("No data available for query metrics"))
844                }
845            }
846            DataSource::PerformanceMetrics => {
847                if let Some(series) = self.metrics_collector.get_time_series("performance").await {
848                    self.chart_generator
849                        .generate_line_chart(&series, &widget.config)
850                        .await
851                } else {
852                    Err(anyhow!("No data available for performance metrics"))
853                }
854            }
855            DataSource::FederationTopology => {
856                if let Some(series) = self
857                    .metrics_collector
858                    .get_time_series("federation_topology")
859                    .await
860                {
861                    self.chart_generator
862                        .generate_line_chart(&series, &widget.config)
863                        .await
864                } else {
865                    Err(anyhow!(
866                        "No data available for federation topology; record metrics under \
867                         the 'federation_topology' key via record_metric()"
868                    ))
869                }
870            }
871            DataSource::SecurityAlerts => {
872                if let Some(series) = self
873                    .metrics_collector
874                    .get_time_series("security_alerts")
875                    .await
876                {
877                    self.chart_generator
878                        .generate_line_chart(&series, &widget.config)
879                        .await
880                } else {
881                    Err(anyhow!(
882                        "No data available for security alerts; record metrics under \
883                         the 'security_alerts' key via record_metric()"
884                    ))
885                }
886            }
887            DataSource::ComplianceStatus => {
888                if let Some(series) = self
889                    .metrics_collector
890                    .get_time_series("compliance_status")
891                    .await
892                {
893                    self.chart_generator
894                        .generate_line_chart(&series, &widget.config)
895                        .await
896                } else {
897                    Err(anyhow!(
898                        "No data available for compliance status; record metrics under \
899                         the 'compliance_status' key via record_metric()"
900                    ))
901                }
902            }
903            DataSource::ServiceHealth => {
904                if let Some(series) = self
905                    .metrics_collector
906                    .get_time_series("service_health")
907                    .await
908                {
909                    self.chart_generator
910                        .generate_line_chart(&series, &widget.config)
911                        .await
912                } else {
913                    Err(anyhow!(
914                        "No data available for service health; record metrics under \
915                         the 'service_health' key via record_metric()"
916                    ))
917                }
918            }
919            DataSource::Custom(ref key) => {
920                if let Some(series) = self.metrics_collector.get_time_series(key).await {
921                    self.chart_generator
922                        .generate_line_chart(&series, &widget.config)
923                        .await
924                } else {
925                    Err(anyhow!(
926                        "No data available for custom data source '{}'; \
927                         record metrics under that key via record_metric()",
928                        key
929                    ))
930                }
931            }
932        }
933    }
934
935    /// Generate topology visualization
936    pub async fn generate_topology_visualization(&self) -> Result<TopologyVisualization> {
937        self.topology_visualizer.generate_topology().await
938    }
939
940    /// Add topology node
941    pub async fn add_topology_node(&self, node: TopologyNode) -> Result<()> {
942        self.topology_visualizer.add_node(node).await
943    }
944
945    /// Add topology edge
946    pub async fn add_topology_edge(&self, edge: TopologyEdge) -> Result<()> {
947        self.topology_visualizer.add_edge(edge).await
948    }
949
950    /// Add alert
951    pub async fn add_alert(&self, alert: Alert) -> Result<()> {
952        self.alert_visualizer.add_alert(alert).await
953    }
954
955    /// Get alert timeline
956    pub async fn get_alert_timeline(&self) -> Result<AlertTimeline> {
957        self.alert_visualizer.generate_alert_timeline().await
958    }
959
960    /// Export dashboard
961    pub async fn export_dashboard(
962        &self,
963        dashboard_id: &str,
964        format: ExportFormat,
965    ) -> Result<Vec<u8>> {
966        let dashboard = self
967            .get_dashboard(dashboard_id)
968            .await
969            .ok_or_else(|| anyhow!("Dashboard not found"))?;
970
971        match format {
972            ExportFormat::JSON => {
973                let json = serde_json::to_string_pretty(&dashboard)?;
974                Ok(json.into_bytes())
975            }
976            ExportFormat::SVG => {
977                // Simplified SVG export
978                let svg = format!(
979                    r#"<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600">
980                    <text x="10" y="20">{}</text>
981                </svg>"#,
982                    dashboard.name
983                );
984                Ok(svg.into_bytes())
985            }
986            ExportFormat::CSV => {
987                // Serialize dashboard widgets as CSV: id, title, widget_type, data_source
988                let mut csv = String::new();
989                csv.push_str("id,title,widget_type,data_source\n");
990                for widget in &dashboard.widgets {
991                    let widget_type = format!("{:?}", widget.widget_type);
992                    let data_source = format!("{:?}", widget.data_source);
993                    // Escape fields: wrap in quotes and escape inner quotes
994                    let escape = |s: &str| {
995                        if s.contains(',') || s.contains('"') || s.contains('\n') {
996                            format!("\"{}\"", s.replace('"', "\"\""))
997                        } else {
998                            s.to_string()
999                        }
1000                    };
1001                    csv.push_str(&format!(
1002                        "{},{},{},{}\n",
1003                        escape(&widget.id),
1004                        escape(&widget.title),
1005                        escape(&widget_type),
1006                        escape(&data_source),
1007                    ));
1008                }
1009                Ok(csv.into_bytes())
1010            }
1011            ExportFormat::PNG => Err(anyhow!(
1012                "PNG export requires SVG rasterization; use ExportFormat::SVG and convert \
1013                 externally (resvg/tiny-skia depend on miniz_oxide which is prohibited by \
1014                 COOLJAPAN Pure Rust Policy)"
1015            )),
1016            ExportFormat::PDF => Err(anyhow!(
1017                "PDF export is not supported; export as SVG or JSON and convert using an \
1018                 external tool"
1019            )),
1020        }
1021    }
1022
1023    /// Create default performance dashboard
1024    pub async fn create_default_performance_dashboard(&self) -> Result<String> {
1025        let dashboard = Dashboard {
1026            id: uuid::Uuid::new_v4().to_string(),
1027            name: "Performance Dashboard".to_string(),
1028            description: "Real-time performance metrics".to_string(),
1029            layout: DashboardLayout::Grid { rows: 2, cols: 2 },
1030            widgets: vec![
1031                Widget {
1032                    id: uuid::Uuid::new_v4().to_string(),
1033                    widget_type: WidgetType::Chart,
1034                    title: "Query Latency".to_string(),
1035                    position: WidgetPosition { row: 0, col: 0 },
1036                    size: WidgetSize {
1037                        width: 1,
1038                        height: 1,
1039                    },
1040                    data_source: DataSource::QueryMetrics,
1041                    visualization_type: VisualizationType::LineChart,
1042                    config: WidgetConfig::default(),
1043                },
1044                Widget {
1045                    id: uuid::Uuid::new_v4().to_string(),
1046                    widget_type: WidgetType::Chart,
1047                    title: "Throughput".to_string(),
1048                    position: WidgetPosition { row: 0, col: 1 },
1049                    size: WidgetSize {
1050                        width: 1,
1051                        height: 1,
1052                    },
1053                    data_source: DataSource::PerformanceMetrics,
1054                    visualization_type: VisualizationType::BarChart,
1055                    config: WidgetConfig::default(),
1056                },
1057                Widget {
1058                    id: uuid::Uuid::new_v4().to_string(),
1059                    widget_type: WidgetType::Topology,
1060                    title: "Federation Topology".to_string(),
1061                    position: WidgetPosition { row: 1, col: 0 },
1062                    size: WidgetSize {
1063                        width: 2,
1064                        height: 1,
1065                    },
1066                    data_source: DataSource::FederationTopology,
1067                    visualization_type: VisualizationType::NetworkGraph,
1068                    config: WidgetConfig::default(),
1069                },
1070            ],
1071            created_at: Utc::now(),
1072            last_updated: Utc::now(),
1073            auto_refresh: true,
1074            refresh_interval: Duration::from_secs(5),
1075        };
1076
1077        self.create_dashboard(dashboard).await
1078    }
1079
1080    /// Create security monitoring dashboard
1081    pub async fn create_security_dashboard(&self) -> Result<String> {
1082        let dashboard = Dashboard {
1083            id: uuid::Uuid::new_v4().to_string(),
1084            name: "Security Monitoring".to_string(),
1085            description: "Real-time security alerts and metrics".to_string(),
1086            layout: DashboardLayout::TwoColumn,
1087            widgets: vec![
1088                Widget {
1089                    id: uuid::Uuid::new_v4().to_string(),
1090                    widget_type: WidgetType::Alert,
1091                    title: "Security Alerts".to_string(),
1092                    position: WidgetPosition { row: 0, col: 0 },
1093                    size: WidgetSize {
1094                        width: 1,
1095                        height: 1,
1096                    },
1097                    data_source: DataSource::SecurityAlerts,
1098                    visualization_type: VisualizationType::Table,
1099                    config: WidgetConfig::default(),
1100                },
1101                Widget {
1102                    id: uuid::Uuid::new_v4().to_string(),
1103                    widget_type: WidgetType::Chart,
1104                    title: "Threat Level".to_string(),
1105                    position: WidgetPosition { row: 0, col: 1 },
1106                    size: WidgetSize {
1107                        width: 1,
1108                        height: 1,
1109                    },
1110                    data_source: DataSource::SecurityAlerts,
1111                    visualization_type: VisualizationType::Gauge,
1112                    config: WidgetConfig::default(),
1113                },
1114            ],
1115            created_at: Utc::now(),
1116            last_updated: Utc::now(),
1117            auto_refresh: true,
1118            refresh_interval: Duration::from_secs(10),
1119        };
1120
1121        self.create_dashboard(dashboard).await
1122    }
1123}
1124
1125#[cfg(test)]
1126mod tests {
1127    use super::*;
1128
1129    #[tokio::test]
1130    async fn test_visualization_creation() {
1131        let config = VisualizationConfig::default();
1132        let viz = AdvancedVisualization::new(config);
1133
1134        let dashboard_id = viz
1135            .create_default_performance_dashboard()
1136            .await
1137            .expect("async operation should succeed");
1138        assert!(!dashboard_id.is_empty());
1139
1140        let dashboard = viz.get_dashboard(&dashboard_id).await;
1141        assert!(dashboard.is_some());
1142    }
1143
1144    #[tokio::test]
1145    async fn test_metrics_collection() {
1146        let collector = MetricsCollector::new();
1147
1148        collector
1149            .record_metric("test_metric", 100.0)
1150            .await
1151            .expect("async operation should succeed");
1152        collector
1153            .record_metric("test_metric", 200.0)
1154            .await
1155            .expect("async operation should succeed");
1156
1157        let series = collector.get_time_series("test_metric").await;
1158        assert!(series.is_some());
1159        assert_eq!(
1160            series.expect("operation should succeed").data_points.len(),
1161            2
1162        );
1163    }
1164
1165    #[tokio::test]
1166    async fn test_aggregation() {
1167        let collector = MetricsCollector::new();
1168
1169        for i in 1..=10 {
1170            collector
1171                .record_metric("test", i as f64)
1172                .await
1173                .expect("async operation should succeed");
1174        }
1175
1176        let avg = collector
1177            .calculate_aggregation("test", AggregationType::Average, Duration::from_secs(3600))
1178            .await
1179            .expect("operation should succeed");
1180
1181        assert!((avg - 5.5).abs() < 0.1);
1182    }
1183
1184    #[tokio::test]
1185    async fn test_chart_generation() {
1186        let config = WidgetConfig::default();
1187        let generator = ChartGenerator::new(ChartTheme::Dark);
1188
1189        let time_series = TimeSeries {
1190            name: "test".to_string(),
1191            data_points: (1..=5)
1192                .map(|i| DataPoint {
1193                    timestamp: Utc::now(),
1194                    value: i as f64,
1195                })
1196                .collect(),
1197            unit: "ms".to_string(),
1198        };
1199
1200        let chart = generator
1201            .generate_line_chart(&time_series, &config)
1202            .await
1203            .expect("operation should succeed");
1204
1205        assert_eq!(chart.series.len(), 1);
1206        assert_eq!(chart.series[0].data.len(), 5);
1207    }
1208
1209    #[tokio::test]
1210    async fn test_topology_visualization() {
1211        let visualizer = TopologyVisualizer::new();
1212
1213        visualizer
1214            .add_node(TopologyNode {
1215                id: "node1".to_string(),
1216                label: "Service 1".to_string(),
1217                node_type: NodeType::Service,
1218                status: NodeStatus::Healthy,
1219                metadata: HashMap::new(),
1220            })
1221            .await
1222            .expect("operation should succeed");
1223
1224        visualizer
1225            .add_node(TopologyNode {
1226                id: "node2".to_string(),
1227                label: "Service 2".to_string(),
1228                node_type: NodeType::Service,
1229                status: NodeStatus::Healthy,
1230                metadata: HashMap::new(),
1231            })
1232            .await
1233            .expect("operation should succeed");
1234
1235        visualizer
1236            .add_edge(TopologyEdge {
1237                source_id: "node1".to_string(),
1238                target_id: "node2".to_string(),
1239                edge_type: EdgeType::Query,
1240                weight: 1.0,
1241                label: None,
1242            })
1243            .await
1244            .expect("operation should succeed");
1245
1246        let topology = visualizer
1247            .generate_topology()
1248            .await
1249            .expect("async operation should succeed");
1250        assert_eq!(topology.nodes.len(), 2);
1251        assert_eq!(topology.edges.len(), 1);
1252    }
1253
1254    #[tokio::test]
1255    async fn test_alert_visualization() {
1256        let visualizer = AlertVisualizer::new();
1257
1258        visualizer
1259            .add_alert(Alert {
1260                id: uuid::Uuid::new_v4().to_string(),
1261                timestamp: Utc::now(),
1262                severity: AlertSeverity::Critical,
1263                title: "Test Alert".to_string(),
1264                description: "Test description".to_string(),
1265                source: "test".to_string(),
1266                acknowledged: false,
1267            })
1268            .await
1269            .expect("operation should succeed");
1270
1271        let timeline = visualizer
1272            .generate_alert_timeline()
1273            .await
1274            .expect("async operation should succeed");
1275        assert_eq!(timeline.alerts.len(), 1);
1276    }
1277
1278    #[tokio::test]
1279    async fn test_dashboard_export() {
1280        let config = VisualizationConfig::default();
1281        let viz = AdvancedVisualization::new(config);
1282
1283        let dashboard_id = viz
1284            .create_default_performance_dashboard()
1285            .await
1286            .expect("async operation should succeed");
1287
1288        let json_export = viz
1289            .export_dashboard(&dashboard_id, ExportFormat::JSON)
1290            .await
1291            .expect("operation should succeed");
1292        assert!(!json_export.is_empty());
1293
1294        let svg_export = viz
1295            .export_dashboard(&dashboard_id, ExportFormat::SVG)
1296            .await
1297            .expect("operation should succeed");
1298        assert!(!svg_export.is_empty());
1299    }
1300
1301    #[tokio::test]
1302    async fn test_export_dashboard_csv() {
1303        let config = VisualizationConfig::default();
1304        let viz = AdvancedVisualization::new(config);
1305
1306        let dashboard_id = viz
1307            .create_default_performance_dashboard()
1308            .await
1309            .expect("async operation should succeed");
1310
1311        let csv_bytes = viz
1312            .export_dashboard(&dashboard_id, ExportFormat::CSV)
1313            .await
1314            .expect("CSV export should succeed");
1315
1316        let csv = String::from_utf8(csv_bytes).expect("CSV must be valid UTF-8");
1317        assert!(
1318            csv.starts_with("id,title,widget_type,data_source\n"),
1319            "must have header row"
1320        );
1321        assert!(csv.contains("Query Latency"), "must contain widget title");
1322    }
1323
1324    #[tokio::test]
1325    async fn test_export_dashboard_png_returns_error() {
1326        let config = VisualizationConfig::default();
1327        let viz = AdvancedVisualization::new(config);
1328
1329        let dashboard_id = viz
1330            .create_default_performance_dashboard()
1331            .await
1332            .expect("async operation should succeed");
1333
1334        let result = viz.export_dashboard(&dashboard_id, ExportFormat::PNG).await;
1335
1336        assert!(
1337            result.is_err(),
1338            "PNG export should return an error per policy"
1339        );
1340        let msg = result.unwrap_err().to_string();
1341        assert!(msg.contains("PNG"), "Error message must mention PNG: {msg}");
1342    }
1343
1344    #[tokio::test]
1345    async fn test_widget_chart_custom_data_source() {
1346        let config = VisualizationConfig::default();
1347        let viz = AdvancedVisualization::new(config);
1348
1349        // Record data under a custom key
1350        viz.record_metric("my_custom_metric", 42.0)
1351            .await
1352            .expect("record_metric should succeed");
1353
1354        let widget = Widget {
1355            id: uuid::Uuid::new_v4().to_string(),
1356            widget_type: WidgetType::Chart,
1357            title: "Custom".to_string(),
1358            position: WidgetPosition { row: 0, col: 0 },
1359            size: WidgetSize {
1360                width: 1,
1361                height: 1,
1362            },
1363            data_source: DataSource::Custom("my_custom_metric".to_string()),
1364            visualization_type: VisualizationType::LineChart,
1365            config: WidgetConfig::default(),
1366        };
1367
1368        let chart = viz.generate_widget_chart(&widget).await;
1369        assert!(
1370            chart.is_ok(),
1371            "Custom data source with recorded data should succeed"
1372        );
1373    }
1374
1375    #[tokio::test]
1376    async fn test_widget_chart_security_alerts_no_data() {
1377        let config = VisualizationConfig::default();
1378        let viz = AdvancedVisualization::new(config);
1379
1380        let widget = Widget {
1381            id: uuid::Uuid::new_v4().to_string(),
1382            widget_type: WidgetType::Alert,
1383            title: "Alerts".to_string(),
1384            position: WidgetPosition { row: 0, col: 0 },
1385            size: WidgetSize {
1386                width: 1,
1387                height: 1,
1388            },
1389            data_source: DataSource::SecurityAlerts,
1390            visualization_type: VisualizationType::Table,
1391            config: WidgetConfig::default(),
1392        };
1393
1394        // No data recorded — should return descriptive error, not a panic
1395        let result = viz.generate_widget_chart(&widget).await;
1396        assert!(
1397            result.is_err(),
1398            "Missing data should return an error, not panic"
1399        );
1400        let msg = result.unwrap_err().to_string();
1401        assert!(
1402            msg.contains("security_alerts"),
1403            "Error must mention the metric key: {msg}"
1404        );
1405    }
1406}