Skip to main content

axonml_tui/views/
data.rs

1//! Data View - Display Dataset Structure and Statistics
2//!
3//! Shows dataset information including sample counts, class distributions,
4//! feature dimensions, and data splits.
5//!
6//! @version 0.1.0
7//! @author AutomataNexus Development Team
8
9use 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// =============================================================================
22// Types
23// =============================================================================
24
25/// Class distribution information
26#[derive(Debug, Clone)]
27pub struct ClassInfo {
28    pub name: String,
29    pub count: usize,
30    pub percentage: f32,
31}
32
33/// Data split information
34#[derive(Debug, Clone)]
35pub struct SplitInfo {
36    pub name: String,
37    pub samples: usize,
38    pub percentage: f32,
39}
40
41/// Feature information
42#[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/// Dataset information
53#[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
66// =============================================================================
67// Data View
68// =============================================================================
69
70/// Dataset view state
71pub struct DataView {
72    /// Loaded dataset info
73    pub dataset: Option<DatasetInfo>,
74
75    /// Selected class index
76    pub selected_class: usize,
77
78    /// List state for class navigation
79    pub list_state: ListState,
80
81    /// Active panel (0=classes, 1=features)
82    pub active_panel: usize,
83}
84
85impl DataView {
86    /// Create a new data view with demo data
87    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        // Load demo data by default
96        view.load_demo_data();
97        view.list_state.select(Some(0));
98        view
99    }
100
101    /// Load demo dataset for visualization
102    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    /// Move selection up
150    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    /// Move selection down
158    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    /// Load a dataset from a file path
168    pub fn load_dataset(&mut self, _path: &Path) -> Result<(), String> {
169        // For now, just load demo data
170        // In real implementation, would parse actual dataset files
171        self.load_demo_data();
172        Ok(())
173    }
174
175    /// Switch active panel
176    pub fn switch_panel(&mut self) {
177        self.active_panel = (self.active_panel + 1) % 2;
178    }
179
180    /// Go to previous panel
181    pub fn prev_panel(&mut self) {
182        if self.active_panel > 0 {
183            self.active_panel -= 1;
184        } else {
185            self.active_panel = 1; // Wrap around
186        }
187    }
188
189    /// Go to next panel
190    pub fn next_panel(&mut self) {
191        self.active_panel = (self.active_panel + 1) % 2;
192    }
193
194    /// Render the data view
195    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),  // Header
201                    Constraint::Min(10),    // Main content
202                    Constraint::Length(6),  // Splits panel
203                ])
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),  // Class distribution
256                Constraint::Percentage(50),  // Features
257            ])
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                // Create a simple bar using unicode blocks
277                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),  // Name
353                Constraint::Length(6),   // Type
354                Constraint::Length(10),  // Shape
355                Constraint::Length(8),   // Min
356                Constraint::Length(8),   // Max
357                Constraint::Length(8),   // Mean
358            ],
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
443// =============================================================================
444// Helpers
445// =============================================================================
446
447fn 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}