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 #[must_use]
20 pub fn new() -> Self {
21 Self { handle_keys: true }
22 }
23
24 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 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 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 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 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 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#[derive(Debug, Clone, PartialEq)]
160pub enum StatsAction {
161 Continue,
163 Close,
165 Quit,
167 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}