1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15pub enum ChartType {
16 Line,
18 Bar,
20 Pie,
22 Scatter,
24 Heatmap,
26 Network,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32pub enum ColorScheme {
33 Default,
35 Dark,
37 HighContrast,
39 Performance,
41 Categorical,
43}
44
45impl ColorScheme {
46 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct VisualizationConfig {
83 pub chart_width: u32,
85 pub chart_height: u32,
87 pub color_scheme: ColorScheme,
89 pub interactive: bool,
91 pub max_data_points: usize,
93 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#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct DataPoint {
113 pub x: f64,
115 pub y: f64,
117 pub label: Option<String>,
119 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#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct ChartSeries {
147 pub name: String,
149 pub data: Vec<DataPoint>,
151 pub color: String,
153 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#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct Chart {
180 pub title: String,
182 pub chart_type: ChartType,
184 pub x_label: String,
186 pub y_label: String,
188 pub series: Vec<ChartSeries>,
190 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#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct Dashboard {
225 pub title: String,
227 pub charts: Vec<Chart>,
229 pub layout: DashboardLayout,
231 pub config: VisualizationConfig,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct DashboardLayout {
238 pub columns: u32,
240 pub spacing: u32,
242 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
281pub struct VisualizationGenerator {
283 config: VisualizationConfig,
285}
286
287impl VisualizationGenerator {
288 pub fn new() -> Self {
290 Self::with_config(VisualizationConfig::default())
291 }
292
293 pub fn with_config(config: VisualizationConfig) -> Self {
295 Self { config }
296 }
297
298 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 if let Ok(system_chart) = self.create_system_metrics_chart() {
305 dashboard.add_chart(system_chart);
306 }
307
308 if let Ok(comm_chart) = self.create_communication_metrics_chart() {
310 dashboard.add_chart(comm_chart);
311 }
312
313 if let Ok(training_chart) = self.create_training_progress_chart() {
315 dashboard.add_chart(training_chart);
316 }
317
318 if let Ok(bottleneck_chart) = self.create_bottleneck_chart() {
320 dashboard.add_chart(bottleneck_chart);
321 }
322
323 Ok(dashboard)
324 }
325
326 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let key = (event.rank, event.rank);
547 *rank_comm.entry(key).or_insert(0.0) += event.data_size_bytes as f64;
548 }
549
550 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 pub fn generate_svg_chart(&self, chart: &Chart) -> TorshResult<String> {
570 let mut svg = String::new();
571
572 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 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 _ => {} }
596
597 svg.push_str("</svg>");
598 Ok(svg)
599 }
600
601 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; for (series_idx, series) in chart.series.iter().enumerate() {
608 if series.data.is_empty() {
609 continue;
610 }
611
612 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 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 svg.push_str(&format!(
653 r#"<path d="{}" stroke="{}" stroke-width="2" fill="none"/>"#,
654 path_data, series.color
655 ));
656
657 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 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 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 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 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 pub fn generate_html_dashboard(&self, dashboard: &Dashboard) -> TorshResult<String> {
796 let mut html = String::new();
797
798 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 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.push_str(
863 r#" </div>
864</body>
865</html>"#,
866 );
867
868 Ok(html)
869 }
870
871 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
886pub 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
893pub 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}