1use std::path::Path;
10
11use ratatui::{
12 layout::{Constraint, Direction, Layout, Rect},
13 style::Style,
14 text::{Line, Span},
15 widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Row, Table},
16 Frame,
17};
18
19use crate::theme::AxonmlTheme;
20
21#[derive(Debug, Clone)]
27pub struct ClassInfo {
28 pub name: String,
29 pub count: usize,
30 pub percentage: f32,
31}
32
33#[derive(Debug, Clone)]
35pub struct SplitInfo {
36 pub name: String,
37 pub samples: usize,
38 pub percentage: f32,
39}
40
41#[derive(Debug, Clone)]
43pub struct FeatureInfo {
44 pub name: String,
45 pub dtype: String,
46 pub shape: String,
47 pub min: Option<f32>,
48 pub max: Option<f32>,
49 pub mean: Option<f32>,
50}
51
52#[derive(Debug, Clone)]
54pub struct DatasetInfo {
55 pub name: String,
56 pub total_samples: usize,
57 pub num_classes: usize,
58 pub feature_dim: String,
59 pub classes: Vec<ClassInfo>,
60 pub splits: Vec<SplitInfo>,
61 pub features: Vec<FeatureInfo>,
62 pub file_path: String,
63 pub file_size: u64,
64}
65
66pub struct DataView {
72 pub dataset: Option<DatasetInfo>,
74
75 pub selected_class: usize,
77
78 pub list_state: ListState,
80
81 pub active_panel: usize,
83}
84
85impl DataView {
86 pub fn new() -> Self {
88 let mut view = Self {
89 dataset: None,
90 selected_class: 0,
91 list_state: ListState::default(),
92 active_panel: 0,
93 };
94
95 view.load_demo_data();
97 view.list_state.select(Some(0));
98 view
99 }
100
101 pub fn load_demo_data(&mut self) {
103 let dataset = DatasetInfo {
104 name: "MNIST".to_string(),
105 total_samples: 70_000,
106 num_classes: 10,
107 feature_dim: "[28, 28, 1]".to_string(),
108 classes: vec![
109 ClassInfo { name: "0".to_string(), count: 6903, percentage: 9.86 },
110 ClassInfo { name: "1".to_string(), count: 7877, percentage: 11.25 },
111 ClassInfo { name: "2".to_string(), count: 6990, percentage: 9.99 },
112 ClassInfo { name: "3".to_string(), count: 7141, percentage: 10.20 },
113 ClassInfo { name: "4".to_string(), count: 6824, percentage: 9.75 },
114 ClassInfo { name: "5".to_string(), count: 6313, percentage: 9.02 },
115 ClassInfo { name: "6".to_string(), count: 6876, percentage: 9.82 },
116 ClassInfo { name: "7".to_string(), count: 7293, percentage: 10.42 },
117 ClassInfo { name: "8".to_string(), count: 6825, percentage: 9.75 },
118 ClassInfo { name: "9".to_string(), count: 6958, percentage: 9.94 },
119 ],
120 splits: vec![
121 SplitInfo { name: "Train".to_string(), samples: 60_000, percentage: 85.71 },
122 SplitInfo { name: "Test".to_string(), samples: 10_000, percentage: 14.29 },
123 ],
124 features: vec![
125 FeatureInfo {
126 name: "pixels".to_string(),
127 dtype: "f32".to_string(),
128 shape: "[28, 28]".to_string(),
129 min: Some(0.0),
130 max: Some(255.0),
131 mean: Some(33.32),
132 },
133 FeatureInfo {
134 name: "label".to_string(),
135 dtype: "u8".to_string(),
136 shape: "[1]".to_string(),
137 min: Some(0.0),
138 max: Some(9.0),
139 mean: None,
140 },
141 ],
142 file_path: "/data/mnist.npz".to_string(),
143 file_size: 11_490_434,
144 };
145
146 self.dataset = Some(dataset);
147 }
148
149 pub fn select_prev(&mut self) {
151 if self.dataset.is_some() && self.selected_class > 0 {
152 self.selected_class -= 1;
153 self.list_state.select(Some(self.selected_class));
154 }
155 }
156
157 pub fn select_next(&mut self) {
159 if let Some(dataset) = &self.dataset {
160 if self.selected_class < dataset.classes.len() - 1 {
161 self.selected_class += 1;
162 self.list_state.select(Some(self.selected_class));
163 }
164 }
165 }
166
167 pub fn load_dataset(&mut self, _path: &Path) -> Result<(), String> {
169 self.load_demo_data();
172 Ok(())
173 }
174
175 pub fn switch_panel(&mut self) {
177 self.active_panel = (self.active_panel + 1) % 2;
178 }
179
180 pub fn prev_panel(&mut self) {
182 if self.active_panel > 0 {
183 self.active_panel -= 1;
184 } else {
185 self.active_panel = 1; }
187 }
188
189 pub fn next_panel(&mut self) {
191 self.active_panel = (self.active_panel + 1) % 2;
192 }
193
194 pub fn render(&mut self, frame: &mut Frame, area: Rect) {
196 if let Some(dataset) = &self.dataset.clone() {
197 let chunks = Layout::default()
198 .direction(Direction::Vertical)
199 .constraints([
200 Constraint::Length(6), Constraint::Min(10), Constraint::Length(6), ])
204 .split(area);
205
206 self.render_header(frame, chunks[0], dataset);
207 self.render_main(frame, chunks[1], dataset);
208 self.render_splits(frame, chunks[2], dataset);
209 } else {
210 self.render_empty(frame, area);
211 }
212 }
213
214 fn render_header(&self, frame: &mut Frame, area: Rect, dataset: &DatasetInfo) {
215 let header_text = vec![
216 Line::from(vec![
217 Span::styled("Dataset: ", AxonmlTheme::muted()),
218 Span::styled(&dataset.name, AxonmlTheme::title()),
219 ]),
220 Line::from(vec![
221 Span::styled("Path: ", AxonmlTheme::muted()),
222 Span::styled(&dataset.file_path, AxonmlTheme::accent()),
223 ]),
224 Line::from(vec![
225 Span::styled("Total Samples: ", AxonmlTheme::muted()),
226 Span::styled(format_number(dataset.total_samples), AxonmlTheme::metric_value()),
227 Span::raw(" "),
228 Span::styled("Classes: ", AxonmlTheme::muted()),
229 Span::styled(dataset.num_classes.to_string(), AxonmlTheme::metric_value()),
230 Span::raw(" "),
231 Span::styled("Feature Shape: ", AxonmlTheme::muted()),
232 Span::styled(&dataset.feature_dim, AxonmlTheme::layer_shape()),
233 ]),
234 Line::from(vec![
235 Span::styled("File Size: ", AxonmlTheme::muted()),
236 Span::styled(format_size(dataset.file_size), AxonmlTheme::accent()),
237 ]),
238 ];
239
240 let header = Paragraph::new(header_text)
241 .block(
242 Block::default()
243 .borders(Borders::ALL)
244 .border_style(AxonmlTheme::border())
245 .title(Span::styled(" Dataset Info ", AxonmlTheme::header())),
246 );
247
248 frame.render_widget(header, area);
249 }
250
251 fn render_main(&mut self, frame: &mut Frame, area: Rect, dataset: &DatasetInfo) {
252 let chunks = Layout::default()
253 .direction(Direction::Horizontal)
254 .constraints([
255 Constraint::Percentage(50), Constraint::Percentage(50), ])
258 .split(area);
259
260 self.render_class_distribution(frame, chunks[0], dataset);
261 self.render_features(frame, chunks[1], dataset);
262 }
263
264 fn render_class_distribution(&mut self, frame: &mut Frame, area: Rect, dataset: &DatasetInfo) {
265 let items: Vec<ListItem> = dataset
266 .classes
267 .iter()
268 .enumerate()
269 .map(|(i, class)| {
270 let style = if i == self.selected_class && self.active_panel == 0 {
271 AxonmlTheme::selected()
272 } else {
273 Style::default()
274 };
275
276 let bar_width = (class.percentage * 0.3) as usize;
278 let bar = "\u{2588}".repeat(bar_width.min(15));
279
280 let content = Line::from(vec![
281 Span::styled(
282 format!("Class {:>2}: ", class.name),
283 AxonmlTheme::layer_type(),
284 ),
285 Span::styled(
286 format!("{:>6} ", format_number(class.count)),
287 AxonmlTheme::metric_value(),
288 ),
289 Span::styled(
290 format!("({:>5.1}%) ", class.percentage),
291 AxonmlTheme::muted(),
292 ),
293 Span::styled(bar, AxonmlTheme::graph_primary()),
294 ]);
295
296 ListItem::new(content).style(style)
297 })
298 .collect();
299
300 let border_style = if self.active_panel == 0 {
301 AxonmlTheme::border_focused()
302 } else {
303 AxonmlTheme::border()
304 };
305
306 let list = List::new(items)
307 .block(
308 Block::default()
309 .borders(Borders::ALL)
310 .border_style(border_style)
311 .title(Span::styled(" Class Distribution ", AxonmlTheme::header())),
312 )
313 .highlight_style(AxonmlTheme::selected());
314
315 frame.render_stateful_widget(list, area, &mut self.list_state);
316 }
317
318 fn render_features(&self, frame: &mut Frame, area: Rect, dataset: &DatasetInfo) {
319 let rows: Vec<Row> = dataset
320 .features
321 .iter()
322 .map(|feature| {
323 Row::new(vec![
324 Span::styled(&feature.name, AxonmlTheme::layer_type()),
325 Span::styled(&feature.dtype, AxonmlTheme::accent()),
326 Span::styled(&feature.shape, AxonmlTheme::layer_shape()),
327 Span::styled(
328 feature.min.map(|v| format!("{:.2}", v)).unwrap_or_else(|| "-".to_string()),
329 AxonmlTheme::muted(),
330 ),
331 Span::styled(
332 feature.max.map(|v| format!("{:.2}", v)).unwrap_or_else(|| "-".to_string()),
333 AxonmlTheme::muted(),
334 ),
335 Span::styled(
336 feature.mean.map(|v| format!("{:.2}", v)).unwrap_or_else(|| "-".to_string()),
337 AxonmlTheme::metric_value(),
338 ),
339 ])
340 })
341 .collect();
342
343 let border_style = if self.active_panel == 1 {
344 AxonmlTheme::border_focused()
345 } else {
346 AxonmlTheme::border()
347 };
348
349 let table = Table::new(
350 rows,
351 [
352 Constraint::Length(10), Constraint::Length(6), Constraint::Length(10), Constraint::Length(8), Constraint::Length(8), Constraint::Length(8), ],
359 )
360 .header(
361 Row::new(vec!["Name", "Type", "Shape", "Min", "Max", "Mean"])
362 .style(AxonmlTheme::header())
363 .bottom_margin(1),
364 )
365 .block(
366 Block::default()
367 .borders(Borders::ALL)
368 .border_style(border_style)
369 .title(Span::styled(" Features ", AxonmlTheme::header())),
370 );
371
372 frame.render_widget(table, area);
373 }
374
375 fn render_splits(&self, frame: &mut Frame, area: Rect, dataset: &DatasetInfo) {
376 let split_text: Vec<Line> = dataset
377 .splits
378 .iter()
379 .map(|split| {
380 let bar_width = (split.percentage * 0.4) as usize;
381 let bar = "\u{2588}".repeat(bar_width.min(40));
382
383 Line::from(vec![
384 Span::styled(format!("{:<8}", split.name), AxonmlTheme::layer_type()),
385 Span::styled(
386 format!("{:>8} samples ", format_number(split.samples)),
387 AxonmlTheme::metric_value(),
388 ),
389 Span::styled(format!("({:>5.1}%) ", split.percentage), AxonmlTheme::muted()),
390 Span::styled(bar, AxonmlTheme::graph_secondary()),
391 ])
392 })
393 .collect();
394
395 let splits = Paragraph::new(split_text)
396 .block(
397 Block::default()
398 .borders(Borders::ALL)
399 .border_style(AxonmlTheme::border())
400 .title(Span::styled(" Data Splits ", AxonmlTheme::header())),
401 );
402
403 frame.render_widget(splits, area);
404 }
405
406 fn render_empty(&self, frame: &mut Frame, area: Rect) {
407 let text = vec![
408 Line::from(""),
409 Line::from(Span::styled(
410 "No dataset loaded",
411 AxonmlTheme::muted(),
412 )),
413 Line::from(""),
414 Line::from(Span::styled(
415 "Press 'o' to open a dataset file",
416 AxonmlTheme::info(),
417 )),
418 Line::from(Span::styled(
419 "Supported formats: .npz, .csv, .parquet",
420 AxonmlTheme::muted(),
421 )),
422 ];
423
424 let paragraph = Paragraph::new(text)
425 .block(
426 Block::default()
427 .borders(Borders::ALL)
428 .border_style(AxonmlTheme::border())
429 .title(Span::styled(" Dataset ", AxonmlTheme::header())),
430 )
431 .alignment(ratatui::layout::Alignment::Center);
432
433 frame.render_widget(paragraph, area);
434 }
435}
436
437impl Default for DataView {
438 fn default() -> Self {
439 Self::new()
440 }
441}
442
443fn format_size(bytes: u64) -> String {
448 const KB: u64 = 1024;
449 const MB: u64 = KB * 1024;
450 const GB: u64 = MB * 1024;
451
452 if bytes >= GB {
453 format!("{:.2} GB", bytes as f64 / GB as f64)
454 } else if bytes >= MB {
455 format!("{:.2} MB", bytes as f64 / MB as f64)
456 } else if bytes >= KB {
457 format!("{:.2} KB", bytes as f64 / KB as f64)
458 } else {
459 format!("{} B", bytes)
460 }
461}
462
463fn format_number(n: usize) -> String {
464 if n >= 1_000_000 {
465 format!("{:.2}M", n as f64 / 1_000_000.0)
466 } else if n >= 1_000 {
467 format!("{:.1}K", n as f64 / 1_000.0)
468 } else {
469 n.to_string()
470 }
471}