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}