feat: add view history navigation and drill-into-cell
Add view navigation history with back/forward stacks (bound to < and >). Introduce CategoryKind enum to distinguish regular categories from virtual ones (_Index, _Dim) that are synthesized at query time. Add DrillIntoCell command that creates a drill view showing raw data for an aggregated cell, expanding categories on Axis::None into Row and Column axes while filtering by the cell's fixed coordinates. Virtual categories default to Axis::None and are automatically added to all views when the model is initialized. Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/Qwen3.5-35B-A3B-GGUF:Q5_K_M)
This commit is contained in:
@ -28,25 +28,48 @@ pub struct Model {
|
||||
|
||||
impl Model {
|
||||
pub fn new(name: impl Into<String>) -> Self {
|
||||
use crate::model::category::CategoryKind;
|
||||
let name = name.into();
|
||||
let default_view = View::new("Default");
|
||||
let mut views = IndexMap::new();
|
||||
views.insert("Default".to_string(), default_view);
|
||||
Self {
|
||||
let mut categories = IndexMap::new();
|
||||
// Virtual categories — always present, default to Axis::None
|
||||
categories.insert(
|
||||
"_Index".to_string(),
|
||||
Category::new(0, "_Index").with_kind(CategoryKind::VirtualIndex),
|
||||
);
|
||||
categories.insert(
|
||||
"_Dim".to_string(),
|
||||
Category::new(1, "_Dim").with_kind(CategoryKind::VirtualDim),
|
||||
);
|
||||
let mut m = Self {
|
||||
name,
|
||||
categories: IndexMap::new(),
|
||||
categories,
|
||||
data: DataStore::new(),
|
||||
formulas: Vec::new(),
|
||||
views,
|
||||
active_view: "Default".to_string(),
|
||||
next_category_id: 0,
|
||||
next_category_id: 2,
|
||||
measure_agg: HashMap::new(),
|
||||
};
|
||||
// Add virtuals to existing views (default view)
|
||||
for view in m.views.values_mut() {
|
||||
view.on_category_added("_Index");
|
||||
view.on_category_added("_Dim");
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
pub fn add_category(&mut self, name: impl Into<String>) -> Result<CategoryId> {
|
||||
let name = name.into();
|
||||
if self.categories.len() >= MAX_CATEGORIES {
|
||||
// Count only regular categories for the limit
|
||||
let regular_count = self
|
||||
.categories
|
||||
.values()
|
||||
.filter(|c| !c.kind.is_virtual())
|
||||
.count();
|
||||
if regular_count >= MAX_CATEGORIES {
|
||||
return Err(anyhow!("Maximum of {MAX_CATEGORIES} categories reached"));
|
||||
}
|
||||
if self.categories.contains_key(&name) {
|
||||
@ -157,7 +180,17 @@ impl Model {
|
||||
}
|
||||
|
||||
/// Return all category names
|
||||
/// Names of all regular (non-virtual) categories.
|
||||
pub fn category_names(&self) -> Vec<&str> {
|
||||
self.categories
|
||||
.iter()
|
||||
.filter(|(_, c)| !c.kind.is_virtual())
|
||||
.map(|(s, _)| s.as_str())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Names of all categories including virtual ones.
|
||||
pub fn all_category_names(&self) -> Vec<&str> {
|
||||
self.categories.keys().map(|s| s.as_str()).collect()
|
||||
}
|
||||
|
||||
@ -399,7 +432,7 @@ mod model_tests {
|
||||
let id1 = m.add_category("Region").unwrap();
|
||||
let id2 = m.add_category("Region").unwrap();
|
||||
assert_eq!(id1, id2);
|
||||
assert_eq!(m.categories.len(), 1);
|
||||
assert_eq!(m.category_names().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1367,12 +1400,12 @@ mod five_category {
|
||||
#[test]
|
||||
fn five_categories_well_within_limit() {
|
||||
let m = build_model();
|
||||
assert_eq!(m.categories.len(), 5);
|
||||
assert_eq!(m.category_names().len(), 5);
|
||||
let mut m2 = build_model();
|
||||
for i in 0..7 {
|
||||
m2.add_category(format!("Extra{i}")).unwrap();
|
||||
}
|
||||
assert_eq!(m2.categories.len(), 12);
|
||||
assert_eq!(m2.category_names().len(), 12);
|
||||
assert!(m2.add_category("OneMore").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user