sql_cli/widgets/
stats_widget.rs1use 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
12pub struct StatsWidget {
14 handle_keys: bool,
16}
17
18impl StatsWidget {
19 pub fn new() -> Self {
20 Self { handle_keys: true }
21 }
22
23 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 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 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 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 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 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#[derive(Debug, Clone, PartialEq)]
162pub enum StatsAction {
163 Continue,
165 Close,
167 Quit,
169 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}