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