feat: add prune empty feature for pivot views

Add prune_empty feature to hide empty rows/columns in pivot mode.

View gains prune_empty boolean (default false for backward compat).
GridLayout::prune_empty() removes data rows where all columns are
empty and data columns where all rows are empty.

Group headers are preserved if at least one data item survives.
In records mode, pruning is skipped (user drilled in to see all data).

EditOrDrill command updated to check for regular (non-virtual)
categories when determining if a cell is aggregated.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/Qwen3.5-35B-A3B-GGUF:Q5_K_M)
This commit is contained in:
Edward Langley
2026-04-06 15:09:57 -07:00
parent 55cad99ae1
commit eb83df9984
3 changed files with 185 additions and 7 deletions

View File

@ -4,6 +4,10 @@ use std::collections::{HashMap, HashSet};
use super::axis::Axis;
fn default_prune() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct View {
pub name: String,
@ -17,6 +21,9 @@ pub struct View {
pub collapsed_groups: HashMap<String, HashSet<String>>,
/// Number format string (e.g. ",.0f" for comma-separated integer)
pub number_format: String,
/// When true, empty rows/columns are pruned from the display.
#[serde(default = "default_prune")]
pub prune_empty: bool,
/// Scroll offset for grid
pub row_offset: usize,
pub col_offset: usize,
@ -33,6 +40,7 @@ impl View {
hidden_items: HashMap::new(),
collapsed_groups: HashMap::new(),
number_format: ",.0".to_string(),
prune_empty: false,
row_offset: 0,
col_offset: 0,
selected: (0, 0),
@ -41,16 +49,47 @@ impl View {
pub fn on_category_added(&mut self, cat_name: &str) {
if !self.category_axes.contains_key(cat_name) {
// Virtual categories (names starting with `_`) default to Axis::None.
// Virtual/underscore categories default to Axis::None.
// Regular categories auto-assign: first → Row, second → Column, rest → Page.
// If a virtual currently holds Row or Column and a regular category needs
// the slot, bump the virtual to None.
let axis = if cat_name.starts_with('_') {
Axis::None
} else {
let rows = self.categories_on(Axis::Row).len();
let cols = self.categories_on(Axis::Column).len();
if rows == 0 {
let regular_rows: Vec<String> = self
.categories_on(Axis::Row)
.into_iter()
.filter(|c| !c.starts_with('_'))
.map(String::from)
.collect();
let regular_cols: Vec<String> = self
.categories_on(Axis::Column)
.into_iter()
.filter(|c| !c.starts_with('_'))
.map(String::from)
.collect();
if regular_rows.is_empty() {
// Bump any virtual on Row to None
let bump: Vec<String> = self
.categories_on(Axis::Row)
.into_iter()
.filter(|c| c.starts_with('_'))
.map(String::from)
.collect();
for c in bump {
self.category_axes.insert(c, Axis::None);
}
Axis::Row
} else if cols == 0 {
} else if regular_cols.is_empty() {
let bump: Vec<String> = self
.categories_on(Axis::Column)
.into_iter()
.filter(|c| c.starts_with('_'))
.map(String::from)
.collect();
for c in bump {
self.category_axes.insert(c, Axis::None);
}
Axis::Column
} else {
Axis::Page