1#![allow(dead_code)]
12use crate::analytics::{ModelPerformanceData, ModelUsageStats, RealTimeMetrics};
13use crate::fine_tuning::TrainingHistory;
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use std::path::PathBuf;
17use std::time::{Duration, SystemTime};
18use torsh_core::error::{Result, TorshError};
19
20pub struct VisualizationEngine {
22 config: VisualizationConfig,
23 chart_renderer: ChartRenderer,
24 dashboard_generator: DashboardGenerator,
25 export_manager: ExportManager,
26}
27
28impl VisualizationEngine {
29 pub fn chart_renderer(&self) -> &ChartRenderer {
31 &self.chart_renderer
32 }
33
34 pub fn dashboard_generator(&self) -> &DashboardGenerator {
36 &self.dashboard_generator
37 }
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct VisualizationConfig {
43 pub theme: VisualizationTheme,
44 pub default_chart_size: ChartSize,
45 pub color_palette: ColorPalette,
46 pub animation_enabled: bool,
47 pub high_dpi_enabled: bool,
48 pub export_formats: Vec<ExportFormat>,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub enum VisualizationTheme {
54 Light,
55 Dark,
56 HighContrast,
57 Custom(CustomTheme),
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct CustomTheme {
63 pub background_color: String,
64 pub text_color: String,
65 pub primary_color: String,
66 pub secondary_color: String,
67 pub accent_color: String,
68 pub grid_color: String,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct ChartSize {
74 pub width: u32,
75 pub height: u32,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct ColorPalette {
81 pub primary_colors: Vec<String>,
82 pub gradient_colors: Vec<String>,
83 pub status_colors: StatusColors,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct StatusColors {
89 pub success: String,
90 pub warning: String,
91 pub error: String,
92 pub info: String,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub enum ExportFormat {
98 PNG,
99 SVG,
100 PDF,
101 HTML,
102 JSON,
103}
104
105pub struct ChartRenderer {
107 config: VisualizationConfig,
108}
109
110impl ChartRenderer {
111 pub fn config(&self) -> &VisualizationConfig {
113 &self.config
114 }
115}
116
117pub struct DashboardGenerator {
119 templates: HashMap<String, DashboardTemplate>,
120}
121
122impl DashboardGenerator {
123 pub fn templates(&self) -> &HashMap<String, DashboardTemplate> {
125 &self.templates
126 }
127}
128
129pub struct ExportManager {
131 output_directory: PathBuf,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136pub enum ChartType {
137 Line,
138 Bar,
139 Pie,
140 Scatter,
141 Heatmap,
142 Histogram,
143 BoxPlot,
144 Radar,
145 Treemap,
146 Sankey,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct ChartData {
152 pub title: String,
153 pub chart_type: ChartType,
154 pub datasets: Vec<Dataset>,
155 pub x_axis: Axis,
156 pub y_axis: Axis,
157 pub legend: Option<Legend>,
158 pub annotations: Vec<Annotation>,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct Dataset {
164 pub label: String,
165 pub data: Vec<DataPoint>,
166 pub color: Option<String>,
167 pub style: Option<LineStyle>,
168 pub fill: bool,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct DataPoint {
174 pub x: f64,
175 pub y: f64,
176 pub label: Option<String>,
177 pub metadata: HashMap<String, String>,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct Axis {
183 pub title: String,
184 pub min: Option<f64>,
185 pub max: Option<f64>,
186 pub scale: AxisScale,
187 pub format: Option<String>,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
192pub enum AxisScale {
193 Linear,
194 Logarithmic,
195 Time,
196 Category,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub enum LineStyle {
202 Solid,
203 Dashed,
204 Dotted,
205 DashDot,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct Legend {
211 pub position: LegendPosition,
212 pub columns: u32,
213 pub font_size: u32,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
218pub enum LegendPosition {
219 Top,
220 Bottom,
221 Left,
222 Right,
223 TopLeft,
224 TopRight,
225 BottomLeft,
226 BottomRight,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct Annotation {
232 pub annotation_type: AnnotationType,
233 pub x: f64,
234 pub y: f64,
235 pub text: String,
236 pub color: Option<String>,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
241pub enum AnnotationType {
242 Point,
243 Line,
244 Rectangle,
245 Circle,
246 Arrow,
247 Text,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct DashboardTemplate {
253 pub name: String,
254 pub description: String,
255 pub layout: DashboardLayout,
256 pub widgets: Vec<DashboardWidget>,
257 pub refresh_interval: Option<Duration>,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct DashboardLayout {
263 pub columns: u32,
264 pub rows: u32,
265 pub padding: u32,
266 pub margin: u32,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct DashboardWidget {
272 pub widget_type: WidgetType,
273 pub title: String,
274 pub position: WidgetPosition,
275 pub size: WidgetSize,
276 pub data_source: DataSource,
277 pub update_interval: Option<Duration>,
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize)]
282pub enum WidgetType {
283 Chart(ChartType),
284 Metric,
285 Table,
286 Text,
287 Image,
288 Gauge,
289 Progress,
290 StatusIndicator,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct WidgetPosition {
296 pub x: u32,
297 pub y: u32,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct WidgetSize {
303 pub width: u32,
304 pub height: u32,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
309pub enum DataSource {
310 Analytics(String),
311 Metrics(String),
312 Performance(String),
313 Usage(String),
314 Custom(String),
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct PerformanceVisualization {
320 pub model_id: String,
321 pub inference_time_chart: ChartData,
322 pub throughput_chart: ChartData,
323 pub resource_utilization_chart: ChartData,
324 pub bottleneck_analysis: Vec<BottleneckVisualization>,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct BottleneckVisualization {
330 pub bottleneck_type: String,
331 pub severity: String,
332 pub impact_chart: ChartData,
333 pub timeline: Vec<TimelineEvent>,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct TimelineEvent {
339 pub timestamp: SystemTime,
340 pub event_type: String,
341 pub description: String,
342 pub severity: String,
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct UsageVisualization {
348 pub usage_trends: ChartData,
349 pub popular_models: ChartData,
350 pub user_patterns: ChartData,
351 pub geographic_distribution: Option<ChartData>,
352 pub time_series_usage: ChartData,
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
357pub struct TrainingVisualization {
358 pub loss_curve: ChartData,
359 pub accuracy_curve: ChartData,
360 pub learning_rate_schedule: ChartData,
361 pub gradient_norms: ChartData,
362 pub validation_metrics: ChartData,
363}
364
365impl Default for VisualizationConfig {
366 fn default() -> Self {
367 Self {
368 theme: VisualizationTheme::Light,
369 default_chart_size: ChartSize {
370 width: 800,
371 height: 600,
372 },
373 color_palette: ColorPalette::default(),
374 animation_enabled: true,
375 high_dpi_enabled: true,
376 export_formats: vec![ExportFormat::PNG, ExportFormat::SVG, ExportFormat::HTML],
377 }
378 }
379}
380
381impl VisualizationConfig {
382 pub fn dark_theme() -> Self {
385 Self {
386 theme: VisualizationTheme::Dark,
387 color_palette: ColorPalette {
388 primary_colors: vec![
389 "#FF7979".to_string(),
390 "#74B9FF".to_string(),
391 "#00B894".to_string(),
392 "#FDCB6E".to_string(),
393 "#E17055".to_string(),
394 "#A29BFE".to_string(),
395 "#FD79A8".to_string(),
396 "#81ECEC".to_string(),
397 ],
398 gradient_colors: vec![
399 "#2C3E50".to_string(),
400 "#34495E".to_string(),
401 "#7F8C8D".to_string(),
402 "#95A5A6".to_string(),
403 ],
404 status_colors: StatusColors {
405 success: "#00B894".to_string(),
406 warning: "#FDCB6E".to_string(),
407 error: "#E74C3C".to_string(),
408 info: "#74B9FF".to_string(),
409 },
410 },
411 ..Default::default()
412 }
413 }
414
415 pub fn high_contrast() -> Self {
418 Self {
419 theme: VisualizationTheme::HighContrast,
420 color_palette: ColorPalette {
421 primary_colors: vec![
422 "#000000".to_string(),
423 "#FFFFFF".to_string(),
424 "#FF0000".to_string(),
425 "#00FF00".to_string(),
426 "#0000FF".to_string(),
427 "#FFFF00".to_string(),
428 "#FF00FF".to_string(),
429 "#00FFFF".to_string(),
430 ],
431 gradient_colors: vec!["#000000".to_string(), "#FFFFFF".to_string()],
432 status_colors: StatusColors {
433 success: "#00FF00".to_string(),
434 warning: "#FFFF00".to_string(),
435 error: "#FF0000".to_string(),
436 info: "#0000FF".to_string(),
437 },
438 },
439 ..Default::default()
440 }
441 }
442
443 pub fn print_optimized() -> Self {
446 Self {
447 animation_enabled: false,
448 high_dpi_enabled: true,
449 export_formats: vec![ExportFormat::PDF, ExportFormat::SVG, ExportFormat::PNG],
450 color_palette: ColorPalette {
451 primary_colors: vec![
452 "#2C3E50".to_string(),
453 "#E74C3C".to_string(),
454 "#3498DB".to_string(),
455 "#2ECC71".to_string(),
456 "#F39C12".to_string(),
457 "#9B59B6".to_string(),
458 "#1ABC9C".to_string(),
459 "#E67E22".to_string(),
460 ],
461 gradient_colors: vec!["#BDC3C7".to_string(), "#95A5A6".to_string()],
462 status_colors: StatusColors {
463 success: "#27AE60".to_string(),
464 warning: "#F39C12".to_string(),
465 error: "#E74C3C".to_string(),
466 info: "#3498DB".to_string(),
467 },
468 },
469 ..Default::default()
470 }
471 }
472}
473
474impl Default for ColorPalette {
475 fn default() -> Self {
476 Self {
477 primary_colors: vec![
478 "#FF6B6B".to_string(),
479 "#4ECDC4".to_string(),
480 "#45B7D1".to_string(),
481 "#96CEB4".to_string(),
482 "#FECA57".to_string(),
483 "#FF9FF3".to_string(),
484 "#54A0FF".to_string(),
485 "#5F27CD".to_string(),
486 ],
487 gradient_colors: vec![
488 "#667eea".to_string(),
489 "#764ba2".to_string(),
490 "#f093fb".to_string(),
491 "#f5576c".to_string(),
492 ],
493 status_colors: StatusColors {
494 success: "#28a745".to_string(),
495 warning: "#ffc107".to_string(),
496 error: "#dc3545".to_string(),
497 info: "#17a2b8".to_string(),
498 },
499 }
500 }
501}
502
503impl VisualizationEngine {
504 pub fn new(config: VisualizationConfig, output_dir: PathBuf) -> Result<Self> {
506 std::fs::create_dir_all(&output_dir).map_err(|e| TorshError::IoError(e.to_string()))?;
507
508 Ok(Self {
509 chart_renderer: ChartRenderer::new(config.clone()),
510 dashboard_generator: DashboardGenerator::new(),
511 export_manager: ExportManager::new(output_dir),
512 config,
513 })
514 }
515
516 pub fn create_performance_visualization(
518 &self,
519 data: &ModelPerformanceData,
520 ) -> Result<PerformanceVisualization> {
521 let inference_time_chart = self.create_inference_time_chart(data)?;
522 let throughput_chart = self.create_throughput_chart(data)?;
523 let resource_utilization_chart = self.create_resource_utilization_chart(data)?;
524 let bottleneck_analysis = self.create_bottleneck_analysis(data)?;
525
526 Ok(PerformanceVisualization {
527 model_id: data.model_id.clone(),
528 inference_time_chart,
529 throughput_chart,
530 resource_utilization_chart,
531 bottleneck_analysis,
532 })
533 }
534
535 pub fn create_usage_visualization(
537 &self,
538 stats: &[ModelUsageStats],
539 ) -> Result<UsageVisualization> {
540 let usage_trends = self.create_usage_trends_chart(stats)?;
541 let popular_models = self.create_popular_models_chart(stats)?;
542 let user_patterns = self.create_user_patterns_chart(stats)?;
543 let time_series_usage = self.create_time_series_usage_chart(stats)?;
544
545 Ok(UsageVisualization {
546 usage_trends,
547 popular_models,
548 user_patterns,
549 geographic_distribution: None, time_series_usage,
551 })
552 }
553
554 pub fn create_training_visualization(
556 &self,
557 history: &TrainingHistory,
558 ) -> Result<TrainingVisualization> {
559 let loss_curve = self.create_loss_curve_chart(history)?;
560 let accuracy_curve = self.create_accuracy_curve_chart(history)?;
561 let learning_rate_schedule = self.create_lr_schedule_chart(history)?;
562 let gradient_norms = self.create_gradient_norms_chart(history)?;
563 let validation_metrics = self.create_validation_metrics_chart(history)?;
564
565 Ok(TrainingVisualization {
566 loss_curve,
567 accuracy_curve,
568 learning_rate_schedule,
569 gradient_norms,
570 validation_metrics,
571 })
572 }
573
574 pub fn create_realtime_dashboard(
576 &self,
577 _metrics: &RealTimeMetrics,
578 ) -> Result<DashboardTemplate> {
579 let dashboard = DashboardTemplate {
580 name: "Real-time Model Hub Dashboard".to_string(),
581 description: "Live monitoring of model hub metrics".to_string(),
582 layout: DashboardLayout {
583 columns: 3,
584 rows: 2,
585 padding: 10,
586 margin: 20,
587 },
588 widgets: vec![
589 DashboardWidget {
590 widget_type: WidgetType::Metric,
591 title: "Active Models".to_string(),
592 position: WidgetPosition { x: 0, y: 0 },
593 size: WidgetSize {
594 width: 1,
595 height: 1,
596 },
597 data_source: DataSource::Metrics("active_models".to_string()),
598 update_interval: Some(Duration::from_secs(5)),
599 },
600 DashboardWidget {
601 widget_type: WidgetType::Chart(ChartType::Line),
602 title: "Requests per Second".to_string(),
603 position: WidgetPosition { x: 1, y: 0 },
604 size: WidgetSize {
605 width: 2,
606 height: 1,
607 },
608 data_source: DataSource::Metrics("rps".to_string()),
609 update_interval: Some(Duration::from_secs(1)),
610 },
611 DashboardWidget {
612 widget_type: WidgetType::Gauge,
613 title: "CPU Usage".to_string(),
614 position: WidgetPosition { x: 0, y: 1 },
615 size: WidgetSize {
616 width: 1,
617 height: 1,
618 },
619 data_source: DataSource::Metrics("cpu_usage".to_string()),
620 update_interval: Some(Duration::from_secs(2)),
621 },
622 DashboardWidget {
623 widget_type: WidgetType::Gauge,
624 title: "Memory Usage".to_string(),
625 position: WidgetPosition { x: 1, y: 1 },
626 size: WidgetSize {
627 width: 1,
628 height: 1,
629 },
630 data_source: DataSource::Metrics("memory_usage".to_string()),
631 update_interval: Some(Duration::from_secs(2)),
632 },
633 DashboardWidget {
634 widget_type: WidgetType::Chart(ChartType::Bar),
635 title: "Error Rate".to_string(),
636 position: WidgetPosition { x: 2, y: 1 },
637 size: WidgetSize {
638 width: 1,
639 height: 1,
640 },
641 data_source: DataSource::Metrics("error_rate".to_string()),
642 update_interval: Some(Duration::from_secs(10)),
643 },
644 ],
645 refresh_interval: Some(Duration::from_secs(5)),
646 };
647
648 Ok(dashboard)
649 }
650
651 pub fn export_visualization<T: Serialize>(
653 &self,
654 visualization: &T,
655 filename: &str,
656 format: ExportFormat,
657 ) -> Result<PathBuf> {
658 self.export_manager.export(visualization, filename, format)
659 }
660
661 fn create_inference_time_chart(&self, data: &ModelPerformanceData) -> Result<ChartData> {
663 let dataset = Dataset {
664 label: "Inference Time".to_string(),
665 data: data
666 .inference_times
667 .iter()
668 .enumerate()
669 .map(|(i, duration)| DataPoint {
670 x: i as f64,
671 y: duration.as_millis() as f64,
672 label: None,
673 metadata: HashMap::new(),
674 })
675 .collect(),
676 color: Some(self.config.color_palette.primary_colors[0].clone()),
677 style: Some(LineStyle::Solid),
678 fill: false,
679 };
680
681 Ok(ChartData {
682 title: "Model Inference Time".to_string(),
683 chart_type: ChartType::Line,
684 datasets: vec![dataset],
685 x_axis: Axis {
686 title: "Request Number".to_string(),
687 min: None,
688 max: None,
689 scale: AxisScale::Linear,
690 format: None,
691 },
692 y_axis: Axis {
693 title: "Time (ms)".to_string(),
694 min: Some(0.0),
695 max: None,
696 scale: AxisScale::Linear,
697 format: Some("%.1f ms".to_string()),
698 },
699 legend: Some(Legend {
700 position: LegendPosition::TopRight,
701 columns: 1,
702 font_size: 12,
703 }),
704 annotations: vec![],
705 })
706 }
707
708 fn create_throughput_chart(&self, data: &ModelPerformanceData) -> Result<ChartData> {
710 let dataset = Dataset {
711 label: "Requests per Second".to_string(),
712 data: data
713 .throughput_data
714 .iter()
715 .enumerate()
716 .map(|(i, measurement)| DataPoint {
717 x: i as f64,
718 y: measurement.requests_per_second as f64,
719 label: None,
720 metadata: HashMap::new(),
721 })
722 .collect(),
723 color: Some(self.config.color_palette.primary_colors[1].clone()),
724 style: Some(LineStyle::Solid),
725 fill: true,
726 };
727
728 Ok(ChartData {
729 title: "Model Throughput".to_string(),
730 chart_type: ChartType::Line,
731 datasets: vec![dataset],
732 x_axis: Axis {
733 title: "Time".to_string(),
734 min: None,
735 max: None,
736 scale: AxisScale::Time,
737 format: None,
738 },
739 y_axis: Axis {
740 title: "Requests/Second".to_string(),
741 min: Some(0.0),
742 max: None,
743 scale: AxisScale::Linear,
744 format: Some("%.1f RPS".to_string()),
745 },
746 legend: Some(Legend {
747 position: LegendPosition::TopLeft,
748 columns: 1,
749 font_size: 12,
750 }),
751 annotations: vec![],
752 })
753 }
754
755 fn create_resource_utilization_chart(&self, data: &ModelPerformanceData) -> Result<ChartData> {
757 let cpu_dataset = Dataset {
758 label: "CPU Usage".to_string(),
759 data: data
760 .resource_utilization
761 .cpu_usage
762 .iter()
763 .enumerate()
764 .map(|(i, usage)| DataPoint {
765 x: i as f64,
766 y: *usage as f64,
767 label: None,
768 metadata: HashMap::new(),
769 })
770 .collect(),
771 color: Some(self.config.color_palette.primary_colors[2].clone()),
772 style: Some(LineStyle::Solid),
773 fill: false,
774 };
775
776 let memory_dataset = Dataset {
777 label: "Memory Usage".to_string(),
778 data: data
779 .resource_utilization
780 .memory_usage
781 .iter()
782 .enumerate()
783 .map(|(i, usage)| DataPoint {
784 x: i as f64,
785 y: (*usage as f64) / (1024.0 * 1024.0), label: None,
787 metadata: HashMap::new(),
788 })
789 .collect(),
790 color: Some(self.config.color_palette.primary_colors[3].clone()),
791 style: Some(LineStyle::Dashed),
792 fill: false,
793 };
794
795 Ok(ChartData {
796 title: "Resource Utilization".to_string(),
797 chart_type: ChartType::Line,
798 datasets: vec![cpu_dataset, memory_dataset],
799 x_axis: Axis {
800 title: "Time".to_string(),
801 min: None,
802 max: None,
803 scale: AxisScale::Linear,
804 format: None,
805 },
806 y_axis: Axis {
807 title: "Usage".to_string(),
808 min: Some(0.0),
809 max: None,
810 scale: AxisScale::Linear,
811 format: None,
812 },
813 legend: Some(Legend {
814 position: LegendPosition::TopRight,
815 columns: 1,
816 font_size: 12,
817 }),
818 annotations: vec![],
819 })
820 }
821
822 fn create_bottleneck_analysis(
824 &self,
825 data: &ModelPerformanceData,
826 ) -> Result<Vec<BottleneckVisualization>> {
827 let mut visualizations = Vec::new();
828
829 for bottleneck in &data.bottlenecks {
830 let impact_chart = ChartData {
831 title: format!("{:?} Bottleneck Impact", bottleneck.bottleneck_type),
832 chart_type: ChartType::Bar,
833 datasets: vec![Dataset {
834 label: "Impact".to_string(),
835 data: vec![DataPoint {
836 x: 0.0,
837 y: bottleneck.impact_percentage as f64,
838 label: Some(bottleneck.description.clone()),
839 metadata: HashMap::new(),
840 }],
841 color: Some(match bottleneck.severity {
842 crate::analytics::BottleneckSeverity::Low => {
843 self.config.color_palette.status_colors.info.clone()
844 }
845 crate::analytics::BottleneckSeverity::Medium => {
846 self.config.color_palette.status_colors.warning.clone()
847 }
848 crate::analytics::BottleneckSeverity::High => {
849 self.config.color_palette.status_colors.error.clone()
850 }
851 crate::analytics::BottleneckSeverity::Critical => "#8B0000".to_string(),
852 }),
853 style: None,
854 fill: true,
855 }],
856 x_axis: Axis {
857 title: "Bottleneck".to_string(),
858 min: None,
859 max: None,
860 scale: AxisScale::Category,
861 format: None,
862 },
863 y_axis: Axis {
864 title: "Impact (%)".to_string(),
865 min: Some(0.0),
866 max: Some(100.0),
867 scale: AxisScale::Linear,
868 format: Some("%.1f%%".to_string()),
869 },
870 legend: None,
871 annotations: vec![],
872 };
873
874 visualizations.push(BottleneckVisualization {
875 bottleneck_type: format!("{:?}", bottleneck.bottleneck_type),
876 severity: format!("{:?}", bottleneck.severity),
877 impact_chart,
878 timeline: vec![], });
880 }
881
882 Ok(visualizations)
883 }
884
885 fn create_usage_trends_chart(&self, stats: &[ModelUsageStats]) -> Result<ChartData> {
887 let mut daily_totals: HashMap<String, u64> = HashMap::new();
889
890 for stat in stats {
891 for (date, count) in &stat.daily_usage {
892 *daily_totals.entry(date.clone()).or_insert(0) += count;
893 }
894 }
895
896 let mut sorted_dates: Vec<_> = daily_totals.keys().cloned().collect();
898 sorted_dates.sort();
899
900 let dataset = Dataset {
902 label: "Daily Usage".to_string(),
903 data: sorted_dates
904 .iter()
905 .enumerate()
906 .map(|(i, date)| {
907 let count = daily_totals.get(date).copied().unwrap_or(0);
908 DataPoint {
909 x: i as f64,
910 y: count as f64,
911 label: Some(date.clone()),
912 metadata: HashMap::new(),
913 }
914 })
915 .collect(),
916 color: Some("#3b82f6".to_string()), style: Some(LineStyle::Solid),
918 fill: true,
919 };
920
921 Ok(ChartData {
922 title: "Usage Trends Over Time".to_string(),
923 chart_type: ChartType::Line,
924 datasets: vec![dataset],
925 x_axis: Axis {
926 title: "Date".to_string(),
927 min: None,
928 max: None,
929 scale: AxisScale::Category,
930 format: None,
931 },
932 y_axis: Axis {
933 title: "Usage Count".to_string(),
934 min: Some(0.0),
935 max: None,
936 scale: AxisScale::Linear,
937 format: Some("d".to_string()),
938 },
939 legend: None,
940 annotations: vec![],
941 })
942 }
943
944 fn create_popular_models_chart(&self, stats: &[ModelUsageStats]) -> Result<ChartData> {
945 let mut sorted_stats: Vec<_> = stats.iter().collect();
947 sorted_stats.sort_by(|a, b| {
948 b.popularity_score
949 .partial_cmp(&a.popularity_score)
950 .expect("popularity_score should be comparable")
951 });
952 let top_models = sorted_stats.iter().take(10);
953
954 let dataset = Dataset {
955 label: "Popularity Score".to_string(),
956 data: top_models
957 .enumerate()
958 .map(|(i, stat)| DataPoint {
959 x: i as f64,
960 y: stat.popularity_score as f64,
961 label: Some(stat.model_id.clone()),
962 metadata: HashMap::new(),
963 })
964 .collect(),
965 color: Some("#8b5cf6".to_string()), style: Some(LineStyle::Solid),
967 fill: false,
968 };
969
970 Ok(ChartData {
971 title: "Popular Models".to_string(),
972 chart_type: ChartType::Bar,
973 datasets: vec![dataset],
974 x_axis: Axis {
975 title: "Model".to_string(),
976 min: None,
977 max: None,
978 scale: AxisScale::Linear,
979 format: None,
980 },
981 y_axis: Axis {
982 title: "Popularity Score".to_string(),
983 min: Some(0.0),
984 max: None,
985 scale: AxisScale::Linear,
986 format: Some(".2f".to_string()),
987 },
988 legend: None,
989 annotations: vec![],
990 })
991 }
992
993 fn create_user_patterns_chart(&self, stats: &[ModelUsageStats]) -> Result<ChartData> {
994 let mut hourly_totals = [0u64; 24];
996 for stat in stats {
997 for (hour, &count) in stat.hourly_patterns.iter().enumerate() {
998 hourly_totals[hour] += count;
999 }
1000 }
1001
1002 let dataset = Dataset {
1003 label: "Usage Count".to_string(),
1004 data: hourly_totals
1005 .iter()
1006 .enumerate()
1007 .map(|(hour, &count)| DataPoint {
1008 x: hour as f64,
1009 y: count as f64,
1010 label: Some(format!("{:02}:00", hour)),
1011 metadata: HashMap::new(),
1012 })
1013 .collect(),
1014 color: Some("#f59e0b".to_string()), style: Some(LineStyle::Solid),
1016 fill: false,
1017 };
1018
1019 Ok(ChartData {
1020 title: "User Patterns (Hourly Usage)".to_string(),
1021 chart_type: ChartType::Bar,
1022 datasets: vec![dataset],
1023 x_axis: Axis {
1024 title: "Hour of Day".to_string(),
1025 min: Some(0.0),
1026 max: Some(23.0),
1027 scale: AxisScale::Linear,
1028 format: Some("d".to_string()),
1029 },
1030 y_axis: Axis {
1031 title: "Usage Count".to_string(),
1032 min: Some(0.0),
1033 max: None,
1034 scale: AxisScale::Linear,
1035 format: Some("d".to_string()),
1036 },
1037 legend: None,
1038 annotations: vec![],
1039 })
1040 }
1041
1042 fn create_time_series_usage_chart(&self, stats: &[ModelUsageStats]) -> Result<ChartData> {
1043 let mut sorted_stats: Vec<_> = stats.iter().collect();
1045 sorted_stats.sort_by(|a, b| b.total_inferences.cmp(&a.total_inferences));
1046 let top_models = sorted_stats.iter().take(5);
1047
1048 let colors = [
1049 "#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6", ];
1055
1056 let mut datasets = vec![];
1057
1058 for (idx, stat) in top_models.enumerate() {
1059 let mut dates: Vec<_> = stat.daily_usage.keys().cloned().collect();
1061 dates.sort();
1062
1063 let data: Vec<DataPoint> = dates
1064 .iter()
1065 .enumerate()
1066 .map(|(i, date)| {
1067 let count = stat.daily_usage.get(date).copied().unwrap_or(0);
1068 DataPoint {
1069 x: i as f64,
1070 y: count as f64,
1071 label: Some(date.clone()),
1072 metadata: HashMap::new(),
1073 }
1074 })
1075 .collect();
1076
1077 if !data.is_empty() {
1078 datasets.push(Dataset {
1079 label: stat.model_id.clone(),
1080 data,
1081 color: Some(colors[idx % colors.len()].to_string()),
1082 style: Some(LineStyle::Solid),
1083 fill: false,
1084 });
1085 }
1086 }
1087
1088 Ok(ChartData {
1089 title: "Time Series Usage by Model".to_string(),
1090 chart_type: ChartType::Line,
1091 datasets,
1092 x_axis: Axis {
1093 title: "Date".to_string(),
1094 min: None,
1095 max: None,
1096 scale: AxisScale::Category,
1097 format: None,
1098 },
1099 y_axis: Axis {
1100 title: "Usage Count".to_string(),
1101 min: Some(0.0),
1102 max: None,
1103 scale: AxisScale::Linear,
1104 format: Some("d".to_string()),
1105 },
1106 legend: Some(Legend {
1107 position: LegendPosition::TopRight,
1108 font_size: 12,
1109 columns: 1,
1110 }),
1111 annotations: vec![],
1112 })
1113 }
1114
1115 fn create_loss_curve_chart(&self, history: &TrainingHistory) -> Result<ChartData> {
1116 let epochs: Vec<f64> = (1..=history.loss.len()).map(|e| e as f64).collect();
1117
1118 let mut datasets = vec![Dataset {
1119 label: "Training Loss".to_string(),
1120 data: history
1121 .loss
1122 .iter()
1123 .enumerate()
1124 .map(|(i, &loss)| DataPoint {
1125 x: epochs[i],
1126 y: loss,
1127 label: None,
1128 metadata: HashMap::new(),
1129 })
1130 .collect(),
1131 color: Some("#3b82f6".to_string()), style: Some(LineStyle::Solid),
1133 fill: false,
1134 }];
1135
1136 if !history.val_loss.is_empty() {
1138 datasets.push(Dataset {
1139 label: "Validation Loss".to_string(),
1140 data: history
1141 .val_loss
1142 .iter()
1143 .enumerate()
1144 .map(|(i, &loss)| DataPoint {
1145 x: (i + 1) as f64,
1146 y: loss,
1147 label: None,
1148 metadata: HashMap::new(),
1149 })
1150 .collect(),
1151 color: Some("#ef4444".to_string()), style: Some(LineStyle::Dashed),
1153 fill: false,
1154 });
1155 }
1156
1157 Ok(ChartData {
1158 title: "Training Loss".to_string(),
1159 chart_type: ChartType::Line,
1160 datasets,
1161 x_axis: Axis {
1162 title: "Epoch".to_string(),
1163 min: Some(1.0),
1164 max: Some(history.loss.len() as f64),
1165 scale: AxisScale::Linear,
1166 format: Some("d".to_string()),
1167 },
1168 y_axis: Axis {
1169 title: "Loss".to_string(),
1170 min: None,
1171 max: None,
1172 scale: AxisScale::Linear,
1173 format: Some(".4f".to_string()),
1174 },
1175 legend: Some(Legend {
1176 position: LegendPosition::TopRight,
1177 font_size: 12,
1178 columns: 1,
1179 }),
1180 annotations: vec![],
1181 })
1182 }
1183
1184 fn create_accuracy_curve_chart(&self, history: &TrainingHistory) -> Result<ChartData> {
1185 let mut datasets = vec![];
1187
1188 if let Some(train_accuracy) = history.metrics.get("accuracy") {
1189 let epochs: Vec<f64> = (1..=train_accuracy.len()).map(|e| e as f64).collect();
1190 datasets.push(Dataset {
1191 label: "Training Accuracy".to_string(),
1192 data: train_accuracy
1193 .iter()
1194 .enumerate()
1195 .map(|(i, &acc)| DataPoint {
1196 x: epochs[i],
1197 y: acc,
1198 label: None,
1199 metadata: HashMap::new(),
1200 })
1201 .collect(),
1202 color: Some("#3b82f6".to_string()), style: Some(LineStyle::Solid),
1204 fill: false,
1205 });
1206 }
1207
1208 if let Some(val_accuracy) = history.metrics.get("val_accuracy") {
1209 let epochs: Vec<f64> = (1..=val_accuracy.len()).map(|e| e as f64).collect();
1210 datasets.push(Dataset {
1211 label: "Validation Accuracy".to_string(),
1212 data: val_accuracy
1213 .iter()
1214 .enumerate()
1215 .map(|(i, &acc)| DataPoint {
1216 x: epochs[i],
1217 y: acc,
1218 label: None,
1219 metadata: HashMap::new(),
1220 })
1221 .collect(),
1222 color: Some("#10b981".to_string()), style: Some(LineStyle::Dashed),
1224 fill: false,
1225 });
1226 }
1227
1228 let num_epochs = datasets.first().map(|d| d.data.len()).unwrap_or(0);
1229
1230 Ok(ChartData {
1231 title: "Training Accuracy".to_string(),
1232 chart_type: ChartType::Line,
1233 datasets,
1234 x_axis: Axis {
1235 title: "Epoch".to_string(),
1236 min: Some(1.0),
1237 max: Some(num_epochs as f64),
1238 scale: AxisScale::Linear,
1239 format: Some("d".to_string()),
1240 },
1241 y_axis: Axis {
1242 title: "Accuracy".to_string(),
1243 min: Some(0.0),
1244 max: Some(1.0),
1245 scale: AxisScale::Linear,
1246 format: Some(".2%".to_string()),
1247 },
1248 legend: Some(Legend {
1249 position: LegendPosition::BottomRight,
1250 font_size: 12,
1251 columns: 1,
1252 }),
1253 annotations: vec![],
1254 })
1255 }
1256
1257 fn create_lr_schedule_chart(&self, history: &TrainingHistory) -> Result<ChartData> {
1258 let epochs: Vec<f64> = (1..=history.learning_rates.len())
1259 .map(|e| e as f64)
1260 .collect();
1261
1262 let dataset = Dataset {
1263 label: "Learning Rate".to_string(),
1264 data: history
1265 .learning_rates
1266 .iter()
1267 .enumerate()
1268 .map(|(i, &lr)| DataPoint {
1269 x: epochs[i],
1270 y: lr,
1271 label: None,
1272 metadata: HashMap::new(),
1273 })
1274 .collect(),
1275 color: Some("#10b981".to_string()), style: Some(LineStyle::Solid),
1277 fill: false,
1278 };
1279
1280 Ok(ChartData {
1281 title: "Learning Rate Schedule".to_string(),
1282 chart_type: ChartType::Line,
1283 datasets: vec![dataset],
1284 x_axis: Axis {
1285 title: "Epoch".to_string(),
1286 min: Some(1.0),
1287 max: Some(history.learning_rates.len() as f64),
1288 scale: AxisScale::Linear,
1289 format: Some("d".to_string()),
1290 },
1291 y_axis: Axis {
1292 title: "Learning Rate".to_string(),
1293 min: None,
1294 max: None,
1295 scale: AxisScale::Logarithmic,
1296 format: Some(".2e".to_string()),
1297 },
1298 legend: Some(Legend {
1299 position: LegendPosition::TopRight,
1300 font_size: 12,
1301 columns: 1,
1302 }),
1303 annotations: vec![],
1304 })
1305 }
1306
1307 fn create_gradient_norms_chart(&self, history: &TrainingHistory) -> Result<ChartData> {
1308 let mut datasets = vec![];
1309
1310 for (metric_name, values) in &history.metrics {
1312 if metric_name.to_lowercase().contains("grad")
1313 && (metric_name.to_lowercase().contains("norm")
1314 || metric_name.to_lowercase().contains("magnitude"))
1315 {
1316 let epochs: Vec<f64> = (1..=values.len()).map(|e| e as f64).collect();
1317 datasets.push(Dataset {
1318 label: metric_name.clone(),
1319 data: values
1320 .iter()
1321 .enumerate()
1322 .map(|(i, &val)| DataPoint {
1323 x: epochs[i],
1324 y: val,
1325 label: None,
1326 metadata: HashMap::new(),
1327 })
1328 .collect(),
1329 color: None, style: Some(LineStyle::Solid),
1331 fill: false,
1332 });
1333 }
1334 }
1335
1336 let num_epochs = datasets.first().map(|d| d.data.len()).unwrap_or(1);
1338
1339 Ok(ChartData {
1340 title: "Gradient Norms".to_string(),
1341 chart_type: ChartType::Line,
1342 datasets,
1343 x_axis: Axis {
1344 title: "Epoch".to_string(),
1345 min: Some(1.0),
1346 max: Some(num_epochs as f64),
1347 scale: AxisScale::Linear,
1348 format: Some("d".to_string()),
1349 },
1350 y_axis: Axis {
1351 title: "Gradient Norm".to_string(),
1352 min: None,
1353 max: None,
1354 scale: AxisScale::Logarithmic,
1355 format: Some(".2e".to_string()),
1356 },
1357 legend: Some(Legend {
1358 position: LegendPosition::TopRight,
1359 font_size: 12,
1360 columns: 1,
1361 }),
1362 annotations: vec![],
1363 })
1364 }
1365
1366 fn create_validation_metrics_chart(&self, history: &TrainingHistory) -> Result<ChartData> {
1367 let mut datasets = vec![];
1368
1369 for (metric_name, values) in &history.metrics {
1371 if metric_name.starts_with("val_") {
1372 let epochs: Vec<f64> = (1..=values.len()).map(|e| e as f64).collect();
1373 datasets.push(Dataset {
1374 label: metric_name.clone(),
1375 data: values
1376 .iter()
1377 .enumerate()
1378 .map(|(i, &val)| DataPoint {
1379 x: epochs[i],
1380 y: val,
1381 label: None,
1382 metadata: HashMap::new(),
1383 })
1384 .collect(),
1385 color: None, style: Some(LineStyle::Solid),
1387 fill: false,
1388 });
1389 }
1390 }
1391
1392 let num_epochs = datasets.first().map(|d| d.data.len()).unwrap_or(0);
1393
1394 Ok(ChartData {
1395 title: "Validation Metrics".to_string(),
1396 chart_type: ChartType::Line,
1397 datasets,
1398 x_axis: Axis {
1399 title: "Epoch".to_string(),
1400 min: Some(1.0),
1401 max: Some(num_epochs as f64),
1402 scale: AxisScale::Linear,
1403 format: Some("d".to_string()),
1404 },
1405 y_axis: Axis {
1406 title: "Metric Value".to_string(),
1407 min: None,
1408 max: None,
1409 scale: AxisScale::Linear,
1410 format: Some(".4f".to_string()),
1411 },
1412 legend: Some(Legend {
1413 position: LegendPosition::TopRight,
1414 font_size: 12,
1415 columns: 1,
1416 }),
1417 annotations: vec![],
1418 })
1419 }
1420
1421 fn create_placeholder_chart(&self, title: &str) -> ChartData {
1422 ChartData {
1423 title: title.to_string(),
1424 chart_type: ChartType::Line,
1425 datasets: vec![],
1426 x_axis: Axis {
1427 title: "X Axis".to_string(),
1428 min: None,
1429 max: None,
1430 scale: AxisScale::Linear,
1431 format: None,
1432 },
1433 y_axis: Axis {
1434 title: "Y Axis".to_string(),
1435 min: None,
1436 max: None,
1437 scale: AxisScale::Linear,
1438 format: None,
1439 },
1440 legend: None,
1441 annotations: vec![],
1442 }
1443 }
1444}
1445
1446impl ChartRenderer {
1447 fn new(config: VisualizationConfig) -> Self {
1448 Self { config }
1449 }
1450
1451 pub fn render_to_html(&self, chart: &ChartData) -> Result<String> {
1453 let html = format!(
1455 r#"
1456 <!DOCTYPE html>
1457 <html>
1458 <head>
1459 <title>{}</title>
1460 <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
1461 </head>
1462 <body>
1463 <div id="chart" style="width:100%;height:600px;"></div>
1464 <script>
1465 // Chart implementation would go here
1466 Plotly.newPlot('chart', [], {{}});
1467 </script>
1468 </body>
1469 </html>
1470 "#,
1471 chart.title
1472 );
1473 Ok(html)
1474 }
1475
1476 pub fn render_to_svg(&self, chart: &ChartData) -> Result<String> {
1478 Ok(format!("<svg><!-- {} chart --></svg>", chart.title))
1480 }
1481}
1482
1483impl DashboardGenerator {
1484 fn new() -> Self {
1485 Self {
1486 templates: HashMap::new(),
1487 }
1488 }
1489
1490 pub fn generate_dashboard_html(&self, template: &DashboardTemplate) -> Result<String> {
1492 let html = format!(
1494 r#"
1495 <!DOCTYPE html>
1496 <html>
1497 <head>
1498 <title>{}</title>
1499 <link rel="stylesheet" href="dashboard.css">
1500 <script src="dashboard.js"></script>
1501 </head>
1502 <body>
1503 <div class="dashboard">
1504 <h1>{}</h1>
1505 <div class="dashboard-grid">
1506 <!-- Widgets would be generated here -->
1507 </div>
1508 </div>
1509 </body>
1510 </html>
1511 "#,
1512 template.name, template.name
1513 );
1514 Ok(html)
1515 }
1516}
1517
1518impl ExportManager {
1519 fn new(output_directory: PathBuf) -> Self {
1520 Self { output_directory }
1521 }
1522
1523 pub fn export<T: Serialize>(
1525 &self,
1526 data: &T,
1527 filename: &str,
1528 format: ExportFormat,
1529 ) -> Result<PathBuf> {
1530 let output_path = match format {
1531 ExportFormat::JSON => {
1532 let path = self.output_directory.join(format!("{}.json", filename));
1533 let json = serde_json::to_string_pretty(data)
1534 .map_err(|e| TorshError::SerializationError(e.to_string()))?;
1535 std::fs::write(&path, json).map_err(|e| TorshError::IoError(e.to_string()))?;
1536 path
1537 }
1538 ExportFormat::HTML => {
1539 let path = self.output_directory.join(format!("{}.html", filename));
1540 std::fs::write(&path, "<!-- HTML export -->")?;
1542 path
1543 }
1544 ExportFormat::PNG | ExportFormat::SVG | ExportFormat::PDF => {
1545 let extension = match format {
1546 ExportFormat::PNG => "png",
1547 ExportFormat::SVG => "svg",
1548 ExportFormat::PDF => "pdf",
1549 _ => {
1550 return Err(TorshError::InvalidArgument(format!(
1551 "Unsupported binary export format: {:?}",
1552 format
1553 )));
1554 }
1555 };
1556 let path = self
1557 .output_directory
1558 .join(format!("{}.{}", filename, extension));
1559 std::fs::write(&path, b"binary data placeholder")?;
1561 path
1562 }
1563 };
1564
1565 Ok(output_path)
1566 }
1567}
1568
1569#[cfg(test)]
1570mod tests {
1571 use super::*;
1572
1573 #[test]
1574 fn test_visualization_config_default() {
1575 let config = VisualizationConfig::default();
1576 assert_eq!(config.default_chart_size.width, 800);
1577 assert_eq!(config.default_chart_size.height, 600);
1578 assert!(config.animation_enabled);
1579 assert!(config.high_dpi_enabled);
1580 }
1581
1582 #[test]
1583 fn test_color_palette_default() {
1584 let palette = ColorPalette::default();
1585 assert!(!palette.primary_colors.is_empty());
1586 assert_eq!(palette.status_colors.success, "#28a745");
1587 assert_eq!(palette.status_colors.error, "#dc3545");
1588 }
1589
1590 #[test]
1591 fn test_chart_data_creation() {
1592 let chart = ChartData {
1593 title: "Test Chart".to_string(),
1594 chart_type: ChartType::Line,
1595 datasets: vec![],
1596 x_axis: Axis {
1597 title: "X".to_string(),
1598 min: None,
1599 max: None,
1600 scale: AxisScale::Linear,
1601 format: None,
1602 },
1603 y_axis: Axis {
1604 title: "Y".to_string(),
1605 min: None,
1606 max: None,
1607 scale: AxisScale::Linear,
1608 format: None,
1609 },
1610 legend: None,
1611 annotations: vec![],
1612 };
1613
1614 assert_eq!(chart.title, "Test Chart");
1615 assert!(matches!(chart.chart_type, ChartType::Line));
1616 }
1617}