Skip to main content

datui_lib/analysis/
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::analysis::sampling::ReadWatch;
5use crate::analysis::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;