Skip to main content

datui_lib/
value_counts_modal.rs

1//! State of the Value Counts screen: which column, the counts read for the view,
2//! the order, the cursor, and the count in flight.
3
4use crate::sampling::ReadWatch;
5use crate::value_counts::{LineKind, Order, ValueCounts};
6use std::collections::HashMap;
7use std::sync::Arc;
8
9/// A count being read.
10#[derive(Debug, Clone)]
11pub struct Computing {
12    pub column: String,
13    /// Every row, asked for after a sample.
14    pub exact: bool,
15    /// How the read is told to stop, and what it has read.
16    pub watch: ReadWatch,
17    /// Where each of the dataset's files starts, with the total last, when the read
18    /// is of the files in order: how many of them the rows read have reached.
19    pub file_starts: Option<std::sync::Arc<Vec<usize>>>,
20}
21
22impl Computing {
23    /// The files the rows read so far have reached, and the files there are.
24    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    /// The view's columns, in its order, as the screen opened: what ←→ step through.
36    pub columns: Vec<String>,
37    /// The column on screen, an index into `columns`.
38    pub at: usize,
39    pub order: Order,
40    /// The line the cursor is on, and the first line drawn.
41    pub selected: usize,
42    pub offset: usize,
43    /// Lines the last frame drew: what PgUp and PgDn move.
44    pub page: usize,
45    /// Where the last frame's listing (or histogram) began, under the summary.
46    pub body_top: u16,
47    pub computing: Option<Computing>,
48    /// Why the column on screen could not be counted.
49    pub failed: Option<(String, String)>,
50    /// The frame (`len_generation`) the counts held are of.
51    frame: Option<u64>,
52    /// Counts read for the frame, by column: going back to a column reads nothing.
53    held: HashMap<String, Arc<ValueCounts>>,
54    /// Set by a drill from this screen: Esc out of that drill comes back here.
55    pub drill_return: bool,
56    /// The histogram or the listing, as chosen with `c`; `None` until then, which
57    /// shows a number column's histogram and anything else's listing.
58    pub view: Option<CountsView>,
59}
60
61/// What the screen shows of the counts.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum CountsView {
64    Listing,
65    Histogram,
66}
67
68impl ValueCountsModal {
69    /// Open on column `at` of `columns`, for the view `frame`. The counts held for
70    /// another frame are of other rows, and go.
71    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    /// The view came back as it was (Esc out of a drill), under a new frame number.
86    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    /// The counts of the column on screen, once read.
95    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    /// Whether the column on screen is being counted.
104    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    /// Step to the column `by` places over. Whether it moved.
111    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    /// Whether the counts on screen show as a histogram: a number column's do, until
124    /// `c` turns to the listing.
125    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    /// `c`: between the histogram and the listing, where the column has both.
131    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    /// Move the cursor `by` lines, staying on the listing.
158    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    /// What the line under the cursor stands for.
172    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    /// Keep the cursor among the `height` lines drawn.
178    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    /// A count of a dataset of files says how many the rows it has read reach.
197    #[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    /// A number column opens as its histogram; `c` turns to the listing and back,
248    /// and the next column starts from its own type again.
249    #[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}