1use crate::sampling::ReadWatch;
5use crate::value_counts::{LineKind, Order, ValueCounts};
6use std::collections::HashMap;
7use std::sync::Arc;
8
9#[derive(Debug, Clone)]
11pub struct Computing {
12 pub column: String,
13 pub exact: bool,
15 pub watch: ReadWatch,
17 pub file_starts: Option<std::sync::Arc<Vec<usize>>>,
20}
21
22impl Computing {
23 pub fn files_reached(&self) -> Option<(usize, usize)> {
25 let starts = self.file_starts.as_ref()?;
26 let files = starts.len().checked_sub(1)?;
27 let seen = self.watch.rows_seen()?;
28 let reached = starts[..files].partition_point(|&start| start <= seen);
29 Some((reached.min(files), files))
30 }
31}
32
33#[derive(Debug, Default)]
34pub struct ValueCountsModal {
35 pub columns: Vec<String>,
37 pub at: usize,
39 pub order: Order,
40 pub selected: usize,
42 pub offset: usize,
43 pub page: usize,
45 pub body_top: u16,
47 pub computing: Option<Computing>,
48 pub failed: Option<(String, String)>,
50 frame: Option<u64>,
52 held: HashMap<String, Arc<ValueCounts>>,
54 pub drill_return: bool,
56 pub view: Option<CountsView>,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum CountsView {
64 Listing,
65 Histogram,
66}
67
68impl ValueCountsModal {
69 pub fn open(&mut self, columns: Vec<String>, at: usize, frame: u64) {
72 if self.frame != Some(frame) || self.columns != columns {
73 self.held.clear();
74 }
75 self.frame = Some(frame);
76 self.columns = columns;
77 self.at = at.min(self.columns.len().saturating_sub(1));
78 self.computing = None;
79 self.failed = None;
80 self.drill_return = false;
81 self.view = None;
82 self.reset_cursor();
83 }
84
85 pub fn rebase(&mut self, frame: u64) {
87 self.frame = Some(frame);
88 }
89
90 pub fn column(&self) -> Option<&str> {
91 self.columns.get(self.at).map(String::as_str)
92 }
93
94 pub fn current(&self) -> Option<&Arc<ValueCounts>> {
96 self.held.get(self.column()?)
97 }
98
99 pub fn hold(&mut self, counts: ValueCounts) {
100 self.held.insert(counts.column.clone(), Arc::new(counts));
101 }
102
103 pub fn counting(&self) -> bool {
105 self.computing
106 .as_ref()
107 .is_some_and(|c| Some(c.column.as_str()) == self.column())
108 }
109
110 pub fn step(&mut self, by: isize) -> bool {
112 let to = self.at.saturating_add_signed(by);
113 if to == self.at || to >= self.columns.len() {
114 return false;
115 }
116 self.at = to;
117 self.failed = None;
118 self.view = None;
119 self.reset_cursor();
120 true
121 }
122
123 pub fn shows_histogram(&self) -> bool {
126 let has = self.current().is_some_and(|c| c.histogram.is_some());
127 has && self.view != Some(CountsView::Listing)
128 }
129
130 pub fn toggle_view(&mut self) {
132 if self.current().is_some_and(|c| c.histogram.is_some()) {
133 self.view = Some(if self.shows_histogram() {
134 CountsView::Listing
135 } else {
136 CountsView::Histogram
137 });
138 }
139 }
140
141 pub fn toggle_order(&mut self) {
142 self.order = self.order.toggled();
143 self.reset_cursor();
144 }
145
146 fn reset_cursor(&mut self) {
147 self.selected = 0;
148 self.offset = 0;
149 }
150
151 fn lines(&self) -> usize {
152 self.current()
153 .map(|c| c.lines(self.order).len())
154 .unwrap_or(0)
155 }
156
157 pub fn move_by(&mut self, by: isize) {
159 let last = self.lines().saturating_sub(1);
160 self.selected = self.selected.saturating_add_signed(by).min(last);
161 }
162
163 pub fn move_to_end(&mut self) {
164 self.selected = self.lines().saturating_sub(1);
165 }
166
167 pub fn move_to_start(&mut self) {
168 self.selected = 0;
169 }
170
171 pub fn selected_kind(&self) -> Option<LineKind> {
173 let counts = self.current()?;
174 counts.lines(self.order).get(self.selected).map(|l| l.kind)
175 }
176
177 pub fn scroll_into_view(&mut self, height: usize) {
179 let height = height.max(1);
180 self.page = height;
181 if self.selected < self.offset {
182 self.offset = self.selected;
183 } else if self.selected >= self.offset + height {
184 self.offset = self.selected + 1 - height;
185 }
186 let last = self.lines().saturating_sub(height);
187 self.offset = self.offset.min(last);
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use polars::prelude::*;
195
196 #[test]
198 fn a_count_says_how_many_files_its_rows_reach() {
199 let computing = Computing {
200 column: "k".to_string(),
201 exact: false,
202 watch: ReadWatch::default(),
203 file_starts: Some(std::sync::Arc::new(vec![0, 100, 250, 400])),
204 };
205 assert_eq!(computing.files_reached(), None, "nothing read yet");
206 computing.watch.saw(0);
207 assert_eq!(computing.files_reached(), Some((1, 3)));
208 computing.watch.saw(150);
209 assert_eq!(computing.files_reached(), Some((2, 3)));
210 computing.watch.saw(1_000);
211 assert_eq!(computing.files_reached(), Some((3, 3)));
212 }
213
214 fn counts(column: &str, values: &[i32]) -> ValueCounts {
215 crate::value_counts::Plan {
216 lf: DataFrame::new_infer_height(vec![Column::new(column.into(), values)])
217 .unwrap()
218 .lazy(),
219 column: column.to_string(),
220 read: crate::value_counts::Read::Exact,
221 known_total: None,
222 streaming: false,
223 }
224 .run(&ReadWatch::default())
225 .unwrap()
226 }
227
228 #[test]
229 fn counts_are_held_per_column_and_dropped_with_the_frame() {
230 let mut modal = ValueCountsModal::default();
231 let columns = vec!["a".to_string(), "b".to_string()];
232 modal.open(columns.clone(), 0, 1);
233 modal.hold(counts("a", &[1, 1, 2]));
234 assert!(modal.current().is_some());
235 assert!(modal.step(1));
236 assert!(modal.current().is_none(), "b is not counted yet");
237 assert!(!modal.step(1), "no column past the last");
238 assert!(modal.step(-1));
239 assert_eq!(modal.current().unwrap().summary.rows, 3);
240
241 modal.open(columns.clone(), 0, 1);
242 assert!(modal.current().is_some(), "the same view keeps its counts");
243 modal.open(columns, 0, 2);
244 assert!(modal.current().is_none(), "another view's counts go");
245 }
246
247 #[test]
250 fn numbers_open_as_a_histogram_and_c_toggles() {
251 let mut modal = ValueCountsModal::default();
252 modal.open(vec!["n".to_string(), "s".to_string()], 0, 1);
253 modal.hold(counts("n", &[1, 1, 2, 3, 3, 3]));
254 assert!(modal.shows_histogram());
255 let histogram = modal.current().unwrap().histogram.clone().unwrap();
256 assert_eq!(
257 histogram.bins.iter().map(|b| b.count).collect::<Vec<_>>(),
258 [2.0, 1.0, 3.0],
259 "a bin per value of a short integer range"
260 );
261 modal.toggle_view();
262 assert!(!modal.shows_histogram());
263 modal.toggle_view();
264 assert!(modal.shows_histogram());
265 modal.toggle_view();
266 assert!(modal.step(1));
267 let text = crate::value_counts::Plan {
268 lf: DataFrame::new_infer_height(vec![Column::new("s".into(), ["a", "b"])])
269 .unwrap()
270 .lazy(),
271 column: "s".to_string(),
272 read: crate::value_counts::Read::Exact,
273 known_total: None,
274 streaming: false,
275 }
276 .run(&ReadWatch::default())
277 .unwrap();
278 modal.hold(text);
279 assert!(!modal.shows_histogram(), "text has no histogram");
280 modal.toggle_view();
281 assert!(!modal.shows_histogram());
282 assert!(modal.step(-1));
283 assert!(modal.shows_histogram(), "back to the number's default");
284 }
285
286 #[test]
287 fn the_cursor_stays_on_the_listing_and_in_view() {
288 let mut modal = ValueCountsModal::default();
289 modal.open(vec!["a".to_string()], 0, 1);
290 modal.hold(counts("a", &[1, 2, 3, 4, 5, 6]));
291 modal.move_by(10);
292 assert_eq!(modal.selected, 5);
293 modal.scroll_into_view(3);
294 assert_eq!(modal.offset, 3);
295 modal.move_by(-10);
296 modal.scroll_into_view(3);
297 assert_eq!((modal.selected, modal.offset), (0, 0));
298 modal.toggle_order();
299 assert_eq!(modal.order, Order::Value);
300 }
301}