entrenar/monitor/
dashboard.rs1use super::MetricsSummary;
7
8#[derive(Debug, Clone)]
10pub struct DashboardConfig {
11 pub width: usize,
13 pub height: usize,
15 pub refresh_ms: u64,
17 pub ascii_mode: bool,
19}
20
21impl Default for DashboardConfig {
22 fn default() -> Self {
23 Self { width: 80, height: 24, refresh_ms: 1000, ascii_mode: true }
24 }
25}
26
27pub struct Dashboard {
29 config: DashboardConfig,
30 history: Vec<MetricsSummary>,
31 max_history: usize,
32}
33
34impl Dashboard {
35 pub fn new() -> Self {
37 Self::with_config(DashboardConfig::default())
38 }
39
40 pub fn with_config(config: DashboardConfig) -> Self {
42 Self { config, history: Vec::new(), max_history: 100 }
43 }
44
45 pub fn update(&mut self, summary: MetricsSummary) {
47 self.history.push(summary);
48 if self.history.len() > self.max_history {
49 self.history.remove(0);
50 }
51 }
52
53 pub fn render_ascii(&self) -> String {
55 let mut output = String::new();
56 output.push_str(&"═".repeat(self.config.width));
57 output.push('\n');
58 output.push_str(" TRAINING MONITOR\n");
59 output.push_str(&"─".repeat(self.config.width));
60 output.push('\n');
61
62 if let Some(latest) = self.history.last() {
63 for (metric, stats) in latest {
64 output.push_str(&format!(
65 " {:<15} mean={:.4} std={:.4} min={:.4} max={:.4}\n",
66 metric.as_str(),
67 stats.mean,
68 stats.std,
69 stats.min,
70 stats.max
71 ));
72 }
73 } else {
74 output.push_str(" No metrics recorded yet\n");
75 }
76
77 output.push_str(&"═".repeat(self.config.width));
78 output.push('\n');
79 output
80 }
81
82 pub fn sparkline(&self, metric: &super::Metric) -> String {
84 let chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
85 let values: Vec<f64> =
86 self.history.iter().filter_map(|s| s.get(metric).map(|st| st.mean)).collect();
87
88 if values.is_empty() {
89 return String::new();
90 }
91
92 let min = values.iter().copied().fold(f64::INFINITY, f64::min);
93 let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
94 let range = max - min;
95
96 if range == 0.0 {
97 return chars[4].to_string().repeat(values.len());
98 }
99
100 values
101 .iter()
102 .map(|v| {
103 let idx = (((v - min) / range) * 7.0).round() as usize;
104 chars[idx.min(7)]
105 })
106 .collect()
107 }
108}
109
110impl Default for Dashboard {
111 fn default() -> Self {
112 Self::new()
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::monitor::{Metric, MetricStats};
120 use std::collections::HashMap;
121
122 #[test]
123 fn test_dashboard_new() {
124 let dashboard = Dashboard::new();
125 assert_eq!(dashboard.history.len(), 0);
126 }
127
128 #[test]
129 fn test_dashboard_update() {
130 let mut dashboard = Dashboard::new();
131 let mut summary = HashMap::new();
132 summary.insert(
133 Metric::Loss,
134 MetricStats {
135 count: 1,
136 mean: 0.5,
137 std: 0.0,
138 min: 0.5,
139 max: 0.5,
140 sum: 0.5,
141 has_nan: false,
142 has_inf: false,
143 },
144 );
145 dashboard.update(summary);
146 assert_eq!(dashboard.history.len(), 1);
147 }
148
149 #[test]
150 fn test_render_ascii_empty() {
151 let dashboard = Dashboard::new();
152 let output = dashboard.render_ascii();
153 assert!(output.contains("No metrics"));
154 }
155
156 #[test]
157 fn test_render_ascii_with_data() {
158 let mut dashboard = Dashboard::new();
159 let mut summary = HashMap::new();
160 summary.insert(
161 Metric::Loss,
162 MetricStats {
163 count: 10,
164 mean: 0.25,
165 std: 0.1,
166 min: 0.1,
167 max: 0.5,
168 sum: 2.5,
169 has_nan: false,
170 has_inf: false,
171 },
172 );
173 dashboard.update(summary);
174 let output = dashboard.render_ascii();
175 assert!(output.contains("loss"));
176 assert!(output.contains("0.25"));
177 }
178
179 #[test]
180 fn test_sparkline() {
181 let mut dashboard = Dashboard::new();
182
183 for i in 0..10 {
185 let mut summary = HashMap::new();
186 summary.insert(
187 Metric::Loss,
188 MetricStats {
189 count: 1,
190 mean: 1.0 - (f64::from(i) * 0.1),
191 std: 0.0,
192 min: 0.0,
193 max: 1.0,
194 sum: 0.0,
195 has_nan: false,
196 has_inf: false,
197 },
198 );
199 dashboard.update(summary);
200 }
201
202 let spark = dashboard.sparkline(&Metric::Loss);
203 assert_eq!(spark.chars().count(), 10);
204 }
205
206 #[test]
207 fn test_dashboard_config_default() {
208 let config = DashboardConfig::default();
209 assert_eq!(config.width, 80);
210 assert_eq!(config.height, 24);
211 assert_eq!(config.refresh_ms, 1000);
212 assert!(config.ascii_mode);
213 }
214
215 #[test]
216 fn test_dashboard_with_config() {
217 let config = DashboardConfig { width: 100, height: 30, refresh_ms: 500, ascii_mode: false };
218 let dashboard = Dashboard::with_config(config.clone());
219 assert_eq!(dashboard.config.width, 100);
220 }
221
222 #[test]
223 fn test_sparkline_empty() {
224 let dashboard = Dashboard::new();
225 let spark = dashboard.sparkline(&Metric::Loss);
226 assert!(spark.is_empty());
227 }
228
229 #[test]
230 fn test_dashboard_default() {
231 let dashboard = Dashboard::default();
232 assert!(dashboard.history.is_empty());
233 }
234}