presto_cli/
tui.rs

1use crossterm::{
2    event::{self, Event, KeyCode},
3    execute,
4    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
5};
6use ratatui::{
7    backend::CrosstermBackend,
8    layout::{Constraint, Direction, Layout, Rect},
9    style::{Color, Modifier, Style},
10    text::{Line, Span},
11    widgets::{Block, BorderType, Borders, Paragraph, Row, Table, TableState, Tabs},
12    Terminal,
13};
14use std::io;
15use crate::{Dataset, Description, PrestoError};
16use serde_json;
17
18pub fn render_tui(dataset: &Dataset, description: &Description) -> Result<(), PrestoError> {
19    enable_raw_mode().map_err(|e| PrestoError::InvalidNumeric(e.to_string()))?;
20    let mut stdout = io::stdout();
21    execute!(stdout, EnterAlternateScreen).map_err(|e| PrestoError::InvalidNumeric(e.to_string()))?;
22    let backend = CrosstermBackend::new(stdout);
23    let mut terminal = Terminal::new(backend).map_err(|e| PrestoError::InvalidNumeric(e.to_string()))?;
24    let mut tab_index = 0;
25    let mut table_state = TableState::default();
26    let mut table_h_scroll = 0usize;
27    let mut corr_state = TableState::default();
28    let mut corr_h_scroll = 0usize;
29    let mut details_v_scroll = 0u16;
30    let mut details_h_scroll = 0u16;
31    let mut advanced_v_scroll = 0u16;
32    let mut advanced_h_scroll = 0u16;
33    let mut plots_v_scroll = 0u16;
34    let mut plots_h_scroll = 0u16;
35
36    loop {
37        let size = terminal.size().map_err(|e| PrestoError::InvalidNumeric(e.to_string()))?;
38        let full_area = Rect::new(0, 0, size.width, size.height);
39        let chunks = Layout::default()
40            .direction(Direction::Vertical)
41            .margin(1)
42            .constraints([
43                Constraint::Length(3),  
44                Constraint::Length(3),  
45                Constraint::Min(10),    
46                Constraint::Length(3),  
47            ])
48            .split(full_area);
49        let content_area = chunks[2];
50        let content_height = content_area.height.saturating_sub(2) as usize;
51        let content_width = content_area.width.saturating_sub(2) as usize;
52
53        let header_cells = vec![
54            "Column", "Mean", "Median", "StdDev", "Variance", "Min", "Max", "Skew", "Kurt",
55        ];
56        let widths = [15usize, 10, 10, 10, 10, 10, 10, 10, 10];
57        let total_cols = header_cells.len();
58        let total_width: usize = widths.iter().sum();
59
60        terminal.draw(|f| {
61            let title = Paragraph::new("⚡ Presto Presto accelerates preprocessing with precision ⚡")
62                .style(Style::default().fg(Color::Green).add_modifier(Modifier::BOLD))
63                .block(Block::default().borders(Borders::ALL).border_style(Style::default().fg(Color::Cyan)));
64            f.render_widget(title, chunks[0]);
65
66            let tab_titles = vec!["📊 Stats", "📋 Details", "🔍 Advanced", "🔗 Correlations", "📈 Plots"];
67            let tabs = Tabs::new(tab_titles.into_iter().map(String::from).collect::<Vec<_>>())
68                .select(tab_index)
69                .style(Style::default().fg(Color::White))
70                .highlight_style(Style::default().fg(Color::Green).add_modifier(Modifier::BOLD))
71                .divider("│");
72            f.render_widget(tabs, chunks[1]);
73
74            match tab_index {
75                0 => { 
76                    let mut visible_width = 0;
77                    let mut end_col = table_h_scroll;
78                    for i in table_h_scroll..total_cols {
79                        visible_width += widths[i];
80                        if visible_width > content_width {
81                            end_col = i;
82                            break;
83                        }
84                        end_col = i + 1;
85                    }
86                    let start_col = table_h_scroll;
87                    let visible_headers = &header_cells[start_col..end_col];
88                    let visible_widths = &widths[start_col..end_col];
89
90                    let all_rows: Vec<Row> = dataset.headers.iter().enumerate().map(|(i, header)| {
91                        let stats = &description.stats[i];
92                        let skew_desc = stats.skewness.map(|s| match s {
93                            s if s > 1.0 => "Highly +ve skewed",
94                            s if s > 0.5 => "Mod. +ve skewed",
95                            s if s < -1.0 => "Highly -ve skewed",
96                            s if s < -0.5 => "Mod. -ve skewed",
97                            _ => "Symmetric",
98                        }).unwrap_or("N/A");
99                        let kurt_desc = stats.kurtosis.map(|k| match k {
100                            k if k > 3.0 => "Leptokurtic",
101                            k if k < 3.0 => "Platykurtic",
102                            _ => "Mesokurtic",
103                        }).unwrap_or("N/A");
104                        Row::new(vec![
105                            header.clone(),
106                            stats.mean.map_or("N/A".to_string(), |v| format!("{:.2}", v)),
107                            stats.median.map_or("N/A".to_string(), |v| format!("{:.2}", v)),
108                            stats.std_dev.map_or("N/A".to_string(), |v| format!("{:.2}", v)),
109                            stats.variance.map_or("N/A".to_string(), |v| format!("{:.2}", v)),
110                            stats.min.map_or("N/A".to_string(), |v| format!("{:.2}", v)),
111                            stats.max.map_or("N/A".to_string(), |v| format!("{:.2}", v)),
112                            stats.skewness.map_or("N/A".to_string(), |v| format!("{:.2} ({})", v, skew_desc)),
113                            stats.kurtosis.map_or("N/A".to_string(), |v| format!("{:.2} ({})", v, kurt_desc)),
114                        ][start_col..end_col].to_vec())
115                    }).collect();
116
117                    let header = Row::new(visible_headers.to_vec()).style(Style::default().fg(Color::Green));
118                    let stats_table = Table::new(all_rows, visible_widths.iter().map(|&w| Constraint::Length(w as u16)))
119                        .header(header)
120                        .block(Block::default()
121                            .title("Statistics")
122                            .borders(Borders::ALL)
123                            .border_type(BorderType::Thick)
124                            .border_style(Style::default().fg(Color::Cyan)))
125                        .column_spacing(1)
126                        .style(Style::default().fg(Color::White));
127                    if dataset.headers.len() > content_height {
128                        f.render_stateful_widget(stats_table, content_area, &mut table_state);
129                    } else {
130                        f.render_widget(stats_table, content_area);
131                    }
132                }
133                1 => { 
134                    let info_text: Vec<Line> = vec![
135                        Line::from(vec![Span::styled("Rows: ", Style::default().fg(Color::Magenta)), Span::raw(description.total_rows.to_string())]),
136                        Line::from(vec![Span::styled("Cols: ", Style::default().fg(Color::Magenta)), Span::raw(dataset.headers.len().to_string())]),
137                        Line::from(vec![Span::styled("Missing %: ", Style::default().fg(Color::Magenta)), Span::raw(format!("{:.1}", description.missing_pct))]),
138                        Line::from(vec![Span::styled("Unique %: ", Style::default().fg(Color::Magenta)), Span::raw(format!("{:.1}", description.unique_pct))]),
139                        Line::from(vec![Span::styled("Missing: ", Style::default().fg(Color::Magenta)), Span::raw(description.missing.iter().map(|&m| m.to_string()).collect::<Vec<_>>().join(", "))]),
140                        Line::from(vec![Span::styled("Duplicates: ", Style::default().fg(Color::Magenta)), Span::raw(description.duplicates.to_string())]),
141                        Line::from(vec![Span::styled("Outliers: ", Style::default().fg(Color::Magenta)), Span::raw(description.outliers.iter().enumerate().map(|(i, o)| format!("{}: {:?}", dataset.headers[i], o)).collect::<Vec<_>>().join(", "))]),
142                        Line::from(vec![Span::styled("Types: ", Style::default().fg(Color::Magenta)), Span::raw(description.types.iter().map(|t| format!("{:?}", t)).collect::<Vec<_>>().join(", "))]),
143                        Line::from(vec![Span::styled("Cardinality: ", Style::default().fg(Color::Blue)), Span::raw(description.cardinality.iter().map(|&c| c.to_string()).collect::<Vec<_>>().join(", "))]),
144                        Line::from(vec![Span::styled("Distributions: ", Style::default().fg(Color::Blue)), Span::raw(description.distributions.iter().map(|d| d.iter().map(|&(mid, cnt)| format!("{:.1}:{}", mid, cnt)).collect::<Vec<_>>().join("|")).collect::<Vec<_>>().join(", "))]),
145                        Line::from(vec![Span::styled("Top Values: ", Style::default().fg(Color::Blue)), Span::raw(description.top_values.iter().map(|(col, vals)| format!("{}: {}", col, vals.iter().map(|(v, c)| format!("{}({})", v, c)).collect::<Vec<_>>().join(", "))).collect::<Vec<_>>().join("; "))]),
146                    ];
147                    let info_block = Paragraph::new(info_text.clone())
148                        .block(Block::default()
149                            .title("Details")
150                            .borders(Borders::ALL)
151                            .border_type(BorderType::Thick)
152                            .border_style(Style::default().fg(Color::Cyan)))
153                        .style(Style::default().fg(Color::White))
154                        .scroll((details_v_scroll, details_h_scroll));
155                    f.render_widget(info_block, content_area);
156                }
157                2 => { 
158                    let advanced_text: Vec<Line> = vec![
159                        Line::from(vec![Span::styled("Dependency: ", Style::default().fg(Color::Green)), Span::raw(description.dependency_scores.iter().map(|&s| format!("{:.2}", s)).collect::<Vec<_>>().join(", "))]),
160                        Line::from(vec![Span::styled("Drift: ", Style::default().fg(Color::Green)), Span::raw(description.drift_scores.iter().map(|&s| format!("{:.2}", s)).collect::<Vec<_>>().join(", "))]),
161                        Line::from(vec![Span::styled("Consistency Issues: ", Style::default().fg(Color::Red)), Span::raw(description.consistency_issues.iter().map(|&i| i.to_string()).collect::<Vec<_>>().join(", "))]),
162                        Line::from(vec![Span::styled("Temporal: ", Style::default().fg(Color::Red)), Span::raw(description.temporal_patterns.join(", "))]),
163                        Line::from(vec![Span::styled("Transforms: ", Style::default().fg(Color::Red)), Span::raw(description.transform_suggestions.join(", "))]),
164                        Line::from(vec![Span::styled("Noise: ", Style::default().fg(Color::Yellow)), Span::raw(description.noise_scores.iter().map(|&n| format!("{:.2}", n)).collect::<Vec<_>>().join(", "))]),
165                        Line::from(vec![Span::styled("Redundancy: ", Style::default().fg(Color::Yellow)), Span::raw(
166                            if description.redundancy_pairs.is_empty() {
167                                "None".to_string()
168                            } else {
169                                description.redundancy_pairs.iter()
170                                    .map(|&(i, j, s)| format!("{}<->{}:{:.2}", dataset.headers[i], dataset.headers[j], s))
171                                    .collect::<Vec<_>>()
172                                    .join(", ")
173                            }
174                        )]),
175                        Line::from(vec![Span::styled("Feature Importance: ", Style::default().fg(Color::Green)), Span::raw(description.feature_importance.iter().map(|&(col, score)| format!("{}:{:.2}", dataset.headers[col], score)).collect::<Vec<_>>().join(", "))]),
176                        Line::from(vec![Span::styled("Anomalies: ", Style::default().fg(Color::Red)), Span::raw(description.anomalies.iter().map(|(col, val, idx)| format!("{}:{} (idx {})", dataset.headers[*col], val, idx)).collect::<Vec<_>>().join(", "))]),
177                    ];
178                    let advanced_block = Paragraph::new(advanced_text.clone())
179                        .block(Block::default()
180                            .title("Advanced")
181                            .borders(Borders::ALL)
182                            .border_type(BorderType::Thick)
183                            .border_style(Style::default().fg(Color::Cyan)))
184                        .style(Style::default().fg(Color::White))
185                        .scroll((advanced_v_scroll, advanced_h_scroll));
186                    f.render_widget(advanced_block, content_area);
187                }
188                3 => { 
189                    let corr_headers = dataset.headers.clone();
190                    let corr_widths = vec![15usize; corr_headers.len() + 1];
191                    let total_corr_cols = corr_headers.len() + 1;
192                    let _total_corr_width: usize = corr_widths.iter().sum();
193
194                    let mut visible_width = 0;
195                    let mut end_col = corr_h_scroll;
196                    for i in corr_h_scroll..total_corr_cols {
197                        visible_width += corr_widths[i];
198                        if visible_width > content_width {
199                            end_col = i;
200                            break;
201                        }
202                        end_col = i + 1;
203                    }
204                    let start_col = corr_h_scroll;
205                    let visible_headers = &corr_headers[start_col.saturating_sub(1)..end_col.saturating_sub(1)];
206
207                    let all_rows: Vec<Row> = dataset.headers.iter().enumerate().map(|(i, header)| {
208                        let mut row = vec![header.clone()];
209                        row.extend(description.correlations[i].iter().map(|&c| format!("{:.2}", c)));
210                        Row::new(row[start_col..end_col].to_vec())
211                    }).collect();
212
213                    let header = Row::new(["".to_string()].iter().chain(visible_headers).cloned().collect::<Vec<_>>()).style(Style::default().fg(Color::Green));
214                    let corr_table = Table::new(all_rows, corr_widths[start_col..end_col].iter().map(|&w| Constraint::Length(w as u16)))
215                        .header(header)
216                        .block(Block::default()
217                            .title("Correlations")
218                            .borders(Borders::ALL)
219                            .border_type(BorderType::Thick)
220                            .border_style(Style::default().fg(Color::Cyan)))
221                        .column_spacing(1)
222                        .style(Style::default().fg(Color::White));
223                    if dataset.headers.len() > content_height {
224                        f.render_stateful_widget(corr_table, content_area, &mut corr_state);
225                    } else {
226                        f.render_widget(corr_table, content_area);
227                    }
228                }
229                4 => { 
230                    let mut plot_text: Vec<Line> = Vec::new();
231                    let max_height = content_area.height.saturating_sub(4) as usize;
232                    for (i, header) in dataset.headers.iter().enumerate() {
233                        plot_text.push(Line::from(Span::styled(format!("{}:", header), Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD))));
234                        if let Some(dist) = description.distributions.get(i) {
235                            if dist.is_empty() {
236                                plot_text.push(Line::from(Span::raw("  (No numeric data)")));
237                                continue;
238                            }
239                            let max_val = dist.iter().map(|&(_, c)| c).max().unwrap_or(1) as f64;
240                            let bar_heights: Vec<usize> = dist.iter()
241                                .map(|&(_, cnt)| (cnt as f64 / max_val * max_height as f64).round() as usize)
242                                .collect();
243                            let max_label_width = dist.iter()
244                                .map(|&(mid, _)| format!("{:.1}", mid).len())
245                                .max()
246                                .unwrap_or(4);
247                            let step = max_val / max_height as f64;
248                            for h in (0..=max_height).rev() {
249                                let count = (h as f64 * step).round() as usize;
250                                let mut line = format!("{:4} | ", count);
251                                for (j, &height) in bar_heights.iter().enumerate() {
252                                    let mid_str = format!("{:.1}", dist[j].0);
253                                    let padding = max_label_width.saturating_sub(mid_str.len()) / 2;
254                                    if h == 0 {
255                                        line.push_str(&" ".repeat(padding));
256                                        line.push_str(&mid_str);
257                                        line.push_str(&" ".repeat(max_label_width.saturating_sub(mid_str.len() - padding)));
258                                    } else {
259                                        line.push_str(&" ".repeat(max_label_width / 2));
260                                        line.push(if height >= h { '█' } else { ' ' });
261                                        line.push_str(&" ".repeat(max_label_width / 2));
262                                    }
263                                    line.push(' ');
264                                }
265                                plot_text.push(Line::from(Span::raw(line)));
266                            }
267                        }
268                        plot_text.push(Line::from(Span::raw(""))); 
269                    }
270                    let plot_block = Paragraph::new(plot_text.clone())
271                        .block(Block::default()
272                            .title("Plots")
273                            .borders(Borders::ALL)
274                            .border_type(BorderType::Thick)
275                            .border_style(Style::default().fg(Color::Cyan)))
276                        .style(Style::default().fg(Color::White))
277                        .scroll((plots_v_scroll, plots_h_scroll));
278                    f.render_widget(plot_block, content_area);
279                }
280                _ => unreachable!(),
281            }
282
283            let footer = Paragraph::new("'q' to exit | 'e' to export | Tab/Shift+Tab to switch tabs")
284                .style(Style::default().fg(Color::Gray))
285                .block(Block::default().borders(Borders::ALL).border_style(Style::default().fg(Color::Cyan)));
286            f.render_widget(footer, chunks[3]);
287        }).map_err(|e| PrestoError::InvalidNumeric(e.to_string()))?;
288
289        if let Event::Key(key) = event::read().map_err(|e| PrestoError::InvalidNumeric(e.to_string()))? {
290            match key.code {
291                KeyCode::Char('q') => break,
292                KeyCode::Char('e') => {
293                    let json = serde_json::to_string_pretty(&description)
294                        .map_err(|e| PrestoError::InvalidNumeric(e.to_string()))?;
295                    std::fs::write("presto_insights.json", json)
296                        .map_err(|e| PrestoError::InvalidNumeric(e.to_string()))?;
297                }
298                KeyCode::Tab => tab_index = (tab_index + 1) % 5,
299                KeyCode::BackTab => tab_index = (tab_index + 4) % 5,
300                KeyCode::Left => {
301                    match tab_index {
302                        0 => if total_width > content_width && table_h_scroll > 0 { table_h_scroll -= 1; }
303                        1 => {
304                            let info_text = vec![
305                                format!("Rows: {}", description.total_rows),
306                                format!("Cols: {}", dataset.headers.len()),
307                                format!("Missing %: {:.1}", description.missing_pct),
308                                format!("Unique %: {:.1}", description.unique_pct),
309                                format!("Missing: {}", description.missing.iter().map(|&m| m.to_string()).collect::<Vec<_>>().join(", ")),
310                                format!("Duplicates: {}", description.duplicates),
311                                format!("Outliers: {}", description.outliers.iter().enumerate().map(|(i, o)| format!("{}: {:?}", dataset.headers[i], o)).collect::<Vec<_>>().join(", ")),
312                                format!("Types: {}", description.types.iter().map(|t| format!("{:?}", t)).collect::<Vec<_>>().join(", ")),
313                                format!("Cardinality: {}", description.cardinality.iter().map(|&c| c.to_string()).collect::<Vec<_>>().join(", ")),
314                                format!("Distributions: {}", description.distributions.iter().map(|d| d.iter().map(|&(mid, cnt)| format!("{:.1}:{}", mid, cnt)).collect::<Vec<_>>().join("|")).collect::<Vec<_>>().join(", ")),
315                                format!("Top Values: {}", description.top_values.iter().map(|(col, vals)| format!("{}: {}", col, vals.iter().map(|(v, c)| format!("{}({})", v, c)).collect::<Vec<_>>().join(", "))).collect::<Vec<_>>().join("; ")),
316                            ];
317                            let max_line_width = info_text.iter().map(|s| s.len()).max().unwrap_or(0);
318                            if max_line_width > content_width && details_h_scroll > 0 { details_h_scroll -= 1; }
319                        }
320                        2 => {
321                            let advanced_text = vec![
322                                format!("Dependency: {}", description.dependency_scores.iter().map(|&s| format!("{:.2}", s)).collect::<Vec<_>>().join(", ")),
323                                format!("Drift: {}", description.drift_scores.iter().map(|&s| format!("{:.2}", s)).collect::<Vec<_>>().join(", ")),
324                                format!("Consistency Issues: {}", description.consistency_issues.iter().map(|&i| i.to_string()).collect::<Vec<_>>().join(", ")),
325                                format!("Temporal: {}", description.temporal_patterns.join(", ")),
326                                format!("Transforms: {}", description.transform_suggestions.join(", ")),
327                                format!("Noise: {}", description.noise_scores.iter().map(|&n| format!("{:.2}", n)).collect::<Vec<_>>().join(", ")),
328                                format!("Redundancy: {}", if description.redundancy_pairs.is_empty() {
329                                    "None".to_string()
330                                } else {
331                                    description.redundancy_pairs.iter()
332                                        .map(|&(i, j, s)| format!("{}<->{}:{:.2}", dataset.headers[i], dataset.headers[j], s))
333                                        .collect::<Vec<_>>()
334                                        .join(", ")
335                                }),
336                                format!("Feature Importance: {}", description.feature_importance.iter().map(|&(col, score)| format!("{}:{:.2}", dataset.headers[col], score)).collect::<Vec<_>>().join(", ")),
337                                format!("Anomalies: {}", description.anomalies.iter().map(|(col, val, idx)| format!("{}:{} (idx {})", dataset.headers[*col], val, idx)).collect::<Vec<_>>().join(", ")),
338                            ];
339                            let max_line_width = advanced_text.iter().map(|s| s.len()).max().unwrap_or(0);
340                            if max_line_width > content_width && advanced_h_scroll > 0 { advanced_h_scroll -= 1; }
341                        }
342                        3 => {
343                            let corr_widths = vec![15usize; dataset.headers.len() + 1];
344                            let total_corr_width: usize = corr_widths.iter().sum();
345                            if total_corr_width > content_width && corr_h_scroll > 0 { corr_h_scroll -= 1; }
346                        }
347                        4 => {
348                            let mut plot_text = Vec::new();
349                            let max_height = content_area.height.saturating_sub(4) as usize;
350                            let mut max_label_width = 4;
351                            for (i, header) in dataset.headers.iter().enumerate() {
352                                plot_text.push(format!("{}:", header));
353                                if let Some(dist) = description.distributions.get(i) {
354                                    if dist.is_empty() {
355                                        plot_text.push("  (No numeric data)".to_string());
356                                        continue;
357                                    }
358                                    max_label_width = dist.iter()
359                                        .map(|&(mid, _)| format!("{:.1}", mid).len())
360                                        .max()
361                                        .unwrap_or(4)
362                                        .max(max_label_width);
363                                    let max_val = dist.iter().map(|&(_, c)| c).max().unwrap_or(1) as f64;
364                                    let bar_heights: Vec<usize> = dist.iter()
365                                        .map(|&(_, cnt)| (cnt as f64 / max_val * max_height as f64).round() as usize)
366                                        .collect();
367                                    let step = max_val / max_height as f64;
368                                    for h in (0..=max_height).rev() {
369                                        let count = (h as f64 * step).round() as usize;
370                                        let mut line = format!("{:4} | ", count);
371                                        for (j, &height) in bar_heights.iter().enumerate() {
372                                            let mid_str = format!("{:.1}", dist[j].0);
373                                            let padding = max_label_width.saturating_sub(mid_str.len()) / 2;
374                                            if h == 0 {
375                                                line.push_str(&" ".repeat(padding));
376                                                line.push_str(&mid_str);
377                                                line.push_str(&" ".repeat(max_label_width.saturating_sub(mid_str.len() - padding)));
378                                            } else {
379                                                line.push_str(&" ".repeat(max_label_width / 2));
380                                                line.push(if height >= h { '█' } else { ' ' });
381                                                line.push_str(&" ".repeat(max_label_width / 2));
382                                            }
383                                            line.push(' ');
384                                        }
385                                        plot_text.push(line);
386                                    }
387                                }
388                                plot_text.push("".to_string());
389                            }
390                            let max_line_width = plot_text.iter().map(|s| s.len()).max().unwrap_or(0);
391                            if max_line_width > content_width && plots_h_scroll > 0 { plots_h_scroll -= 1; }
392                        }
393                        _ => {}
394                    }
395                }
396                KeyCode::Right => {
397                    match tab_index {
398                        0 => {
399                            let mut visible_width = 0;
400                            for &w in &widths[table_h_scroll..] {
401                                if visible_width + w > content_width { break; }
402                                visible_width += w;
403                            }
404                            let max_h_scroll = total_cols.saturating_sub((content_width / 10).max(1));
405                            if total_width > content_width && table_h_scroll < max_h_scroll { table_h_scroll += 1; }
406                        }
407                        1 => {
408                            let info_text = vec![
409                                format!("Rows: {}", description.total_rows),
410                                format!("Cols: {}", dataset.headers.len()),
411                                format!("Missing %: {:.1}", description.missing_pct),
412                                format!("Unique %: {:.1}", description.unique_pct),
413                                format!("Missing: {}", description.missing.iter().map(|&m| m.to_string()).collect::<Vec<_>>().join(", ")),
414                                format!("Duplicates: {}", description.duplicates),
415                                format!("Outliers: {}", description.outliers.iter().enumerate().map(|(i, o)| format!("{}: {:?}", dataset.headers[i], o)).collect::<Vec<_>>().join(", ")),
416                                format!("Types: {}", description.types.iter().map(|t| format!("{:?}", t)).collect::<Vec<_>>().join(", ")),
417                                format!("Cardinality: {}", description.cardinality.iter().map(|&c| c.to_string()).collect::<Vec<_>>().join(", ")),
418                                format!("Distributions: {}", description.distributions.iter().map(|d| d.iter().map(|&(mid, cnt)| format!("{:.1}:{}", mid, cnt)).collect::<Vec<_>>().join("|")).collect::<Vec<_>>().join(", ")),
419                                format!("Top Values: {}", description.top_values.iter().map(|(col, vals)| format!("{}: {}", col, vals.iter().map(|(v, c)| format!("{}({})", v, c)).collect::<Vec<_>>().join(", "))).collect::<Vec<_>>().join("; ")),
420                            ];
421                            let max_line_width = info_text.iter().map(|s| s.len()).max().unwrap_or(0);
422                            let max_h_scroll = max_line_width.saturating_sub(content_width) as u16;
423                            if max_line_width > content_width && details_h_scroll < max_h_scroll { details_h_scroll += 1; }
424                        }
425                        2 => {
426                            let advanced_text = vec![
427                                format!("Dependency: {}", description.dependency_scores.iter().map(|&s| format!("{:.2}", s)).collect::<Vec<_>>().join(", ")),
428                                format!("Drift: {}", description.drift_scores.iter().map(|&s| format!("{:.2}", s)).collect::<Vec<_>>().join(", ")),
429                                format!("Consistency Issues: {}", description.consistency_issues.iter().map(|&i| i.to_string()).collect::<Vec<_>>().join(", ")),
430                                format!("Temporal: {}", description.temporal_patterns.join(", ")),
431                                format!("Transforms: {}", description.transform_suggestions.join(", ")),
432                                format!("Noise: {}", description.noise_scores.iter().map(|&n| format!("{:.2}", n)).collect::<Vec<_>>().join(", ")),
433                                format!("Redundancy: {}", if description.redundancy_pairs.is_empty() {
434                                    "None".to_string()
435                                } else {
436                                    description.redundancy_pairs.iter()
437                                        .map(|&(i, j, s)| format!("{}<->{}:{:.2}", dataset.headers[i], dataset.headers[j], s))
438                                        .collect::<Vec<_>>()
439                                        .join(", ")
440                                }),
441                                format!("Feature Importance: {}", description.feature_importance.iter().map(|&(col, score)| format!("{}:{:.2}", dataset.headers[col], score)).collect::<Vec<_>>().join(", ")),
442                                format!("Anomalies: {}", description.anomalies.iter().map(|(col, val, idx)| format!("{}:{} (idx {})", dataset.headers[*col], val, idx)).collect::<Vec<_>>().join(", ")),
443                            ];
444                            let max_line_width = advanced_text.iter().map(|s| s.len()).max().unwrap_or(0);
445                            let max_h_scroll = max_line_width.saturating_sub(content_width) as u16;
446                            if max_line_width > content_width && advanced_h_scroll < max_h_scroll { advanced_h_scroll += 1; }
447                        }
448                        3 => {
449                            let corr_widths = vec![15usize; dataset.headers.len() + 1];
450                            let total_corr_width: usize = corr_widths.iter().sum();
451                            let max_h_scroll = (dataset.headers.len() + 1).saturating_sub((content_width / 15).max(1));
452                            if total_corr_width > content_width && corr_h_scroll < max_h_scroll { corr_h_scroll += 1; }
453                        }
454                        4 => {
455                            let mut plot_text = Vec::new();
456                            let max_height = content_area.height.saturating_sub(4) as usize;
457                            let mut max_label_width = 4;
458                            for (i, header) in dataset.headers.iter().enumerate() {
459                                plot_text.push(format!("{}:", header));
460                                if let Some(dist) = description.distributions.get(i) {
461                                    if dist.is_empty() {
462                                        plot_text.push("  (No numeric data)".to_string());
463                                        continue;
464                                    }
465                                    max_label_width = dist.iter()
466                                        .map(|&(mid, _)| format!("{:.1}", mid).len())
467                                        .max()
468                                        .unwrap_or(4)
469                                        .max(max_label_width);
470                                    let max_val = dist.iter().map(|&(_, c)| c).max().unwrap_or(1) as f64;
471                                    let bar_heights: Vec<usize> = dist.iter()
472                                        .map(|&(_, cnt)| (cnt as f64 / max_val * max_height as f64).round() as usize)
473                                        .collect();
474                                    let step = max_val / max_height as f64;
475                                    for h in (0..=max_height).rev() {
476                                        let count = (h as f64 * step).round() as usize;
477                                        let mut line = format!("{:4} | ", count);
478                                        for (j, &height) in bar_heights.iter().enumerate() {
479                                            let mid_str = format!("{:.1}", dist[j].0);
480                                            let padding = max_label_width.saturating_sub(mid_str.len()) / 2;
481                                            if h == 0 {
482                                                line.push_str(&" ".repeat(padding));
483                                                line.push_str(&mid_str);
484                                                line.push_str(&" ".repeat(max_label_width.saturating_sub(mid_str.len() - padding)));
485                                            } else {
486                                                line.push_str(&" ".repeat(max_label_width / 2));
487                                                line.push(if height >= h { '█' } else { ' ' });
488                                                line.push_str(&" ".repeat(max_label_width / 2));
489                                            }
490                                            line.push(' ');
491                                        }
492                                        plot_text.push(line);
493                                    }
494                                }
495                                plot_text.push("".to_string());
496                            }
497                            let max_line_width = plot_text.iter().map(|s| s.len()).max().unwrap_or(0);
498                            let max_h_scroll = max_line_width.saturating_sub(content_width) as u16;
499                            if max_line_width > content_width && plots_h_scroll < max_h_scroll { plots_h_scroll += 1; }
500                        }
501                        _ => {}
502                    }
503                }
504                KeyCode::Up => {
505                    match tab_index {
506                        0 => if dataset.headers.len() > content_height {
507                            if let Some(selected) = table_state.selected() {
508                                table_state.select(Some(selected.saturating_sub(1)));
509                            } else {
510                                table_state.select(Some(dataset.headers.len().saturating_sub(1)));
511                            }
512                        }
513                        1 => {
514                            let info_lines = 12usize;
515                            if info_lines > content_height && details_v_scroll > 0 { details_v_scroll -= 1; }
516                        }
517                        2 => {
518                            let advanced_lines = 9usize;
519                            if advanced_lines > content_height && advanced_v_scroll > 0 { advanced_v_scroll -= 1; }
520                        }
521                        3 => if dataset.headers.len() > content_height {
522                            if let Some(selected) = corr_state.selected() {
523                                corr_state.select(Some(selected.saturating_sub(1)));
524                            } else {
525                                corr_state.select(Some(dataset.headers.len().saturating_sub(1)));
526                            }
527                        }
528                        4 => {
529                            let max_height = content_area.height.saturating_sub(4) as usize;
530                            let plot_lines = dataset.headers.len() * (max_height + 2);
531                            if plot_lines > content_height && plots_v_scroll > 0 { plots_v_scroll -= 1; }
532                        }
533                        _ => {}
534                    }
535                }
536                KeyCode::Down => {
537                    match tab_index {
538                        0 => if dataset.headers.len() > content_height {
539                            if let Some(selected) = table_state.selected() {
540                                table_state.select(Some((selected + 1).min(dataset.headers.len() - 1)));
541                            } else {
542                                table_state.select(Some(0));
543                            }
544                        }
545                        1 => {
546                            let info_lines = 12usize;
547                            let max_v_scroll = (info_lines.saturating_sub(content_height)) as u16;
548                            if info_lines > content_height && details_v_scroll < max_v_scroll { details_v_scroll += 1; }
549                        }
550                        2 => {
551                            let advanced_lines = 9usize;
552                            let max_v_scroll = (advanced_lines.saturating_sub(content_height)) as u16;
553                            if advanced_lines > content_height && advanced_v_scroll < max_v_scroll { advanced_v_scroll += 1; }
554                        }
555                        3 => if dataset.headers.len() > content_height {
556                            if let Some(selected) = corr_state.selected() {
557                                corr_state.select(Some((selected + 1).min(dataset.headers.len() - 1)));
558                            } else {
559                                corr_state.select(Some(0));
560                            }
561                        }
562                        4 => {
563                            let max_height = content_area.height.saturating_sub(4) as usize;
564                            let plot_lines = dataset.headers.len() * (max_height + 2);
565                            let max_v_scroll = (plot_lines.saturating_sub(content_height)) as u16;
566                            if plot_lines > content_height && plots_v_scroll < max_v_scroll { plots_v_scroll += 1; }
567                        }
568                        _ => {}
569                    }
570                }
571                _ => {}
572            }
573        }
574    }
575
576    disable_raw_mode().map_err(|e| PrestoError::InvalidNumeric(e.to_string()))?;
577    execute!(terminal.backend_mut(), LeaveAlternateScreen).map_err(|e| PrestoError::InvalidNumeric(e.to_string()))?;
578    terminal.show_cursor().map_err(|e| PrestoError::InvalidNumeric(e.to_string()))?;
579
580    Ok(())
581}