feat: add Axis::None for hidden dimensions with implicit aggregation

Categories on the None axis are excluded from the grid and cell keys.
When evaluating cells, values across hidden dimensions are aggregated
using a per-measure function (default SUM). Adds evaluate_aggregated
to Model, none_cats to GridLayout, and 'n' shortcut in TileSelect.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Edward Langley
2026-04-02 16:38:35 -07:00
parent dd728ccac8
commit 5a251a1cbe
9 changed files with 193 additions and 12 deletions

View File

@ -402,7 +402,8 @@ impl App {
// yy = yank current cell
('y', KeyCode::Char('y')) => {
if let Some(key) = self.selected_cell_key() {
self.yanked = self.model.evaluate(&key);
let layout = GridLayout::new(&self.model, self.model.active_view());
self.yanked = self.model.evaluate_aggregated(&key, &layout.none_cats);
self.status_msg = "Yanked".to_string();
}
}
@ -1111,6 +1112,19 @@ impl App {
}
self.mode = AppMode::Normal;
}
KeyCode::Char('n') => {
if let Some(name) = cat_names.get(cat_idx) {
command::dispatch(
&mut self.model,
&Command::SetAxis {
category: name.clone(),
axis: Axis::None,
},
);
self.dirty = true;
}
self.mode = AppMode::Normal;
}
_ => {}
}
Ok(())
@ -1386,7 +1400,7 @@ impl App {
Some(k) => k,
None => return false,
};
let s = match self.model.evaluate(&key) {
let s = match self.model.evaluate_aggregated(&key, &layout.none_cats) {
Some(CellValue::Number(n)) => format!("{n}"),
Some(CellValue::Text(t)) => t,
None => String::new(),
@ -1608,7 +1622,7 @@ impl App {
AppMode::CategoryAdd { .. } => "Enter:add & continue Tab:same Esc:done — type a category name",
AppMode::ItemAdd { .. } => "Enter:add & continue Tab:same Esc:done — type an item name",
AppMode::ViewPanel => "jk:nav Enter:switch n:new d:delete Esc:back",
AppMode::TileSelect { .. } => "hl:select Enter:cycle r/c/p:set-axis Esc:back",
AppMode::TileSelect { .. } => "hl:select Enter:cycle r/c/p/n:set-axis Esc:back",
AppMode::CommandMode { .. } => ":q quit :w save :import :add-cat :formula :show-item :help",
AppMode::ImportWizard => "Space:toggle c:cycle Enter:next Esc:cancel",
_ => "",

View File

@ -14,6 +14,7 @@ fn axis_display(axis: Axis) -> (&'static str, Color) {
Axis::Row => ("Row ↕", Color::Green),
Axis::Column => ("Col ↔", Color::Blue),
Axis::Page => ("Page ☰", Color::Magenta),
Axis::None => ("None ∅", Color::DarkGray),
}
}

View File

@ -291,7 +291,7 @@ impl<'a> GridWidget<'a> {
continue;
}
};
let value = self.model.evaluate(&key);
let value = self.model.evaluate_aggregated(&key, &layout.none_cats);
let cell_str = format_value(value.as_ref(), fmt_comma, fmt_decimals);
let is_selected = ri == sel_row && ci == sel_col;
@ -378,7 +378,7 @@ impl<'a> GridWidget<'a> {
}
let total: f64 = (0..layout.row_count())
.filter_map(|ri| layout.cell_key(ri, ci))
.map(|key| self.model.evaluate_f64(&key))
.map(|key| self.model.evaluate_aggregated_f64(&key, &layout.none_cats))
.sum();
let total_str = format_f64(total, fmt_comma, fmt_decimals);
buf.set_string(

View File

@ -14,6 +14,7 @@ fn axis_display(axis: Axis) -> (&'static str, Color) {
Axis::Row => ("", Color::Green),
Axis::Column => ("", Color::Blue),
Axis::Page => ("", Color::Magenta),
Axis::None => ("", Color::DarkGray),
}
}