sql_cli/widgets/
stats_widget.rs

1use crate::buffer::{BufferAPI, ColumnStatistics, ColumnType};
2use crate::widget_traits::DebugInfoProvider;
3use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
4use ratatui::{
5    layout::Rect,
6    style::{Color, Modifier, Style},
7    text::Line,
8    widgets::{Block, Borders, Paragraph, Wrap},
9    Frame,
10};
11
12/// A self-contained widget for displaying column statistics
13pub struct StatsWidget {
14    /// Whether the widget should handle its own key events
15    handle_keys: bool,
16}
17
18impl StatsWidget {
19    #[must_use]
20    pub fn new() -> Self {
21        Self { handle_keys: true }
22    }
23
24    /// Handle key input when the stats widget is active
25    /// Returns true if the app should exit, false otherwise
26    pub fn handle_key(&mut self, key: KeyEvent) -> StatsAction {
27        if !self.handle_keys {
28            return StatsAction::PassThrough;
29        }
30
31        match key.code {
32            KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
33                StatsAction::Quit
34            }
35            KeyCode::Char('q' | 'S') | KeyCode::Esc => StatsAction::Close,
36            _ => StatsAction::Continue,
37        }
38    }
39
40    /// Render the statistics widget
41    pub fn render(&self, f: &mut Frame, area: Rect, buffer: &dyn BufferAPI) {
42        if let Some(stats) = buffer.get_column_stats() {
43            let lines = self.build_stats_lines(stats);
44
45            let stats_paragraph = Paragraph::new(lines)
46                .block(Block::default().borders(Borders::ALL).title(format!(
47                    "Column Statistics - {} (S to close)",
48                    stats.column_name
49                )))
50                .wrap(Wrap { trim: false });
51
52            f.render_widget(stats_paragraph, area);
53        } else {
54            let error = Paragraph::new("No statistics available")
55                .block(
56                    Block::default()
57                        .borders(Borders::ALL)
58                        .title("Column Statistics"),
59                )
60                .style(Style::default().fg(Color::Red));
61            f.render_widget(error, area);
62        }
63    }
64
65    /// Build the lines of text for the statistics display
66    fn build_stats_lines(&self, stats: &ColumnStatistics) -> Vec<Line<'static>> {
67        let mut lines = vec![
68            Line::from(format!("Column Statistics: {}", stats.column_name)).style(
69                Style::default()
70                    .fg(Color::Cyan)
71                    .add_modifier(Modifier::BOLD),
72            ),
73            Line::from(""),
74            Line::from(format!("Type: {:?}", stats.column_type))
75                .style(Style::default().fg(Color::Yellow)),
76            Line::from(format!("Total Rows: {}", stats.total_count)),
77            Line::from(format!("Unique Values: {}", stats.unique_count)),
78            Line::from(format!("Null/Empty Count: {}", stats.null_count)),
79            Line::from(""),
80        ];
81
82        // Add numeric statistics if available
83        if matches!(stats.column_type, ColumnType::Numeric | ColumnType::Mixed) {
84            lines.push(
85                Line::from("Numeric Statistics:").style(
86                    Style::default()
87                        .fg(Color::Green)
88                        .add_modifier(Modifier::BOLD),
89                ),
90            );
91
92            if let Some(min) = stats.min {
93                lines.push(Line::from(format!("  Min: {min:.2}")));
94            }
95            if let Some(max) = stats.max {
96                lines.push(Line::from(format!("  Max: {max:.2}")));
97            }
98            if let Some(mean) = stats.mean {
99                lines.push(Line::from(format!("  Mean: {mean:.2}")));
100            }
101            if let Some(median) = stats.median {
102                lines.push(Line::from(format!("  Median: {median:.2}")));
103            }
104            if let Some(sum) = stats.sum {
105                lines.push(Line::from(format!("  Sum: {sum:.2}")));
106            }
107            lines.push(Line::from(""));
108        }
109
110        // Add frequency distribution if available
111        if let Some(ref freq_map) = stats.frequency_map {
112            lines.push(
113                Line::from("Frequency Distribution:").style(
114                    Style::default()
115                        .fg(Color::Magenta)
116                        .add_modifier(Modifier::BOLD),
117                ),
118            );
119
120            // Sort by frequency (descending) and take top 20
121            let mut freq_vec: Vec<(&String, &usize)> = freq_map.iter().collect();
122            freq_vec.sort_by(|a, b| b.1.cmp(a.1));
123
124            let max_count = freq_vec.first().map_or(1, |(_, c)| **c);
125
126            for (value, count) in freq_vec.iter().take(20) {
127                let bar_width = ((**count as f64 / max_count as f64) * 30.0) as usize;
128                let bar = "█".repeat(bar_width);
129                let display_value = if value.len() > 30 {
130                    format!("{}...", &value[..27])
131                } else {
132                    (*value).to_string()
133                };
134                lines.push(Line::from(format!("  {display_value:30} {bar} ({count})")));
135            }
136
137            if freq_vec.len() > 20 {
138                lines.push(
139                    Line::from(format!(
140                        "  ... and {} more unique values",
141                        freq_vec.len() - 20
142                    ))
143                    .style(Style::default().fg(Color::DarkGray)),
144                );
145            }
146        }
147
148        lines.push(Line::from(""));
149        lines.push(
150            Line::from("Press S or Esc to return to results")
151                .style(Style::default().fg(Color::DarkGray)),
152        );
153
154        lines
155    }
156}
157
158/// Actions that can be returned from handling keys
159#[derive(Debug, Clone, PartialEq)]
160pub enum StatsAction {
161    /// Continue showing stats
162    Continue,
163    /// Close the stats view
164    Close,
165    /// Quit the application
166    Quit,
167    /// Pass the key through to the main handler
168    PassThrough,
169}
170
171impl Default for StatsWidget {
172    fn default() -> Self {
173        Self::new()
174    }
175}
176
177impl DebugInfoProvider for StatsWidget {
178    fn debug_info(&self) -> String {
179        let mut info = String::from("=== STATS WIDGET ===\n");
180        info.push_str("State: Active\n");
181        info.push_str(&format!("Handle Keys: {}\n", self.handle_keys));
182        info
183    }
184
185    fn debug_summary(&self) -> String {
186        format!(
187            "StatsWidget: keys={}",
188            if self.handle_keys { "on" } else { "off" }
189        )
190    }
191}