feat(import): add Label field support for high-cardinality per-row data
Add support for Label-kind categories to handle high-cardinality per-row fields like descriptions, IDs, and notes. These fields are stored alongside regular categories but default to Axis::None and are excluded from pivot category limits. Changes: - analyzer.rs: Label fields now default to accepted=true - wizard.rs: Collect and process label fields during model building, attaching label values as coordinates for each cell - category.rs: Add Label variant to CategoryKind enum - types.rs: Add add_label_category() method and update category counting to only include Regular-kind categories Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/Qwen3.5-35B-A3B-GGUF:Q5_K_M)
This commit is contained in:
@ -40,7 +40,7 @@ impl FieldProposal {
|
|||||||
FieldKind::Category => "Category (dimension)",
|
FieldKind::Category => "Category (dimension)",
|
||||||
FieldKind::Measure => "Measure (numeric)",
|
FieldKind::Measure => "Measure (numeric)",
|
||||||
FieldKind::TimeCategory => "Time Category",
|
FieldKind::TimeCategory => "Time Category",
|
||||||
FieldKind::Label => "Label/Identifier (skip)",
|
FieldKind::Label => "Label (per-row, drill-view only)",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -167,7 +167,7 @@ pub fn analyze_records(records: &[Value]) -> Vec<FieldProposal> {
|
|||||||
field,
|
field,
|
||||||
kind: FieldKind::Label,
|
kind: FieldKind::Label,
|
||||||
distinct_values: distinct_vec,
|
distinct_values: distinct_vec,
|
||||||
accepted: false,
|
accepted: true,
|
||||||
date_format: None,
|
date_format: None,
|
||||||
date_components: vec![],
|
date_components: vec![],
|
||||||
};
|
};
|
||||||
@ -178,7 +178,7 @@ pub fn analyze_records(records: &[Value]) -> Vec<FieldProposal> {
|
|||||||
field,
|
field,
|
||||||
kind: FieldKind::Label,
|
kind: FieldKind::Label,
|
||||||
distinct_values: vec![],
|
distinct_values: vec![],
|
||||||
accepted: false,
|
accepted: true,
|
||||||
date_format: None,
|
date_format: None,
|
||||||
date_components: vec![],
|
date_components: vec![],
|
||||||
}
|
}
|
||||||
|
|||||||
@ -94,6 +94,11 @@ impl ImportPipeline {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|p| p.accepted && p.kind == FieldKind::Measure)
|
.filter(|p| p.accepted && p.kind == FieldKind::Measure)
|
||||||
.collect();
|
.collect();
|
||||||
|
let labels: Vec<&FieldProposal> = self
|
||||||
|
.proposals
|
||||||
|
.iter()
|
||||||
|
.filter(|p| p.accepted && p.kind == FieldKind::Label)
|
||||||
|
.collect();
|
||||||
|
|
||||||
if categories.is_empty() {
|
if categories.is_empty() {
|
||||||
return Err(anyhow!("At least one category must be accepted"));
|
return Err(anyhow!("At least one category must be accepted"));
|
||||||
@ -139,6 +144,11 @@ impl ImportPipeline {
|
|||||||
model.add_category(derived_name)?;
|
model.add_category(derived_name)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create label categories (stored but not pivoted by default)
|
||||||
|
for lab in &labels {
|
||||||
|
model.add_label_category(&lab.field)?;
|
||||||
|
}
|
||||||
|
|
||||||
if !measures.is_empty() {
|
if !measures.is_empty() {
|
||||||
model.add_category("Measure")?;
|
model.add_category("Measure")?;
|
||||||
if let Some(cat) = model.category_mut("Measure") {
|
if let Some(cat) = model.category_mut("Measure") {
|
||||||
@ -187,6 +197,28 @@ impl ImportPipeline {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attach label values as coords (missing labels become "").
|
||||||
|
for lab in &labels {
|
||||||
|
let val = map
|
||||||
|
.get(&lab.field)
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.or_else(|| {
|
||||||
|
map.get(&lab.field).and_then(|v| {
|
||||||
|
if v.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(v.to_string())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
if let Some(cat) = model.category_mut(&lab.field) {
|
||||||
|
cat.add_item(&val);
|
||||||
|
}
|
||||||
|
coords.push((lab.field.clone(), val));
|
||||||
|
}
|
||||||
|
|
||||||
for measure in &measures {
|
for measure in &measures {
|
||||||
if let Some(val) = map.get(&measure.field).and_then(|v| v.as_f64()) {
|
if let Some(val) = map.get(&measure.field).and_then(|v| v.as_f64()) {
|
||||||
let mut cell_coords = coords.clone();
|
let mut cell_coords = coords.clone();
|
||||||
|
|||||||
@ -59,11 +59,18 @@ pub enum CategoryKind {
|
|||||||
VirtualIndex,
|
VirtualIndex,
|
||||||
/// Items are the names of all regular categories + "Value".
|
/// Items are the names of all regular categories + "Value".
|
||||||
VirtualDim,
|
VirtualDim,
|
||||||
|
/// High-cardinality per-row field (description, id, note). Stored
|
||||||
|
/// alongside the data so it shows up in record/drill views, but
|
||||||
|
/// defaults to Axis::None and is excluded from pivot limits and the
|
||||||
|
/// auto Row/Column axis assignment.
|
||||||
|
Label,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CategoryKind {
|
impl CategoryKind {
|
||||||
pub fn is_virtual(&self) -> bool {
|
/// True for user-managed pivot dimensions (what the category
|
||||||
!matches!(self, CategoryKind::Regular)
|
/// count limit and auto axis assignment apply to).
|
||||||
|
pub fn is_regular(&self) -> bool {
|
||||||
|
matches!(self, CategoryKind::Regular)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -63,11 +63,11 @@ impl Model {
|
|||||||
|
|
||||||
pub fn add_category(&mut self, name: impl Into<String>) -> Result<CategoryId> {
|
pub fn add_category(&mut self, name: impl Into<String>) -> Result<CategoryId> {
|
||||||
let name = name.into();
|
let name = name.into();
|
||||||
// Virtuals don't count against the regular category limit
|
// Only regular pivot categories count against the limit.
|
||||||
let regular_count = self
|
let regular_count = self
|
||||||
.categories
|
.categories
|
||||||
.values()
|
.values()
|
||||||
.filter(|c| !c.kind.is_virtual())
|
.filter(|c| c.kind.is_regular())
|
||||||
.count();
|
.count();
|
||||||
if regular_count >= MAX_CATEGORIES {
|
if regular_count >= MAX_CATEGORIES {
|
||||||
return Err(anyhow!("Maximum of {MAX_CATEGORIES} categories reached"));
|
return Err(anyhow!("Maximum of {MAX_CATEGORIES} categories reached"));
|
||||||
@ -86,6 +86,27 @@ impl Model {
|
|||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a Label-kind category: stored alongside regular categories so
|
||||||
|
/// records views can display it, but default to `Axis::None` and
|
||||||
|
/// excluded from the pivot-category count limit.
|
||||||
|
pub fn add_label_category(&mut self, name: impl Into<String>) -> Result<CategoryId> {
|
||||||
|
use crate::model::category::CategoryKind;
|
||||||
|
use crate::view::Axis;
|
||||||
|
let name = name.into();
|
||||||
|
if self.categories.contains_key(&name) {
|
||||||
|
return Ok(self.categories[&name].id);
|
||||||
|
}
|
||||||
|
let id = self.next_category_id;
|
||||||
|
self.next_category_id += 1;
|
||||||
|
let cat = Category::new(id, name.clone()).with_kind(CategoryKind::Label);
|
||||||
|
self.categories.insert(name.clone(), cat);
|
||||||
|
for view in self.views.values_mut() {
|
||||||
|
view.on_category_added(&name);
|
||||||
|
view.set_axis(&name, Axis::None);
|
||||||
|
}
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn category_mut(&mut self, name: &str) -> Option<&mut Category> {
|
pub fn category_mut(&mut self, name: &str) -> Option<&mut Category> {
|
||||||
self.categories.get_mut(name)
|
self.categories.get_mut(name)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user