Skip to main content

torsh_distributed/
visualization.rs

1//! Visualization tools for distributed training monitoring
2//!
3//! This module provides comprehensive visualization capabilities for distributed training
4//! including real-time dashboards, performance charts, communication graphs, and bottleneck analysis.
5
6use crate::metrics::get_global_metrics_collector;
7use crate::profiling::get_global_profiler;
8use crate::{TorshDistributedError, TorshResult};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::time::UNIX_EPOCH;
12
13/// Chart types supported by the visualization system
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15pub enum ChartType {
16    /// Line chart for time series data
17    Line,
18    /// Bar chart for categorical data
19    Bar,
20    /// Pie chart for proportional data
21    Pie,
22    /// Scatter plot for correlation analysis
23    Scatter,
24    /// Heat map for matrix data
25    Heatmap,
26    /// Network graph for communication patterns
27    Network,
28}
29
30/// Color schemes for visualizations
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32pub enum ColorScheme {
33    /// Default blue theme
34    Default,
35    /// Dark theme
36    Dark,
37    /// High contrast theme
38    HighContrast,
39    /// Performance-oriented colors (green/yellow/red)
40    Performance,
41    /// Categorical colors
42    Categorical,
43}
44
45impl ColorScheme {
46    /// Get primary colors for the scheme
47    pub fn colors(&self) -> Vec<&'static str> {
48        match self {
49            ColorScheme::Default => vec!["#3498db", "#2ecc71", "#e74c3c", "#f39c12", "#9b59b6"],
50            ColorScheme::Dark => vec!["#34495e", "#2c3e50", "#e67e22", "#e74c3c", "#95a5a6"],
51            ColorScheme::HighContrast => {
52                vec!["#000000", "#ffffff", "#ff0000", "#00ff00", "#0000ff"]
53            }
54            ColorScheme::Performance => vec!["#27ae60", "#f1c40f", "#e67e22", "#e74c3c", "#c0392b"],
55            ColorScheme::Categorical => vec!["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd"],
56        }
57    }
58
59    /// Get background color for the scheme
60    pub fn background_color(&self) -> &'static str {
61        match self {
62            ColorScheme::Default | ColorScheme::Performance | ColorScheme::Categorical => "#ffffff",
63            ColorScheme::Dark => "#2c3e50",
64            ColorScheme::HighContrast => "#ffffff",
65        }
66    }
67
68    /// Get text color for the scheme
69    pub fn text_color(&self) -> &'static str {
70        match self {
71            ColorScheme::Default
72            | ColorScheme::Performance
73            | ColorScheme::Categorical
74            | ColorScheme::HighContrast => "#333333",
75            ColorScheme::Dark => "#ecf0f1",
76        }
77    }
78}
79
80/// Configuration for visualization generation
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct VisualizationConfig {
83    /// Width of generated charts
84    pub chart_width: u32,
85    /// Height of generated charts
86    pub chart_height: u32,
87    /// Color scheme to use
88    pub color_scheme: ColorScheme,
89    /// Whether to include interactive features
90    pub interactive: bool,
91    /// Maximum number of data points to show
92    pub max_data_points: usize,
93    /// Update interval for real-time charts (seconds)
94    pub update_interval_secs: u64,
95}
96
97impl Default for VisualizationConfig {
98    fn default() -> Self {
99        Self {
100            chart_width: 800,
101            chart_height: 400,
102            color_scheme: ColorScheme::Default,
103            interactive: true,
104            max_data_points: 100,
105            update_interval_secs: 5,
106        }
107    }
108}
109
110/// Data point for visualization
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct DataPoint {
113    /// X-axis value (usually timestamp)
114    pub x: f64,
115    /// Y-axis value
116    pub y: f64,
117    /// Optional label
118    pub label: Option<String>,
119    /// Optional metadata
120    pub metadata: HashMap<String, String>,
121}
122
123impl DataPoint {
124    pub fn new(x: f64, y: f64) -> Self {
125        Self {
126            x,
127            y,
128            label: None,
129            metadata: HashMap::new(),
130        }
131    }
132
133    pub fn with_label(mut self, label: String) -> Self {
134        self.label = Some(label);
135        self
136    }
137
138    pub fn with_metadata(mut self, key: String, value: String) -> Self {
139        self.metadata.insert(key, value);
140        self
141    }
142}
143
144/// Chart data series
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct ChartSeries {
147    /// Series name
148    pub name: String,
149    /// Data points
150    pub data: Vec<DataPoint>,
151    /// Series color
152    pub color: String,
153    /// Series type (overrides chart type if specified)
154    pub chart_type: Option<ChartType>,
155}
156
157impl ChartSeries {
158    pub fn new(name: String, color: String) -> Self {
159        Self {
160            name,
161            data: Vec::new(),
162            color,
163            chart_type: None,
164        }
165    }
166
167    pub fn add_point(&mut self, point: DataPoint) {
168        self.data.push(point);
169    }
170
171    pub fn with_type(mut self, chart_type: ChartType) -> Self {
172        self.chart_type = Some(chart_type);
173        self
174    }
175}
176
177/// Complete chart specification
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct Chart {
180    /// Chart title
181    pub title: String,
182    /// Chart type
183    pub chart_type: ChartType,
184    /// X-axis label
185    pub x_label: String,
186    /// Y-axis label
187    pub y_label: String,
188    /// Data series
189    pub series: Vec<ChartSeries>,
190    /// Configuration
191    pub config: VisualizationConfig,
192}
193
194impl Chart {
195    pub fn new(title: String, chart_type: ChartType) -> Self {
196        Self {
197            title,
198            chart_type,
199            x_label: "X".to_string(),
200            y_label: "Y".to_string(),
201            series: Vec::new(),
202            config: VisualizationConfig::default(),
203        }
204    }
205
206    pub fn with_labels(mut self, x_label: String, y_label: String) -> Self {
207        self.x_label = x_label;
208        self.y_label = y_label;
209        self
210    }
211
212    pub fn add_series(&mut self, series: ChartSeries) {
213        self.series.push(series);
214    }
215
216    pub fn with_config(mut self, config: VisualizationConfig) -> Self {
217        self.config = config;
218        self
219    }
220}
221
222/// Dashboard layout specification
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct Dashboard {
225    /// Dashboard title
226    pub title: String,
227    /// Charts in the dashboard
228    pub charts: Vec<Chart>,
229    /// Layout configuration
230    pub layout: DashboardLayout,
231    /// Global configuration
232    pub config: VisualizationConfig,
233}
234
235/// Dashboard layout options
236#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct DashboardLayout {
238    /// Number of columns
239    pub columns: u32,
240    /// Chart spacing in pixels
241    pub spacing: u32,
242    /// Whether to use responsive layout
243    pub responsive: bool,
244}
245
246impl Default for DashboardLayout {
247    fn default() -> Self {
248        Self {
249            columns: 2,
250            spacing: 20,
251            responsive: true,
252        }
253    }
254}
255
256impl Dashboard {
257    pub fn new(title: String) -> Self {
258        Self {
259            title,
260            charts: Vec::new(),
261            layout: DashboardLayout::default(),
262            config: VisualizationConfig::default(),
263        }
264    }
265
266    pub fn add_chart(&mut self, chart: Chart) {
267        self.charts.push(chart);
268    }
269
270    pub fn with_layout(mut self, layout: DashboardLayout) -> Self {
271        self.layout = layout;
272        self
273    }
274
275    pub fn with_config(mut self, config: VisualizationConfig) -> Self {
276        self.config = config;
277        self
278    }
279}
280
281/// Visualization generator for distributed training data
282pub struct VisualizationGenerator {
283    /// Configuration
284    config: VisualizationConfig,
285}
286
287impl VisualizationGenerator {
288    /// Create a new visualization generator
289    pub fn new() -> Self {
290        Self::with_config(VisualizationConfig::default())
291    }
292
293    /// Create a new visualization generator with custom configuration
294    pub fn with_config(config: VisualizationConfig) -> Self {
295        Self { config }
296    }
297
298    /// Generate performance metrics dashboard
299    pub fn generate_performance_dashboard(&self) -> TorshResult<Dashboard> {
300        let mut dashboard = Dashboard::new("Distributed Training Performance".to_string())
301            .with_config(self.config.clone());
302
303        // System metrics chart
304        if let Ok(system_chart) = self.create_system_metrics_chart() {
305            dashboard.add_chart(system_chart);
306        }
307
308        // Communication metrics chart
309        if let Ok(comm_chart) = self.create_communication_metrics_chart() {
310            dashboard.add_chart(comm_chart);
311        }
312
313        // Training progress chart
314        if let Ok(training_chart) = self.create_training_progress_chart() {
315            dashboard.add_chart(training_chart);
316        }
317
318        // Bottleneck analysis chart
319        if let Ok(bottleneck_chart) = self.create_bottleneck_chart() {
320            dashboard.add_chart(bottleneck_chart);
321        }
322
323        Ok(dashboard)
324    }
325
326    /// Create system metrics chart (CPU, memory, GPU usage)
327    fn create_system_metrics_chart(&self) -> TorshResult<Chart> {
328        let metrics_collector = get_global_metrics_collector();
329        let system_history = metrics_collector.get_system_history()?;
330
331        let mut chart = Chart::new("System Resource Usage".to_string(), ChartType::Line)
332            .with_labels("Time".to_string(), "Usage (%)".to_string())
333            .with_config(self.config.clone());
334
335        let colors = self.config.color_scheme.colors();
336
337        // CPU usage series
338        let mut cpu_series = ChartSeries::new("CPU Usage".to_string(), colors[0].to_string());
339        for point in &system_history {
340            let timestamp = point
341                .timestamp
342                .duration_since(UNIX_EPOCH)
343                .unwrap_or_default()
344                .as_secs_f64();
345            cpu_series.add_point(DataPoint::new(timestamp, point.value.cpu_usage_pct));
346        }
347        chart.add_series(cpu_series);
348
349        // Memory usage series
350        let mut memory_series = ChartSeries::new("Memory Usage".to_string(), colors[1].to_string());
351        for point in &system_history {
352            let timestamp = point
353                .timestamp
354                .duration_since(UNIX_EPOCH)
355                .unwrap_or_default()
356                .as_secs_f64();
357            memory_series.add_point(DataPoint::new(timestamp, point.value.memory_usage_pct));
358        }
359        chart.add_series(memory_series);
360
361        // GPU usage series (if available)
362        if system_history
363            .iter()
364            .any(|p| p.value.gpu_usage_pct.is_some())
365        {
366            let mut gpu_series = ChartSeries::new("GPU Usage".to_string(), colors[2].to_string());
367            for point in &system_history {
368                if let Some(gpu_usage) = point.value.gpu_usage_pct {
369                    let timestamp = point
370                        .timestamp
371                        .duration_since(UNIX_EPOCH)
372                        .unwrap_or_default()
373                        .as_secs_f64();
374                    gpu_series.add_point(DataPoint::new(timestamp, gpu_usage));
375                }
376            }
377            chart.add_series(gpu_series);
378        }
379
380        Ok(chart)
381    }
382
383    /// Create communication metrics chart
384    fn create_communication_metrics_chart(&self) -> TorshResult<Chart> {
385        let metrics_collector = get_global_metrics_collector();
386        let comm_history = metrics_collector.get_communication_history()?;
387
388        let mut chart = Chart::new("Communication Performance".to_string(), ChartType::Line)
389            .with_labels("Time".to_string(), "Value".to_string())
390            .with_config(self.config.clone());
391
392        let colors = self.config.color_scheme.colors();
393
394        // Latency series
395        let mut latency_series =
396            ChartSeries::new("Avg Latency (ms)".to_string(), colors[0].to_string());
397        for point in &comm_history {
398            let timestamp = point
399                .timestamp
400                .duration_since(UNIX_EPOCH)
401                .unwrap_or_default()
402                .as_secs_f64();
403            latency_series.add_point(DataPoint::new(timestamp, point.value.avg_latency_ms));
404        }
405        chart.add_series(latency_series);
406
407        // Bandwidth series
408        let mut bandwidth_series =
409            ChartSeries::new("Avg Bandwidth (MB/s)".to_string(), colors[1].to_string());
410        for point in &comm_history {
411            let timestamp = point
412                .timestamp
413                .duration_since(UNIX_EPOCH)
414                .unwrap_or_default()
415                .as_secs_f64();
416            bandwidth_series.add_point(DataPoint::new(timestamp, point.value.avg_bandwidth_mbps));
417        }
418        chart.add_series(bandwidth_series);
419
420        // Operations per second series
421        let mut ops_series = ChartSeries::new("Operations/sec".to_string(), colors[2].to_string());
422        for point in &comm_history {
423            let timestamp = point
424                .timestamp
425                .duration_since(UNIX_EPOCH)
426                .unwrap_or_default()
427                .as_secs_f64();
428            ops_series.add_point(DataPoint::new(timestamp, point.value.ops_per_second));
429        }
430        chart.add_series(ops_series);
431
432        Ok(chart)
433    }
434
435    /// Create training progress chart
436    fn create_training_progress_chart(&self) -> TorshResult<Chart> {
437        let metrics_collector = get_global_metrics_collector();
438        let training_history = metrics_collector.get_training_history()?;
439
440        let mut chart = Chart::new("Training Progress".to_string(), ChartType::Line)
441            .with_labels("Step".to_string(), "Loss".to_string())
442            .with_config(self.config.clone());
443
444        let colors = self.config.color_scheme.colors();
445
446        // Training loss series
447        let mut train_loss_series =
448            ChartSeries::new("Training Loss".to_string(), colors[0].to_string());
449        for point in &training_history {
450            if let Some(loss) = point.value.training_loss {
451                train_loss_series.add_point(DataPoint::new(point.value.current_step as f64, loss));
452            }
453        }
454        if !train_loss_series.data.is_empty() {
455            chart.add_series(train_loss_series);
456        }
457
458        // Validation loss series
459        let mut val_loss_series =
460            ChartSeries::new("Validation Loss".to_string(), colors[1].to_string());
461        for point in &training_history {
462            if let Some(loss) = point.value.validation_loss {
463                val_loss_series.add_point(DataPoint::new(point.value.current_step as f64, loss));
464            }
465        }
466        if !val_loss_series.data.is_empty() {
467            chart.add_series(val_loss_series);
468        }
469
470        // If no loss data, show samples per second
471        if chart.series.is_empty() {
472            let mut throughput_series =
473                ChartSeries::new("Samples/sec".to_string(), colors[2].to_string());
474            for point in &training_history {
475                if point.value.samples_per_second > 0.0 {
476                    let timestamp = point
477                        .timestamp
478                        .duration_since(UNIX_EPOCH)
479                        .unwrap_or_default()
480                        .as_secs_f64();
481                    throughput_series
482                        .add_point(DataPoint::new(timestamp, point.value.samples_per_second));
483                }
484            }
485            if !throughput_series.data.is_empty() {
486                chart.y_label = "Samples/sec".to_string();
487                chart.x_label = "Time".to_string();
488                chart.add_series(throughput_series);
489            }
490        }
491
492        Ok(chart)
493    }
494
495    /// Create bottleneck analysis chart
496    fn create_bottleneck_chart(&self) -> TorshResult<Chart> {
497        crate::bottleneck_detection::with_global_bottleneck_detector(|detector| {
498            let history = detector.get_bottleneck_history();
499
500            let mut chart = Chart::new("Bottleneck Analysis".to_string(), ChartType::Bar)
501                .with_labels("Bottleneck Type".to_string(), "Count".to_string())
502                .with_config(self.config.clone());
503
504            // Count bottlenecks by type
505            let mut bottleneck_counts: HashMap<String, u32> = HashMap::new();
506            for bottleneck in history {
507                *bottleneck_counts
508                    .entry(bottleneck.bottleneck_type.to_string())
509                    .or_insert(0) += 1;
510            }
511
512            if !bottleneck_counts.is_empty() {
513                let colors = self.config.color_scheme.colors();
514                let mut series =
515                    ChartSeries::new("Bottleneck Count".to_string(), colors[0].to_string());
516
517                for (i, (bottleneck_type, count)) in bottleneck_counts.iter().enumerate() {
518                    series.add_point(
519                        DataPoint::new(i as f64, *count as f64).with_label(bottleneck_type.clone()),
520                    );
521                }
522
523                chart.add_series(series);
524            }
525
526            Ok(chart)
527        })
528    }
529
530    /// Create communication pattern network graph
531    pub fn create_communication_network_graph(&self) -> TorshResult<Chart> {
532        let profiler = get_global_profiler();
533        let events = profiler.get_all_events()?;
534
535        let mut chart = Chart::new("Communication Network".to_string(), ChartType::Network)
536            .with_labels("Rank".to_string(), "Communication Volume".to_string())
537            .with_config(self.config.clone());
538
539        // Analyze communication patterns between ranks
540        let mut rank_comm: HashMap<(u32, u32), f64> = HashMap::new();
541        let mut all_ranks: std::collections::HashSet<u32> = std::collections::HashSet::new();
542
543        for event in &events {
544            all_ranks.insert(event.rank);
545            // For simplicity, create self-communication entries
546            let key = (event.rank, event.rank);
547            *rank_comm.entry(key).or_insert(0.0) += event.data_size_bytes as f64;
548        }
549
550        // Create data points for the network graph
551        let colors = self.config.color_scheme.colors();
552        let mut series =
553            ChartSeries::new("Communication Volume".to_string(), colors[0].to_string());
554
555        for ((src, dst), volume) in rank_comm.iter() {
556            series.add_point(
557                DataPoint::new(*src as f64, *dst as f64)
558                    .with_metadata("volume".to_string(), volume.to_string())
559                    .with_metadata("src_rank".to_string(), src.to_string())
560                    .with_metadata("dst_rank".to_string(), dst.to_string()),
561            );
562        }
563
564        chart.add_series(series);
565        Ok(chart)
566    }
567
568    /// Generate SVG chart
569    pub fn generate_svg_chart(&self, chart: &Chart) -> TorshResult<String> {
570        let mut svg = String::new();
571
572        // SVG header
573        svg.push_str(&format!(
574            r#"<svg width="{}" height="{}" xmlns="http://www.w3.org/2000/svg">
575            <rect width="100%" height="100%" fill="{}"/>
576            "#,
577            chart.config.chart_width,
578            chart.config.chart_height,
579            chart.config.color_scheme.background_color()
580        ));
581
582        // Title
583        svg.push_str(&format!(
584            r#"<text x="{}" y="30" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" fill="{}">{}</text>"#,
585            chart.config.chart_width / 2,
586            chart.config.color_scheme.text_color(),
587            chart.title
588        ));
589
590        match chart.chart_type {
591            ChartType::Line => self.generate_line_chart_svg(&mut svg, chart)?,
592            ChartType::Bar => self.generate_bar_chart_svg(&mut svg, chart)?,
593            ChartType::Pie => self.generate_pie_chart_svg(&mut svg, chart)?,
594            _ => {} // Other chart types not implemented in SVG
595        }
596
597        svg.push_str("</svg>");
598        Ok(svg)
599    }
600
601    /// Generate line chart SVG
602    fn generate_line_chart_svg(&self, svg: &mut String, chart: &Chart) -> TorshResult<()> {
603        let margin = 60;
604        let chart_width = chart.config.chart_width - 2 * margin;
605        let chart_height = chart.config.chart_height - 2 * margin - 40; // 40 for title
606
607        for (series_idx, series) in chart.series.iter().enumerate() {
608            if series.data.is_empty() {
609                continue;
610            }
611
612            // Find data ranges
613            let x_min = series
614                .data
615                .iter()
616                .map(|p| p.x)
617                .fold(f64::INFINITY, f64::min);
618            let x_max = series
619                .data
620                .iter()
621                .map(|p| p.x)
622                .fold(f64::NEG_INFINITY, f64::max);
623            let y_min = series
624                .data
625                .iter()
626                .map(|p| p.y)
627                .fold(f64::INFINITY, f64::min);
628            let y_max = series
629                .data
630                .iter()
631                .map(|p| p.y)
632                .fold(f64::NEG_INFINITY, f64::max);
633
634            let x_range = if x_max > x_min { x_max - x_min } else { 1.0 };
635            let y_range = if y_max > y_min { y_max - y_min } else { 1.0 };
636
637            // Generate path data
638            let mut path_data = String::new();
639            for (i, point) in series.data.iter().enumerate() {
640                let x = margin as f64 + ((point.x - x_min) / x_range) * chart_width as f64;
641                let y = (margin + 40) as f64 + chart_height as f64
642                    - ((point.y - y_min) / y_range) * chart_height as f64;
643
644                if i == 0 {
645                    path_data.push_str(&format!("M{},{}", x, y));
646                } else {
647                    path_data.push_str(&format!(" L{},{}", x, y));
648                }
649            }
650
651            // Add path
652            svg.push_str(&format!(
653                r#"<path d="{}" stroke="{}" stroke-width="2" fill="none"/>"#,
654                path_data, series.color
655            ));
656
657            // Add legend
658            let legend_y = 40 + (series_idx as u32) * 20 + 10;
659            svg.push_str(&format!(
660                r#"<rect x="{}" y="{}" width="15" height="15" fill="{}"/>
661                <text x="{}" y="{}" font-family="Arial, sans-serif" font-size="12" fill="{}">{}</text>"#,
662                chart.config.chart_width - 150,
663                legend_y,
664                series.color,
665                chart.config.chart_width - 130,
666                legend_y + 12,
667                chart.config.color_scheme.text_color(),
668                series.name
669            ));
670        }
671
672        Ok(())
673    }
674
675    /// Generate bar chart SVG
676    fn generate_bar_chart_svg(&self, svg: &mut String, chart: &Chart) -> TorshResult<()> {
677        let margin = 60;
678        let chart_width = chart.config.chart_width - 2 * margin;
679        let chart_height = chart.config.chart_height - 2 * margin - 40;
680
681        for series in &chart.series {
682            if series.data.is_empty() {
683                continue;
684            }
685
686            let max_y = series
687                .data
688                .iter()
689                .map(|p| p.y)
690                .fold(f64::NEG_INFINITY, f64::max);
691            let bar_width = chart_width as f64 / series.data.len() as f64 * 0.8;
692
693            for (i, point) in series.data.iter().enumerate() {
694                let x = margin as f64
695                    + (i as f64 + 0.1) * (chart_width as f64 / series.data.len() as f64);
696                let bar_height = if max_y > 0.0 {
697                    (point.y / max_y) * chart_height as f64
698                } else {
699                    0.0
700                };
701                let y = (margin + 40) as f64 + chart_height as f64 - bar_height;
702
703                svg.push_str(&format!(
704                    r#"<rect x="{}" y="{}" width="{}" height="{}" fill="{}"/>"#,
705                    x, y, bar_width, bar_height, series.color
706                ));
707
708                // Add label if available
709                if let Some(label) = &point.label {
710                    svg.push_str(&format!(
711                        r#"<text x="{}" y="{}" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="{}">{}</text>"#,
712                        x + bar_width / 2.0,
713                        (margin + 40 + chart_height) as f64 + 15.0,
714                        chart.config.color_scheme.text_color(),
715                        label
716                    ));
717                }
718            }
719        }
720
721        Ok(())
722    }
723
724    /// Generate pie chart SVG  
725    fn generate_pie_chart_svg(&self, svg: &mut String, chart: &Chart) -> TorshResult<()> {
726        for series in &chart.series {
727            if series.data.is_empty() {
728                continue;
729            }
730
731            let center_x = chart.config.chart_width as f64 / 2.0;
732            let center_y = (chart.config.chart_height as f64) / 2.0;
733            let radius =
734                ((chart.config.chart_width.min(chart.config.chart_height)) as f64 / 2.0) - 50.0;
735
736            let total: f64 = series.data.iter().map(|p| p.y).sum();
737            let mut current_angle: f64 = 0.0;
738
739            let colors = chart.config.color_scheme.colors();
740
741            for (i, point) in series.data.iter().enumerate() {
742                let slice_angle = (point.y / total) * 2.0 * std::f64::consts::PI;
743
744                let start_x = center_x + radius * current_angle.cos();
745                let start_y = center_y + radius * current_angle.sin();
746
747                current_angle += slice_angle;
748
749                let end_x = center_x + radius * current_angle.cos();
750                let end_y = center_y + radius * current_angle.sin();
751
752                let large_arc_flag = if slice_angle > std::f64::consts::PI {
753                    1
754                } else {
755                    0
756                };
757
758                let color = colors[i % colors.len()];
759
760                svg.push_str(&format!(
761                    r#"<path d="M{},{} L{},{} A{},{} 0 {},{} {},{} Z" fill="{}"/>"#,
762                    center_x,
763                    center_y,
764                    start_x,
765                    start_y,
766                    radius,
767                    radius,
768                    large_arc_flag,
769                    1,
770                    end_x,
771                    end_y,
772                    color
773                ));
774
775                // Add label
776                if let Some(label) = &point.label {
777                    let label_angle = current_angle - slice_angle / 2.0;
778                    let label_x = center_x + (radius + 20.0) * label_angle.cos();
779                    let label_y = center_y + (radius + 20.0) * label_angle.sin();
780
781                    svg.push_str(&format!(
782                        r#"<text x="{}" y="{}" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="{}">{}</text>"#,
783                        label_x, label_y,
784                        chart.config.color_scheme.text_color(),
785                        label
786                    ));
787                }
788            }
789        }
790
791        Ok(())
792    }
793
794    /// Generate HTML dashboard
795    pub fn generate_html_dashboard(&self, dashboard: &Dashboard) -> TorshResult<String> {
796        let mut html = String::new();
797
798        // HTML header
799        html.push_str(&format!(
800            r#"<!DOCTYPE html>
801<html lang="en">
802<head>
803    <meta charset="UTF-8">
804    <meta name="viewport" content="width=device-width, initial-scale=1.0">
805    <title>{}</title>
806    <style>
807        body {{
808            font-family: Arial, sans-serif;
809            margin: 20px;
810            background-color: {};
811            color: {};
812        }}
813        .dashboard {{
814            display: grid;
815            grid-template-columns: repeat({}, 1fr);
816            gap: {}px;
817        }}
818        .chart-container {{
819            border: 1px solid #ddd;
820            border-radius: 8px;
821            padding: 10px;
822            background-color: {};
823        }}
824        h1 {{
825            text-align: center;
826            margin-bottom: 30px;
827        }}
828        .chart {{
829            width: 100%;
830            height: auto;
831        }}
832        {}
833    </style>
834</head>
835<body>
836    <h1>{}</h1>
837    <div class="dashboard">
838"#,
839            dashboard.title,
840            dashboard.config.color_scheme.background_color(),
841            dashboard.config.color_scheme.text_color(),
842            dashboard.layout.columns,
843            dashboard.layout.spacing,
844            dashboard.config.color_scheme.background_color(),
845            if dashboard.layout.responsive {
846                "@media (max-width: 768px) { .dashboard { grid-template-columns: 1fr; } }"
847            } else {
848                ""
849            },
850            dashboard.title
851        ));
852
853        // Add charts
854        for chart in &dashboard.charts {
855            html.push_str(r#"        <div class="chart-container">"#);
856            let svg = self.generate_svg_chart(chart)?;
857            html.push_str(&format!(r#"            <div class="chart">{}</div>"#, svg));
858            html.push_str(r#"        </div>"#);
859        }
860
861        // HTML footer
862        html.push_str(
863            r#"    </div>
864</body>
865</html>"#,
866        );
867
868        Ok(html)
869    }
870
871    /// Export dashboard data as JSON
872    pub fn export_dashboard_json(&self, dashboard: &Dashboard) -> TorshResult<String> {
873        serde_json::to_string_pretty(dashboard).map_err(|e| TorshDistributedError::BackendError {
874            backend: "json".to_string(),
875            message: format!("JSON serialization failed: {}", e),
876        })
877    }
878}
879
880impl Default for VisualizationGenerator {
881    fn default() -> Self {
882        Self::new()
883    }
884}
885
886/// Generate a complete monitoring dashboard HTML file
887pub fn generate_monitoring_dashboard() -> TorshResult<String> {
888    let generator = VisualizationGenerator::new();
889    let dashboard = generator.generate_performance_dashboard()?;
890    generator.generate_html_dashboard(&dashboard)
891}
892
893/// Generate a communication network visualization
894pub fn generate_communication_network_html() -> TorshResult<String> {
895    let generator = VisualizationGenerator::new();
896    let chart = generator.create_communication_network_graph()?;
897
898    let mut dashboard = Dashboard::new("Communication Network Analysis".to_string());
899    dashboard.add_chart(chart);
900
901    generator.generate_html_dashboard(&dashboard)
902}
903
904#[cfg(test)]
905mod tests {
906    use super::*;
907
908    #[test]
909    fn test_color_scheme() {
910        let scheme = ColorScheme::Default;
911        let colors = scheme.colors();
912        assert!(!colors.is_empty());
913        assert_eq!(scheme.background_color(), "#ffffff");
914        assert_eq!(scheme.text_color(), "#333333");
915    }
916
917    #[test]
918    fn test_data_point_creation() {
919        let point = DataPoint::new(1.0, 2.0)
920            .with_label("Test".to_string())
921            .with_metadata("key".to_string(), "value".to_string());
922
923        assert_eq!(point.x, 1.0);
924        assert_eq!(point.y, 2.0);
925        assert_eq!(point.label, Some("Test".to_string()));
926        assert_eq!(point.metadata.get("key"), Some(&"value".to_string()));
927    }
928
929    #[test]
930    fn test_chart_creation() {
931        let mut chart = Chart::new("Test Chart".to_string(), ChartType::Line)
932            .with_labels("X Axis".to_string(), "Y Axis".to_string());
933
934        let mut series = ChartSeries::new("Test Series".to_string(), "#ff0000".to_string());
935        series.add_point(DataPoint::new(1.0, 10.0));
936        series.add_point(DataPoint::new(2.0, 20.0));
937
938        chart.add_series(series);
939
940        assert_eq!(chart.title, "Test Chart");
941        assert_eq!(chart.chart_type, ChartType::Line);
942        assert_eq!(chart.series.len(), 1);
943        assert_eq!(chart.series[0].data.len(), 2);
944    }
945
946    #[test]
947    fn test_dashboard_creation() {
948        let mut dashboard = Dashboard::new("Test Dashboard".to_string());
949        let chart = Chart::new("Test Chart".to_string(), ChartType::Bar);
950        dashboard.add_chart(chart);
951
952        assert_eq!(dashboard.title, "Test Dashboard");
953        assert_eq!(dashboard.charts.len(), 1);
954    }
955
956    #[test]
957    fn test_visualization_generator() {
958        let generator = VisualizationGenerator::new();
959        assert_eq!(generator.config.chart_width, 800);
960        assert_eq!(generator.config.chart_height, 400);
961    }
962
963    #[test]
964    fn test_svg_generation() {
965        let generator = VisualizationGenerator::new();
966        let mut chart = Chart::new("Test".to_string(), ChartType::Line);
967
968        let mut series = ChartSeries::new("Data".to_string(), "#0000ff".to_string());
969        series.add_point(DataPoint::new(0.0, 0.0));
970        series.add_point(DataPoint::new(1.0, 1.0));
971        chart.add_series(series);
972
973        let svg = generator.generate_svg_chart(&chart).unwrap();
974        assert!(svg.contains("<svg"));
975        assert!(svg.contains("</svg>"));
976        assert!(svg.contains("Test"));
977    }
978
979    #[test]
980    fn test_html_dashboard_generation() {
981        let generator = VisualizationGenerator::new();
982        let dashboard = Dashboard::new("Test Dashboard".to_string());
983
984        let html = generator.generate_html_dashboard(&dashboard).unwrap();
985        assert!(html.contains("<!DOCTYPE html>"));
986        assert!(html.contains("Test Dashboard"));
987        assert!(html.contains("</html>"));
988    }
989}