lk-inside 0.3.1

A terminal user interface (TUI) application for interactive data analysis.
Documentation
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];

    // Create a 2-row layout: top for stats, bottom for charts/analysis
    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];

    // Bottom row split into two for Histogram and Null Analysis
    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];

    // Top Row: Descriptive Statistics
    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);
    }

    // Bottom-Left: Histogram
    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);
    }

    // Bottom-Right: Null Analysis (moved here)
    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,
    }
}