Chart: bound the category axis by the point cap, not the raw cell count

Read only the cells the category levels use and cut the category dimension
at the per-series cap, so a multi-level axis keeps all its levels.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
vangel76andClaude Sonnet 5.5 committed 2026-11-07 17:31:27 +01:00
1 parent c35878948a
commit 6a7cf9cf66
2 files changed
+76 -5

No files matched your search

+31 -5
View File
@@ -189,8 +189,29 @@ fn value_text(v: &Value) -> String {
/// taller than a row of values) groups its innermost labels, year beside month. Each outer level
/// has one entry per category, non-empty where a group starts, as the cells are.
fn category_table(wb: &Workbook, sheet: usize, formula: &str, values: &str, limit: usize) -> (Vec<String>, Vec<Vec<String>>) {
let vals = eval_list_in(wb, sheet, formula, limit, 0);
let cells: Option<(usize, RangeRef)> = parse_ref(wb, sheet, formula);
let value_range = parse_ref(wb, sheet, values).map(|(_, r)| r);
// Bound the category dimension by `limit` and read only the cells the levels use (the
// innermost label line and up to three outer ones), so that every kept category keeps its
// cells in every level. Counting raw cells would cut a two-level axis in the middle.
let levels = cells.and_then(|(_, r)| category_levels(&r, value_range.as_ref()));
let (cells, read) = match (cells, levels) {
(Some((sh, r)), Some(lv)) => {
let cut = u32::try_from(limit.saturating_sub(1)).unwrap_or(u32::MAX);
let outer = lv.outer;
let end = if lv.by_columns { CellRef::new(r.end.row.min(r.start.row.saturating_add(cut)), r.end.col) } else { CellRef::new(r.end.row, r.end.col.min(r.start.col.saturating_add(cut))) };
let start = if lv.by_columns { CellRef::new(r.start.row, r.start.col.max(end.col.saturating_sub(outer))) } else { CellRef::new(r.start.row.max(end.row.saturating_sub(outer)), r.start.col) };
let r = RangeRef::new(start, end);
let prefix = formula.rfind('!').and_then(|i| formula.get(..=i)).unwrap_or("");
let cap = (r.width() as usize).saturating_mul(r.height() as usize);
(Some((sh, r)), Some((format!("{prefix}{r}"), cap)))
}
(c, _) => (c, None),
};
let vals = match &read {
Some((f, cap)) => eval_list_in(wb, sheet, f, *cap, 0),
None => eval_list_in(wb, sheet, formula, limit, 0),
};
let src = cells.and_then(|(sh, r)| wb.sheet(sh).map(|s| (s, r)));
let texts: Vec<String> = vals
.iter()
@@ -210,8 +231,7 @@ fn category_table(wb: &Workbook, sheet: usize, formula: &str, values: &str, limi
})
.collect();
let Some((_, r)) = src else { return (texts, vec![]) };
let value_range = parse_ref(wb, sheet, values).map(|(_, r)| r);
let Some(levels) = category_levels(&r, value_range.as_ref()) else { return (texts, vec![]) };
let Some(levels) = levels else { return (texts, vec![]) };
let (w, h) = (r.width().max(1) as usize, r.height().max(1) as usize);
if texts.len() != w.saturating_mul(h) {
return (texts, vec![]); // truncated or not a plain block: a flat list
@@ -252,7 +272,7 @@ fn series_name(wb: &Workbook, sheet: usize, name: &str) -> String {
t.to_string()
}
fn effective_kind(chart_kind: ChartKind, series_kind: Option<ChartKind>, i: usize) -> ChartKind {
pub(crate) fn effective_kind(chart_kind: ChartKind, series_kind: Option<ChartKind>, i: usize) -> ChartKind {
match (chart_kind, series_kind) {
(ChartKind::Combo, Some(k)) if k != ChartKind::Combo => k,
(ChartKind::Combo, _) => {
@@ -267,9 +287,15 @@ fn effective_kind(chart_kind: ChartKind, series_kind: Option<ChartKind>, i: usiz
}
}
/// The kind series `i` is drawn as: its own kind in a combo chart (first series a column, the
/// rest lines when it has none), else the chart's. The one rule shared by resolving and drawing.
pub(crate) fn plot_kind(chart_kind: ChartKind, series_kind: Option<ChartKind>, i: usize) -> ChartKind {
if chart_kind == ChartKind::Combo { effective_kind(chart_kind, series_kind, i) } else { chart_kind }
}
/// The kind series `i` of `chart` is drawn as: its own kind in a combo chart, else the chart's.
pub fn series_plot_kind(chart: &Chart, i: usize) -> ChartKind {
if chart.kind == ChartKind::Combo { effective_kind(chart.kind, chart.series.get(i).and_then(|s| s.kind), i) } else { chart.kind }
plot_kind(chart.kind, chart.series.get(i).and_then(|s| s.kind), i)
}
/// Evaluates a chart's series formulas: names, values, categories (default "1", "2", …), X
+45
View File
@@ -232,3 +232,48 @@ fn resolve_keeps_every_series_within_the_point_budget() {
let counts: Vec<usize> = resolve(&wb, 0, &c).series.iter().map(|s| s.trendlines.len()).collect();
assert_eq!(counts, vec![9, 7, 0, 0]);
}
/// 100 series lower the per-series cap to 20,000, below the number of category cells of a
/// two-level axis (#1155). Each kept category keeps its inner label and its outer level.
fn two_level_chart(n: u32, vertical: bool) -> ChartData {
let at = |i: u32, level: u32| if vertical { CellRef::new(i, level) } else { CellRef::new(level, i) };
let mut wb = Workbook::new();
if let Some(s) = wb.sheet_mut(0) {
for i in 0..n {
s.set_value(at(i, 0), Value::Text(format!("Y{}", i / 3).into()));
s.set_value(at(i, 1), Value::Text(format!("C{i}").into()));
s.set_value(at(i, 2), Value::Number(f64::from(i)));
}
}
let cats = format!("{}:{}", at(0, 0), at(n - 1, 1));
let values = format!("{}:{}", at(0, 2), at(n - 1, 2));
let mut c = chart(ChartKind::ColumnClustered);
c.series = (0..100).map(|_| Series { values: values.clone(), categories: Some(cats.clone()), ..Series::default() }).collect();
resolve(&wb, 0, &c)
}
#[test]
fn multi_level_categories_keep_their_levels_under_the_point_cap() {
for vertical in [true, false] {
let d = two_level_chart(15_000, vertical);
assert_eq!(d.series[0].values.len(), 15_000, "vertical {vertical}");
assert_eq!(d.categories.len(), 15_000, "vertical {vertical}");
assert_eq!(d.categories[0], "C0");
assert_eq!(d.categories[14_999], "C14999");
assert_eq!(d.category_levels.len(), 1, "one outer level, vertical {vertical}");
assert_eq!(d.category_levels[0].len(), 15_000);
assert_eq!(d.category_levels[0][3], "Y1");
}
}
#[test]
fn multi_level_categories_longer_than_the_cap_are_cut_together_with_their_levels() {
// 25,000 categories against a cap of 20,000.
let d = two_level_chart(25_000, true);
let cap = points_per_series(100);
assert_eq!(d.categories.len(), cap);
assert_eq!(d.category_levels.len(), 1);
assert_eq!(d.category_levels[0].len(), cap);
assert_eq!(d.categories[cap - 1], format!("C{}", cap - 1));
assert_eq!(d.category_levels[0][cap - 3], format!("Y{}", (cap - 3) / 3));
}