use crate::sampling::ReadWatch;
use crate::value_counts::{LineKind, Order, ValueCounts};
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct Computing {
pub column: String,
pub exact: bool,
pub watch: ReadWatch,
pub file_starts: Option<std::sync::Arc<Vec<usize>>>,
}
impl Computing {
pub fn files_reached(&self) -> Option<(usize, usize)> {
let starts = self.file_starts.as_ref()?;
let files = starts.len().checked_sub(1)?;
let seen = self.watch.rows_seen()?;
let reached = starts[..files].partition_point(|&start| start <= seen);
Some((reached.min(files), files))
}
}
#[derive(Debug, Default)]
pub struct ValueCountsModal {
pub columns: Vec<String>,
pub at: usize,
pub order: Order,
pub selected: usize,
pub offset: usize,
pub page: usize,
pub body_top: u16,
pub computing: Option<Computing>,
pub failed: Option<(String, String)>,
frame: Option<u64>,
held: HashMap<String, Arc<ValueCounts>>,
pub drill_return: bool,
pub view: Option<CountsView>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CountsView {
Listing,
Histogram,
}
impl ValueCountsModal {
pub fn open(&mut self, columns: Vec<String>, at: usize, frame: u64) {
if self.frame != Some(frame) || self.columns != columns {
self.held.clear();
}
self.frame = Some(frame);
self.columns = columns;
self.at = at.min(self.columns.len().saturating_sub(1));
self.computing = None;
self.failed = None;
self.drill_return = false;
self.view = None;
self.reset_cursor();
}
pub fn rebase(&mut self, frame: u64) {
self.frame = Some(frame);
}
pub fn column(&self) -> Option<&str> {
self.columns.get(self.at).map(String::as_str)
}
pub fn current(&self) -> Option<&Arc<ValueCounts>> {
self.held.get(self.column()?)
}
pub fn hold(&mut self, counts: ValueCounts) {
self.held.insert(counts.column.clone(), Arc::new(counts));
}
pub fn counting(&self) -> bool {
self.computing
.as_ref()
.is_some_and(|c| Some(c.column.as_str()) == self.column())
}
pub fn step(&mut self, by: isize) -> bool {
let to = self.at.saturating_add_signed(by);
if to == self.at || to >= self.columns.len() {
return false;
}
self.at = to;
self.failed = None;
self.view = None;
self.reset_cursor();
true
}
pub fn shows_histogram(&self) -> bool {
let has = self.current().is_some_and(|c| c.histogram.is_some());
has && self.view != Some(CountsView::Listing)
}
pub fn toggle_view(&mut self) {
if self.current().is_some_and(|c| c.histogram.is_some()) {
self.view = Some(if self.shows_histogram() {
CountsView::Listing
} else {
CountsView::Histogram
});
}
}
pub fn toggle_order(&mut self) {
self.order = self.order.toggled();
self.reset_cursor();
}
fn reset_cursor(&mut self) {
self.selected = 0;
self.offset = 0;
}
fn lines(&self) -> usize {
self.current()
.map(|c| c.lines(self.order).len())
.unwrap_or(0)
}
pub fn move_by(&mut self, by: isize) {
let last = self.lines().saturating_sub(1);
self.selected = self.selected.saturating_add_signed(by).min(last);
}
pub fn move_to_end(&mut self) {
self.selected = self.lines().saturating_sub(1);
}
pub fn move_to_start(&mut self) {
self.selected = 0;
}
pub fn selected_kind(&self) -> Option<LineKind> {
let counts = self.current()?;
counts.lines(self.order).get(self.selected).map(|l| l.kind)
}
pub fn scroll_into_view(&mut self, height: usize) {
let height = height.max(1);
self.page = height;
if self.selected < self.offset {
self.offset = self.selected;
} else if self.selected >= self.offset + height {
self.offset = self.selected + 1 - height;
}
let last = self.lines().saturating_sub(height);
self.offset = self.offset.min(last);
}
}
#[cfg(test)]
mod tests {
use super::*;
use polars::prelude::*;
#[test]
fn a_count_says_how_many_files_its_rows_reach() {
let computing = Computing {
column: "k".to_string(),
exact: false,
watch: ReadWatch::default(),
file_starts: Some(std::sync::Arc::new(vec![0, 100, 250, 400])),
};
assert_eq!(computing.files_reached(), None, "nothing read yet");
computing.watch.saw(0);
assert_eq!(computing.files_reached(), Some((1, 3)));
computing.watch.saw(150);
assert_eq!(computing.files_reached(), Some((2, 3)));
computing.watch.saw(1_000);
assert_eq!(computing.files_reached(), Some((3, 3)));
}
fn counts(column: &str, values: &[i32]) -> ValueCounts {
crate::value_counts::Plan {
lf: DataFrame::new_infer_height(vec![Column::new(column.into(), values)])
.unwrap()
.lazy(),
column: column.to_string(),
read: crate::value_counts::Read::Exact,
known_total: None,
streaming: false,
}
.run(&ReadWatch::default())
.unwrap()
}
#[test]
fn counts_are_held_per_column_and_dropped_with_the_frame() {
let mut modal = ValueCountsModal::default();
let columns = vec!["a".to_string(), "b".to_string()];
modal.open(columns.clone(), 0, 1);
modal.hold(counts("a", &[1, 1, 2]));
assert!(modal.current().is_some());
assert!(modal.step(1));
assert!(modal.current().is_none(), "b is not counted yet");
assert!(!modal.step(1), "no column past the last");
assert!(modal.step(-1));
assert_eq!(modal.current().unwrap().summary.rows, 3);
modal.open(columns.clone(), 0, 1);
assert!(modal.current().is_some(), "the same view keeps its counts");
modal.open(columns, 0, 2);
assert!(modal.current().is_none(), "another view's counts go");
}
#[test]
fn numbers_open_as_a_histogram_and_c_toggles() {
let mut modal = ValueCountsModal::default();
modal.open(vec!["n".to_string(), "s".to_string()], 0, 1);
modal.hold(counts("n", &[1, 1, 2, 3, 3, 3]));
assert!(modal.shows_histogram());
let histogram = modal.current().unwrap().histogram.clone().unwrap();
assert_eq!(
histogram.bins.iter().map(|b| b.count).collect::<Vec<_>>(),
[2.0, 1.0, 3.0],
"a bin per value of a short integer range"
);
modal.toggle_view();
assert!(!modal.shows_histogram());
modal.toggle_view();
assert!(modal.shows_histogram());
modal.toggle_view();
assert!(modal.step(1));
let text = crate::value_counts::Plan {
lf: DataFrame::new_infer_height(vec![Column::new("s".into(), ["a", "b"])])
.unwrap()
.lazy(),
column: "s".to_string(),
read: crate::value_counts::Read::Exact,
known_total: None,
streaming: false,
}
.run(&ReadWatch::default())
.unwrap();
modal.hold(text);
assert!(!modal.shows_histogram(), "text has no histogram");
modal.toggle_view();
assert!(!modal.shows_histogram());
assert!(modal.step(-1));
assert!(modal.shows_histogram(), "back to the number's default");
}
#[test]
fn the_cursor_stays_on_the_listing_and_in_view() {
let mut modal = ValueCountsModal::default();
modal.open(vec!["a".to_string()], 0, 1);
modal.hold(counts("a", &[1, 2, 3, 4, 5, 6]));
modal.move_by(10);
assert_eq!(modal.selected, 5);
modal.scroll_into_view(3);
assert_eq!(modal.offset, 3);
modal.move_by(-10);
modal.scroll_into_view(3);
assert_eq!((modal.selected, modal.offset), (0, 0));
modal.toggle_order();
assert_eq!(modal.order, Order::Value);
}
}