use ratatui::{
prelude::*,
widgets::{Block, Borders, Paragraph, BarChart},
};
use crate::ui::components::data_preview::DataPreviewWidget;
use crate::ui::components::stats_panel::StatsPanel;
use crate::{AppState, InputMode, AppScreen};
use crate::ui::components::input_widget::InputWidget;
use crate::ui::components::help_bar::HelpBar;
use crossterm::event::KeyCode;
use crate::ui::theme;
pub struct StatisticsScreen {
pub histogram_column_input: InputWidget,
pub correlation_columns_input: InputWidget,
pub clustering_columns_input: InputWidget,
pub clustering_k_input: InputWidget,
pub histogram_data: Option<Vec<(String, u64)>>,
pub current_page: usize,
pub page_size: usize,
}
impl StatisticsScreen {
pub fn new(settings: &crate::utils::config::Settings) -> Self {
Self {
histogram_column_input: InputWidget::new("Column for Histogram".to_string()),
correlation_columns_input: InputWidget::new("Columns for Correlation (comma-separated)".to_string()),
clustering_columns_input: InputWidget::new("Columns for Clustering (comma-separated)".to_string()),
clustering_k_input: InputWidget::new("Number of Clusters (k)".to_string()),
histogram_data: None,
current_page: 0,
page_size: settings.ui.page_size,
}
}
}
pub fn render_statistics_screen(f: &mut Frame, app_state: &mut AppState) {
let screen = &mut app_state.statistics_screen;
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(0), Constraint::Length(1)].as_ref())
.split(f.area());
let main_area = chunks[0];
let analysis_grid = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(main_area);
let descriptive_stats_area = analysis_grid[0];
let bottom_area = analysis_grid[1];
let bottom_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(bottom_area);
let histogram_area = bottom_chunks[0];
let null_analysis_area = bottom_chunks[1];
if let Some(stats_df) = &app_state.workspace.descriptive_statistics_results {
let stats_panel = StatsPanel::new(stats_df);
stats_panel.render(f, descriptive_stats_area);
} else {
let block = Block::default()
.title("Descriptive Statistics")
.borders(Borders::ALL)
.border_style(theme::BORDER_STYLE);
let paragraph = Paragraph::new("Press 's' to generate descriptive statistics.")
.style(theme::TEXT_STYLE)
.block(block);
f.render_widget(paragraph, descriptive_stats_area);
}
if let Some(data) = &screen.histogram_data {
let borrowed_data: Vec<(&str, u64)> = data.iter().map(|(s, v)| (s.as_str(), *v)).collect();
let barchart = BarChart::default()
.block(Block::default().title("Histogram").borders(Borders::ALL).border_style(theme::BORDER_STYLE))
.data(&borrowed_data)
.bar_width(5)
.bar_style(theme::BORDER_STYLE)
.value_style(theme::SELECTED_ITEM_STYLE);
f.render_widget(barchart, histogram_area);
} else {
let block = Block::default()
.title("Histogram")
.borders(Borders::ALL)
.border_style(theme::BORDER_STYLE);
let paragraph = Paragraph::new("Press 'h' to generate a histogram for a numeric column.")
.style(theme::TEXT_STYLE)
.block(block);
f.render_widget(paragraph, histogram_area);
}
if app_state.input_mode == InputMode::EditingColumnForHistogram {
let input_block_area = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(0), Constraint::Length(3)])
.split(histogram_area)[1];
screen.histogram_column_input.render(f, input_block_area, app_state.input_mode == InputMode::EditingColumnForHistogram);
}
if let Some(null_df) = &app_state.workspace.null_analysis_results {
let null_widget = DataPreviewWidget::new(
null_df,
screen.page_size,
);
if let Err(e) = null_widget.render(f, null_analysis_area, screen.current_page, None, Color::Reset, None) {
let block = Block::default().title("Error (Null Analysis)").borders(Borders::ALL).border_style(theme::ERROR_STYLE);
let paragraph = Paragraph::new(format!("Rendering Error: {}", e)).block(block);
f.render_widget(paragraph, null_analysis_area);
}
} else {
let block = Block::default()
.title("Null Analysis View")
.borders(Borders::ALL)
.border_style(theme::BORDER_STYLE);
let paragraph = Paragraph::new("Press 'u' for null analysis.")
.style(theme::TEXT_STYLE)
.block(block);
f.render_widget(paragraph, null_analysis_area);
}
HelpBar::new().render_widget(chunks[1], f.buffer_mut(), &app_state.current_screen, &app_state.input_mode);
}
pub fn handle_input(key: KeyCode, app_state: &mut AppState) -> Option<AppScreen> {
let analyzer = app_state.workspace.analyzer.as_ref();
let screen = &mut app_state.statistics_screen;
match app_state.input_mode {
InputMode::Normal => match key {
KeyCode::Char('s') => {
if let Some(analyzer) = analyzer {
match analyzer.get_descriptive_statistics() {
Ok(stats_df) => {
app_state.workspace.descriptive_statistics_results = Some(stats_df);
}
Err(e) => {
app_state.status_message = format!("Error getting descriptive statistics: {}", e);
}
}
match analyzer.get_null_counts() {
Ok(null_df) => {
app_state.workspace.null_analysis_results = Some(null_df);
}
Err(e) => {
app_state.status_message = format!("Error getting null counts: {}", e);
}
}
if let Some((col_name, _)) = app_state.workspace.column_datatypes.iter().find(|(_, dtype)| dtype.contains("Int") || dtype.contains("Float")) {
match analyzer.get_histogram(col_name, 10) {
Ok(hist_df) => {
let bins: Vec<String> = hist_df.column("bin").unwrap().str().unwrap().into_iter().map(|o| o.unwrap_or_default().to_string()).collect();
let counts: Vec<u64> = hist_df.column("count").unwrap().u32().unwrap().into_iter().map(|o| o.unwrap_or_default() as u64).collect();
screen.histogram_data = Some(bins.into_iter().zip(counts).collect());
app_state.workspace.histogram_results = Some(hist_df);
}
Err(e) => {
app_state.status_message = format!("Error getting histogram for default column '{}': {}", col_name, e);
}
}
} else {
app_state.status_message = "No numeric column found for default histogram. Specify with 'h'.".to_string();
}
}
None
}
KeyCode::Char('u') => {
if let Some(analyzer) = analyzer {
match analyzer.get_null_counts() {
Ok(null_df) => {
app_state.workspace.null_analysis_results = Some(null_df);
}
Err(e) => {
app_state.status_message = format!("Error getting null counts: {}", e);
}
}
}
None
}
KeyCode::Char('h') => {
app_state.input_mode = InputMode::EditingColumnForHistogram;
screen.histogram_column_input.reset();
None
}
KeyCode::Char('m') => {
app_state.input_mode = crate::InputMode::EditingColumnsForCorrelation;
screen.correlation_columns_input.reset();
None
}
KeyCode::Char('c') => {
app_state.input_mode = InputMode::EditingColumnsForClustering;
screen.clustering_columns_input.reset();
None
}
_ => None,
},
InputMode::EditingColumnForHistogram => match key {
KeyCode::Enter => {
if let Some(analyzer) = analyzer {
let col_name = screen.histogram_column_input.get_input();
match analyzer.get_histogram(&col_name, 10) {
Ok(hist_df) => {
let bins: Vec<String> = hist_df.column("bin").unwrap().str().unwrap().into_iter().map(|o| o.unwrap_or_default().to_string()).collect();
let counts: Vec<u64> = hist_df.column("count").unwrap().u32().unwrap().into_iter().map(|o| o.unwrap_or_default() as u64).collect();
screen.histogram_data = Some(bins.into_iter().zip(counts).collect());
app_state.workspace.histogram_results = Some(hist_df);
}
Err(e) => {
app_state.status_message = format!("Error getting histogram: {}", e);
}
}
}
app_state.input_mode = InputMode::Normal;
None
}
KeyCode::Esc => {
app_state.input_mode = InputMode::Normal;
None
}
_ => {
screen.histogram_column_input.handle_key(key);
None
}
},
crate::InputMode::EditingColumnsForCorrelation => match key {
KeyCode::Enter => {
if let Some(analyzer) = analyzer {
let col_names_str = screen.correlation_columns_input.get_input();
let column_names: Vec<&str> = col_names_str.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect();
match analyzer.get_correlation_matrix(&column_names) {
Ok(corr_df) => {
app_state.workspace.correlation_matrix_results = Some(corr_df);
}
Err(e) => {
app_state.status_message = format!("Error getting correlation matrix: {}", e);
}
}
}
app_state.input_mode = InputMode::Normal;
None
}
KeyCode::Esc => {
app_state.input_mode = InputMode::Normal;
None
}
_ => {
screen.correlation_columns_input.handle_key(key);
None
}
},
InputMode::EditingColumnsForClustering => match key {
KeyCode::Enter => {
let _column_names_str = screen.clustering_columns_input.get_input();
app_state.input_mode = InputMode::EditingKForClustering;
screen.clustering_k_input.reset();
None
}
KeyCode::Esc => {
app_state.input_mode = InputMode::Normal;
None
}
_ => {
screen.clustering_columns_input.handle_key(key);
None
}
},
InputMode::EditingKForClustering => match key {
KeyCode::Enter => {
let column_names_str = screen.clustering_columns_input.get_input();
let column_names: Vec<&str> = column_names_str.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect();
let k_str = screen.clustering_k_input.get_input();
if let Ok(k) = k_str.parse::<usize>() {
if let Some(analyzer) = analyzer {
match analyzer.perform_kmeans_clustering(&column_names, k, 100) {
Ok(clustering_df) => {
app_state.workspace.clustering_results = Some(clustering_df);
}
Err(e) => {
app_state.status_message = format!("Error performing clustering: {}", e);
}
}
}
} else {
app_state.status_message = format!("Invalid k value: {}", k_str);
}
app_state.input_mode = InputMode::Normal;
None
}
KeyCode::Esc => {
app_state.input_mode = InputMode::Normal;
None
}
_ => {
screen.clustering_k_input.handle_key(key);
None
}
},
_ => None,
}
}