Skip to main content

ravenrs_sheet/engine/pivot/
compute.rs

1use super::items::{Acc, FieldItems, build_tree, collect_siblings, is_collapsed, item_label};
2use super::output::{AggMap, CellKind, ColKind, ColSpec, Ctx, Grid, Line, LineKind, Output, RowKind, ValSrc, data_cols_exceed_sheet, flatten_cols};
3use super::{
4    CalcSet, CellError, CellRef, HashMap, HashSet, MAX_AXIS_FIELDS, MAX_COLS, MAX_VALUE_FIELDS, PivotDateGroup, PivotField, PivotFilter, PivotFunc,
5    PivotLayout, PivotShowAs, PivotTable, PivotValue, Source, Value, Workbook, read_source,
6};
7
8/// Why Show Details turns a cell down when it isn't a value cell of a PivotTable (the front end
9/// edits that cell instead).
10pub const NOT_A_VALUE_CELL: &str = "select a value cell in a PivotTable";
11
12/// Show Details: the source list and the indices of its records behind the value cell at
13/// (`row`, `col`) of the report grid (relative to its top-left), in source order. `None` when
14/// that cell holds no value (a label, a header, a blank). An error, for any cell, when the report
15/// on the sheet is no longer what the source computes: the cell's records would be another cell's.
16pub fn detail_records(wb: &Workbook, pt: &PivotTable, pivot_sheet: usize, row: u32, col: u32) -> Result<Option<(Source, Vec<usize>)>, String> {
17    let src = read_source(wb, &pt.source, pivot_sheet)?;
18    let mut probe = Probe { at: Some((row as usize, col as usize)), hit: None, paths: Some(Vec::new()), cells: None };
19    let out = compute_from(wb, pt, &src, Some(&mut probe))?;
20    // Checked first: in a stale report even a cell that computes as a label may show a value.
21    check_current(wb, pt, pivot_sheet, &out)?;
22    let Some((rp, cp)) = probe.hit else { return Ok(None) };
23    let records =
24        probe.paths.unwrap_or_default().into_iter().filter(|(_, r, c)| r.starts_with(&rp) && c.starts_with(&cp)).map(|(i, _, _)| i).collect();
25    Ok(Some((src, records)))
26}
27
28/// The cells of the report grid (relative to its top-left) where [`detail_records`] finds
29/// records: its value, subtotal and grand total cells, from one computation of the report. The
30/// same errors as [`detail_records`].
31pub fn detail_cells(wb: &Workbook, pt: &PivotTable, pivot_sheet: usize) -> Result<HashSet<(u32, u32)>, String> {
32    let src = read_source(wb, &pt.source, pivot_sheet)?;
33    let mut probe = Probe { at: None, hit: None, paths: None, cells: Some(HashSet::new()) };
34    let out = compute_from(wb, pt, &src, Some(&mut probe))?;
35    check_current(wb, pt, pivot_sheet, &out)?;
36    Ok(probe.cells.unwrap_or_default().into_iter().map(|(r, c)| (r as u32, c as u32)).collect())
37}
38
39/// An error when the report on the sheet is not what `out` computes. Excel reads its PivotTable
40/// cache; without one, the report written by the last refresh must still be what the source gives.
41fn check_current(wb: &Workbook, pt: &PivotTable, pivot_sheet: usize, out: &Output) -> Result<(), String> {
42    let sh = wb.sheet(pivot_sheet).ok_or("There's no such sheet.")?;
43    let current = pt.last_range.is_some_and(|r| {
44        r.height() == out.height().max(1)
45            && r.width() == out.width.max(1)
46            && out.grid.iter().enumerate().all(|(ri, line)| {
47                line.iter().enumerate().all(|(ci, oc)| {
48                    sh.value_ref(CellRef::new(r.start.row + ri as u32, r.start.col + ci as u32)).unwrap_or(&Value::Empty) == &oc.value
49                })
50            })
51    });
52    if !current {
53        return Err("The PivotTable report is out of date. Refresh it first.".into());
54    }
55    Ok(())
56}
57
58type ProbePath = (usize, Vec<u32>, Vec<u32>);
59
60/// What [`detail_records`] and [`detail_cells`] ask of a report computation: the row and column
61/// item paths of the value cell at `at`, the paths of every record the filters let through (when
62/// `paths` is set), and every value cell (when `cells` is set).
63pub(super) struct Probe {
64    at: Option<(usize, usize)>,
65    hit: Option<(Vec<u32>, Vec<u32>)>,
66    paths: Option<Vec<ProbePath>>,
67    cells: Option<HashSet<(usize, usize)>>,
68}
69
70struct AggregateInput<'a> {
71    wb: &'a Workbook,
72    pt: &'a PivotTable,
73    src: &'a Source,
74    rows: &'a [(usize, &'a PivotField)],
75    cols: &'a [(usize, &'a PivotField)],
76    vals: &'a [(ValSrc, &'a PivotValue)],
77    extra: &'a [usize],
78    filters: &'a [(usize, &'a PivotFilter, PivotDateGroup, bool)],
79    filter_fmts: &'a [String],
80    /// Each filter's selected labels, ASCII-lowercased, so a record's test is one probe.
81    filter_sets: &'a [Option<HashSet<String>>],
82    row_fmts: &'a [String],
83    col_fmts: &'a [String],
84    empty: bool,
85}
86
87#[derive(Default)]
88struct AggregateData {
89    row_items: Vec<FieldItems>,
90    col_items: Vec<FieldItems>,
91    aggs: AggMap,
92    row_set: HashSet<Vec<u32>>,
93    col_set: HashSet<Vec<u32>>,
94    col_leaves: HashSet<Vec<u32>>,
95}
96
97fn aggregate_record(
98    input: &AggregateInput<'_>,
99    data: &mut AggregateData,
100    probe: &mut Option<&mut Probe>,
101    ri: usize,
102    rec: &[Value],
103) -> Result<(), String> {
104    for (((c, _, group, _), fmt), selected) in input.filters.iter().zip(input.filter_fmts).zip(input.filter_sets) {
105        if let Some(selected) = selected {
106            let label = item_label(input.wb, rec.get(*c).unwrap_or(&Value::Empty), *group, fmt);
107            if !selected.contains(&label.to_ascii_lowercase()) {
108                return Ok(());
109            }
110        }
111    }
112    if input.empty {
113        return Ok(());
114    }
115    let mut row_path = Vec::with_capacity(input.rows.len());
116    for (((column, field), items), fmt) in input.rows.iter().zip(data.row_items.iter_mut()).zip(input.row_fmts) {
117        row_path.push(items.intern(input.wb, rec.get(*column).unwrap_or(&Value::Empty), field.date_group, fmt, ri));
118    }
119    let mut col_path = Vec::with_capacity(input.cols.len());
120    for (((column, field), items), fmt) in input.cols.iter().zip(data.col_items.iter_mut()).zip(input.col_fmts) {
121        col_path.push(items.intern(input.wb, rec.get(*column).unwrap_or(&Value::Empty), field.date_group, fmt, ri));
122    }
123    for row_depth in 0..=row_path.len() {
124        for col_depth in 0..=col_path.len() {
125            let accs = data
126                .aggs
127                .entry((row_path[..row_depth].to_vec(), col_path[..col_depth].to_vec()))
128                .or_insert_with(|| vec![Acc::default(); input.vals.len() + input.extra.len()]);
129            for (acc, (source, _)) in accs.iter_mut().zip(input.vals) {
130                if let ValSrc::Col(column) = source {
131                    acc.add(rec.get(*column).unwrap_or(&Value::Empty));
132                }
133            }
134            for (acc, column) in accs.iter_mut().skip(input.vals.len()).zip(input.extra) {
135                acc.add(rec.get(*column).unwrap_or(&Value::Empty));
136            }
137        }
138    }
139    let shown_depth = input
140        .cols
141        .iter()
142        .zip(data.col_items.iter())
143        .zip(col_path.iter())
144        .position(|(((_, field), items), &item)| !field.collapsed_items.is_empty() && is_collapsed(field, &items.label(item)))
145        .map_or(col_path.len(), |depth| depth + 1);
146    data.col_leaves.insert(col_path[..shown_depth].to_vec());
147    if let Some(paths) = probe.as_deref_mut().and_then(|value| value.paths.as_mut()) {
148        paths.push((ri, row_path.clone(), col_path.clone()));
149    }
150    data.row_set.insert(row_path);
151    data.col_set.insert(col_path);
152    if !input.cols.is_empty() && data_cols_exceed_sheet(data.col_leaves.len(), input.pt.grand_totals_rows, input.vals.len()) {
153        return Err("The PivotTable report will not fit on the sheet.".into());
154    }
155    if data.aggs.len() > 5_000_000 {
156        return Err("The PivotTable report is too large.".into());
157    }
158    Ok(())
159}
160
161fn aggregate_source(input: AggregateInput<'_>, probe: &mut Option<&mut Probe>) -> Result<AggregateData, String> {
162    let mut data = AggregateData {
163        row_items: input.rows.iter().map(|_| FieldItems::default()).collect(),
164        col_items: input.cols.iter().map(|_| FieldItems::default()).collect(),
165        ..AggregateData::default()
166    };
167    for (ri, record) in input.src.rows.iter().enumerate() {
168        aggregate_record(&input, &mut data, probe, ri, record)?;
169    }
170    Ok(data)
171}
172
173struct HeaderInput<'a> {
174    vals: &'a [(ValSrc, &'a PivotValue)],
175    row_fields: &'a [&'a PivotField],
176    col_fields: &'a [&'a PivotField],
177    col_items: &'a [FieldItems],
178    col_fmts: &'a [String],
179    vpos: Option<usize>,
180    data_cols: &'a [(usize, Option<usize>)],
181    specs: &'a [ColSpec],
182    row_levels: usize,
183    compact: bool,
184    on_rows: bool,
185    k: usize,
186    l: usize,
187    headers_on: bool,
188}
189
190fn write_headers(g: &mut Grid, input: HeaderInput<'_>) -> Result<(), String> {
191    let HeaderInput { vals, row_fields, col_fields, col_items, col_fmts, vpos, data_cols, specs, row_levels, compact, on_rows, k, l, headers_on } =
192        input;
193    let val_name = |vi: usize| vals.get(vi).map(|(_, v)| v.name.clone()).unwrap_or_default();
194    let row_header = |g: &mut Grid, r: usize| {
195        if row_levels == 0 || !headers_on {
196            return;
197        }
198        if compact {
199            g.set(r, 0, Value::text("Row Labels"), CellKind::Header, 0, None);
200        } else {
201            // One column per row field, Σ Values ("Values") at its place.
202            let mut names: Vec<&str> = row_fields.iter().map(|f| f.source_col.as_str()).collect();
203            if let Some(p) = vpos {
204                names.insert(p.min(names.len()), "Values");
205            }
206            for (i, name) in names.into_iter().enumerate() {
207                g.set(r, i, Value::text(name), CellKind::Header, 0, None);
208            }
209        }
210    };
211
212    // ---- header rows
213    if col_fields.is_empty() {
214        let r = g.push_row(RowKind::Header)?;
215        row_header(g, r);
216        for (j, (_, vi)) in data_cols.iter().enumerate() {
217            match vi {
218                Some(vi) => g.set(r, l + j, Value::text(val_name(*vi)), CellKind::Header, 0, None),
219                // Values on rows, no column field: the one data column is the grand total.
220                None if on_rows => g.set(r, l + j, Value::text("Total"), CellKind::Header, 0, None),
221                None => {}
222            }
223        }
224    } else {
225        let cap = g.push_row(RowKind::Header)?;
226        if l > 0 && k == 1 {
227            g.set(cap, 0, Value::text(val_name(0)), CellKind::Header, 0, None);
228        }
229        if headers_on {
230            let caption = if compact { "Column Labels".to_string() } else { col_fields.first().map(|f| f.source_col.clone()).unwrap_or_default() };
231            g.set(cap, l, Value::text(caption), CellKind::Header, 0, None);
232        }
233        let levels = col_fields.len() + usize::from(k > 1 && !on_rows);
234        let first_level = g.rows.len();
235        for _ in 0..levels {
236            g.push_row(RowKind::Header)?;
237        }
238        let mut prev: Option<&ColSpec> = None;
239        for (j, (si, vi)) in data_cols.iter().enumerate() {
240            let Some(spec) = specs.get(*si) else { continue };
241            let c = l + j;
242            match spec.kind {
243                ColKind::Leaf => {
244                    for (lv, &it) in spec.path.iter().enumerate() {
245                        let same = prev.is_some_and(|p| p.kind != ColKind::Grand && p.path.get(..=lv) == spec.path.get(..=lv));
246                        if !same {
247                            let Some(item) = col_items.get(lv).and_then(|fi| fi.get(it)) else { continue };
248                            let fmt = if item.formatted { col_fmts.get(lv).cloned() } else { None };
249                            g.set(first_level + lv, c, item.value.clone(), CellKind::ColLabel, 0, fmt);
250                        }
251                    }
252                    if k > 1
253                        && let Some(vi) = vi
254                    {
255                        g.set(first_level + col_fields.len(), c, Value::text(val_name(*vi)), CellKind::ColLabel, 0, None);
256                    }
257                }
258                ColKind::Subtotal => {
259                    let lv = spec.path.len().saturating_sub(1);
260                    let label = spec.path.last().and_then(|it| col_items.get(lv).map(|fi| fi.label(*it))).unwrap_or_default();
261                    let text = match vi {
262                        Some(vi) if k > 1 => format!("{label} {}", val_name(*vi)),
263                        _ => format!("{label} Total"),
264                    };
265                    g.set(first_level + lv, c, Value::text(text), CellKind::ColLabel, 0, None);
266                }
267                ColKind::Grand => {
268                    let text = match vi {
269                        Some(vi) if k > 1 => format!("Total {}", val_name(*vi)),
270                        _ => "Grand Total".to_string(),
271                    };
272                    g.set(first_level, c, Value::text(text), CellKind::ColLabel, 0, None);
273                }
274            }
275            prev = Some(spec);
276        }
277        let last = g.rows.len() - 1;
278        row_header(g, last);
279    }
280
281    Ok(())
282}
283
284struct ValueLookup<'a> {
285    wb: &'a Workbook,
286    source: &'a Source,
287    calc: &'a CalcSet,
288    vals: &'a [(ValSrc, &'a PivotValue)],
289    extra: &'a [usize],
290    aggs: &'a AggMap,
291    value_count: usize,
292}
293
294impl ValueLookup<'_> {
295    fn get(&self, row_path: &[u32], col_path: &[u32], value_index: usize) -> Option<Value> {
296        let (source, definition) = self.vals.get(value_index)?;
297        let accumulators = self.aggs.get(&(row_path.to_vec(), col_path.to_vec()))?;
298        match source {
299            ValSrc::Col(_) => accumulators.get(value_index).map(|acc| acc.result(definition.func)),
300            ValSrc::Calc(index) => {
301                let sum = |name: &str| -> Value {
302                    self.source
303                        .col(name)
304                        .and_then(|column| self.extra.iter().position(|extra_column| *extra_column == column))
305                        .and_then(|index| accumulators.get(self.value_count + index))
306                        .map(|acc| acc.result(PivotFunc::Sum))
307                        .unwrap_or(Value::Number(0.0))
308                };
309                Some(self.calc.value(self.wb, *index, &sum))
310            }
311        }
312    }
313}
314
315struct ShowContext<'a> {
316    row_path: &'a [u32],
317    col_path: &'a [u32],
318    value_index: usize,
319    level0: &'a [u32],
320    siblings: &'a HashMap<Vec<u32>, Vec<u32>>,
321}
322
323fn number(value: &Option<Value>) -> Option<f64> {
324    value.as_ref().and_then(Value::as_f64)
325}
326
327fn ratio(a: Value, b: Option<Value>) -> Value {
328    match (a, b) {
329        (Value::Error(error), _) => Value::Error(error),
330        (_, Some(Value::Error(error))) => Value::Error(error),
331        (Value::Number(_), Some(Value::Number(0.0))) => Value::Error(CellError::Div0),
332        (Value::Number(x), Some(Value::Number(y))) => Value::number(x / y),
333        (value, _) => value,
334    }
335}
336
337fn running_total(raw: Value, lookup: &ValueLookup<'_>, context: &ShowContext<'_>) -> Value {
338    let Some((first, _)) = context.row_path.split_first() else { return raw };
339    if !matches!(raw, Value::Number(_)) {
340        return raw;
341    }
342    let mut total = 0.0;
343    for item in context.level0 {
344        let mut path = context.row_path.to_vec();
345        if let Some(first_item) = path.first_mut() {
346            *first_item = *item;
347        }
348        total += number(&lookup.get(&path, context.col_path, context.value_index)).unwrap_or(0.0);
349        if item == first {
350            break;
351        }
352    }
353    Value::number(total)
354}
355
356fn ranked_value(raw: Value, lookup: &ValueLookup<'_>, context: &ShowContext<'_>) -> Value {
357    let Some((_, parent)) = context.row_path.split_last() else { return Value::Empty };
358    let Some(me) = raw.as_f64() else { return raw };
359    let siblings = context.siblings.get(parent).cloned().unwrap_or_default();
360    // Excel's ranks are dense: ties share a rank and the next value takes the next one
361    // (5, 4, 3, 3, 4 rank 1, 2, 3, 3, 2), so count the distinct larger values.
362    let mut larger: Vec<f64> = siblings
363        .into_iter()
364        .filter_map(|sibling| {
365            let mut path = parent.to_vec();
366            path.push(sibling);
367            number(&lookup.get(&path, context.col_path, context.value_index)).filter(|value| *value > me)
368        })
369        .collect();
370    larger.sort_by(f64::total_cmp);
371    larger.dedup();
372    Value::number(larger.len() as f64 + 1.0)
373}
374
375fn show_value(definition: &PivotValue, raw: Value, lookup: &ValueLookup<'_>, context: &ShowContext<'_>) -> Value {
376    match definition.show_as {
377        PivotShowAs::Normal => raw,
378        PivotShowAs::PercentOfGrandTotal => ratio(raw, lookup.get(&[], &[], context.value_index)),
379        PivotShowAs::PercentOfColumnTotal => ratio(raw, lookup.get(&[], context.col_path, context.value_index)),
380        PivotShowAs::PercentOfRowTotal => ratio(raw, lookup.get(context.row_path, &[], context.value_index)),
381        PivotShowAs::RunningTotal => running_total(raw, lookup, context),
382        PivotShowAs::Rank => ranked_value(raw, lookup, context),
383    }
384}
385
386type ReportFilter<'a> = (usize, &'a PivotFilter, PivotDateGroup, bool);
387
388fn report_filters<'a>(pt: &'a PivotTable, src: &Source) -> (Vec<ReportFilter<'a>>, Vec<ReportFilter<'a>>) {
389    let axis_group =
390        |name: &str| pt.rows.iter().chain(pt.columns.iter()).find(|field| field.source_col.eq_ignore_ascii_case(name)).map(|field| field.date_group);
391    let filters: Vec<_> = pt
392        .filters
393        .iter()
394        .filter_map(|filter| {
395            let column = src.col(&filter.source_col)?;
396            let group = axis_group(&filter.source_col);
397            Some((column, filter, group.unwrap_or_default(), group.is_none()))
398        })
399        .collect();
400    let shown = filters.iter().copied().filter(|filter| filter.3).collect();
401    (filters, shown)
402}
403
404fn values_position(pt: &PivotTable, src: &Source, value_count: usize) -> Option<usize> {
405    if !pt.values_on_rows || value_count <= 1 {
406        return None;
407    }
408    let position = pt.values_position.unwrap_or(pt.rows.len());
409    Some(pt.rows.iter().take(position).filter(|field| src.col(&field.source_col).is_some()).count())
410}
411
412fn calculated_source_columns(vals: &[(ValSrc, &PivotValue)], calc: &CalcSet, src: &Source) -> Vec<usize> {
413    let mut columns = Vec::new();
414    for (source, _) in vals {
415        let ValSrc::Calc(index) = source else { continue };
416        for name in calc.source_refs(*index) {
417            if let Some(column) = src.col(&name)
418                && !columns.contains(&column)
419            {
420                columns.push(column);
421            }
422        }
423    }
424    columns
425}
426
427fn data_columns(specs: &[ColSpec], has_column_fields: bool, on_rows: bool, value_count: usize) -> Result<Vec<(usize, Option<usize>)>, String> {
428    let mut columns = Vec::new();
429    for (spec_index, _) in specs.iter().enumerate() {
430        if on_rows {
431            columns.push((spec_index, None));
432        } else if value_count == 0 {
433            if has_column_fields {
434                columns.push((spec_index, None));
435            }
436        } else {
437            columns.extend((0..value_count).map(|value_index| (spec_index, Some(value_index))));
438        }
439        if columns.len() > MAX_COLS as usize {
440            return Err("The PivotTable report will not fit on the sheet.".into());
441        }
442    }
443    Ok(columns)
444}
445
446pub(super) fn compute_from(wb: &Workbook, pt: &PivotTable, src: &Source, mut probe: Option<&mut Probe>) -> Result<Output, String> {
447    let fmt_of = |c: usize| src.formats.get(c).cloned().unwrap_or_else(|| "General".into());
448    let rows: Vec<(usize, &PivotField)> = pt.rows.iter().filter_map(|f| src.col(&f.source_col).map(|c| (c, f))).collect();
449    let cols: Vec<(usize, &PivotField)> = pt.columns.iter().filter_map(|f| src.col(&f.source_col).map(|c| (c, f))).collect();
450    // Value fields: a source column, or a calculated field evaluated on sums (see `pivot_calc`).
451    let calc = CalcSet::new(pt, &src.headers);
452    let vals: Vec<(ValSrc, &PivotValue)> = pt
453        .values
454        .iter()
455        .filter_map(|v| match src.col(&v.source_col) {
456            Some(c) => Some((ValSrc::Col(c), v)),
457            None => calc.index(&v.source_col).map(|i| (ValSrc::Calc(i), v)),
458        })
459        .collect();
460    // Source columns summed for the calculated fields, accumulated after the value fields.
461    let extra = calculated_source_columns(&vals, &calc, src);
462    if rows.len() + cols.len() > MAX_AXIS_FIELDS {
463        return Err(format!("A PivotTable can have at most {MAX_AXIS_FIELDS} row and column fields."));
464    }
465    if vals.len() > MAX_VALUE_FIELDS {
466        return Err(format!("A PivotTable can have at most {MAX_VALUE_FIELDS} value fields."));
467    }
468    let (filters, shown_filters) = report_filters(pt, src);
469
470    let k = vals.len();
471    let empty = rows.is_empty() && cols.is_empty() && vals.is_empty();
472    // Σ Values in Rows (it exists with two or more value fields): its depth among the row fields
473    // found in the source.
474    let vpos = values_position(pt, src, k);
475    let on_rows = vpos.is_some();
476    let row_levels = rows.len() + usize::from(on_rows);
477
478    // ---- aggregate
479    let filter_fmts: Vec<String> = filters.iter().map(|f| fmt_of(f.0)).collect();
480    let filter_sets: Vec<Option<HashSet<String>>> =
481        filters.iter().map(|(_, field, _, _)| field.selected.as_ref().map(|s| s.iter().map(|v| v.to_ascii_lowercase()).collect())).collect();
482    let row_fmts: Vec<String> = rows.iter().map(|(c, _)| fmt_of(*c)).collect();
483    let col_fmts: Vec<String> = cols.iter().map(|(c, _)| fmt_of(*c)).collect();
484    let aggregate = aggregate_source(
485        AggregateInput {
486            wb,
487            pt,
488            src,
489            rows: &rows,
490            cols: &cols,
491            vals: &vals,
492            extra: &extra,
493            filters: &filters,
494            filter_fmts: &filter_fmts,
495            filter_sets: &filter_sets,
496            row_fmts: &row_fmts,
497            col_fmts: &col_fmts,
498            empty,
499        },
500        &mut probe,
501    )?;
502    let AggregateData { row_items, col_items, aggs, row_set, col_set, col_leaves: _ } = aggregate;
503
504    // ---- axes
505    let row_fields: Vec<&PivotField> = rows.iter().map(|(_, f)| *f).collect();
506    let col_fields: Vec<&PivotField> = cols.iter().map(|(_, f)| *f).collect();
507    let row_paths: Vec<&[u32]> = row_set.iter().map(Vec::as_slice).collect();
508    let col_paths: Vec<&[u32]> = col_set.iter().map(Vec::as_slice).collect();
509    let row_tree = build_tree(&row_paths, 0, &row_fields, &row_items);
510    let col_tree = build_tree(&col_paths, 0, &col_fields, &col_items);
511    let mut siblings: HashMap<Vec<u32>, Vec<u32>> = HashMap::new();
512    collect_siblings(&row_tree, &mut Vec::new(), &mut siblings);
513    let level0: Vec<u32> = row_tree.iter().map(|n| n.item).collect();
514
515    let val_names: Vec<String> = vals.iter().map(|(_, v)| v.name.clone()).collect();
516    let ctx = Ctx { pt, rows: row_fields.clone(), row_items, row_fmts: row_fmts.clone(), vpos, val_names: val_names.clone() };
517    let mut lines: Vec<Line> = Vec::new();
518    if row_levels == 0 {
519        lines.push(Line { kind: LineKind::Item(false), leaf: true, path: vec![], values: true, labels: vec![], vi: None });
520    } else {
521        if pt.layout == PivotLayout::Tabular {
522            ctx.flatten_tabular(&row_tree, 0, None, &mut Vec::new(), &[], &mut lines);
523        } else {
524            ctx.flatten(&row_tree, 0, None, &mut Vec::new(), &mut lines);
525        }
526        // With only Σ Values in Rows its items are the totals already.
527        if pt.grand_totals_cols && !rows.is_empty() {
528            let grand = |text: String, vi: Option<usize>| Line {
529                kind: LineKind::Grand,
530                leaf: false,
531                path: vec![],
532                values: true,
533                labels: vec![(0, Value::text(text), 0, None)],
534                vi,
535            };
536            if on_rows {
537                lines.extend(val_names.iter().enumerate().map(|(vi, name)| grand(format!("Total {name}"), Some(vi))));
538            } else {
539                lines.push(grand("Grand Total".into(), None));
540            }
541        }
542    }
543    let mut specs: Vec<ColSpec> = Vec::new();
544    if cols.is_empty() {
545        specs.push(ColSpec { kind: ColKind::Leaf, path: vec![] });
546    } else {
547        flatten_cols(&col_tree, 0, &col_fields, &col_items, pt.subtotals, &mut Vec::new(), &mut specs);
548        if pt.grand_totals_rows {
549            specs.push(ColSpec { kind: ColKind::Grand, path: vec![] });
550        }
551    }
552    let data_cols = data_columns(&specs, !cols.is_empty(), on_rows, k)?;
553
554    let compact = pt.layout == PivotLayout::Compact;
555    let l = if row_levels == 0 && cols.is_empty() {
556        0
557    } else if compact {
558        1
559    } else {
560        row_levels.max(1)
561    };
562    let mut width = l + data_cols.len();
563    if !shown_filters.is_empty() {
564        width = width.max(2);
565    }
566    if empty {
567        width = width.max(1);
568    }
569    let width = width.max(1);
570    let mut g = Grid { rows: vec![], kinds: vec![], width };
571
572    // ---- report filters
573    for (c, f, _, _) in &shown_filters {
574        let r = g.push_row(RowKind::Filter)?;
575        let name = src.headers.get(*c).cloned().unwrap_or_default();
576        let shown = match &f.selected {
577            None => "(All)".to_string(),
578            Some(v) if v.len() == 1 => v.first().cloned().unwrap_or_default(),
579            Some(_) => "(Multiple Items)".to_string(),
580        };
581        g.set(r, 0, Value::text(name), CellKind::Filter, 0, None);
582        g.set(r, 1, Value::text(shown), CellKind::Filter, 0, None);
583    }
584    if !shown_filters.is_empty() {
585        g.push_row(RowKind::Blank)?;
586    }
587    let body_offset = g.rows.len() as u32;
588
589    if empty {
590        let r = g.push_row(RowKind::Placeholder)?;
591        g.set(r, 0, Value::text(format!("{} — choose fields", pt.name)), CellKind::Placeholder, 0, None);
592        return Ok(Output {
593            width: g.width as u32,
594            grid: g.rows,
595            row_kinds: g.kinds,
596            body_offset,
597            chart_rows: vec![],
598            chart_cols: vec![],
599            label_col: None,
600        });
601    }
602
603    write_headers(
604        &mut g,
605        HeaderInput {
606            vals: &vals,
607            row_fields: &row_fields,
608            col_fields: &col_fields,
609            col_items: &col_items,
610            col_fmts: &col_fmts,
611            vpos,
612            data_cols: &data_cols,
613            specs: &specs,
614            row_levels,
615            compact,
616            on_rows,
617            k,
618            l,
619            headers_on: pt.show_headers,
620        },
621    )?;
622    let val_name = |vi: usize| vals.get(vi).map(|(_, value)| value.name.clone()).unwrap_or_default();
623
624    // ---- body
625    let data_fmt: Vec<Option<String>> = vals
626        .iter()
627        .map(|(vs, v)| match v.show_as {
628            PivotShowAs::PercentOfGrandTotal | PivotShowAs::PercentOfColumnTotal | PivotShowAs::PercentOfRowTotal => {
629                Some(v.number_format.clone().unwrap_or_else(|| "0.00%".into()))
630            }
631            PivotShowAs::Rank => v.number_format.clone(),
632            _ => v.number_format.clone().or_else(|| match v.func {
633                PivotFunc::Count | PivotFunc::CountNumbers => None,
634                _ => {
635                    let f = match vs {
636                        ValSrc::Col(c) => fmt_of(*c),
637                        ValSrc::Calc(_) => "General".to_string(),
638                    };
639                    (f != "General").then_some(f)
640                }
641            }),
642        })
643        .collect();
644    let lookup = ValueLookup { wb, source: src, calc: &calc, vals: &vals, extra: &extra, aggs: &aggs, value_count: k };
645    let mut chart_rows: Vec<u32> = Vec::new();
646    let chart_cols: Vec<(u32, String)> = data_cols
647        .iter()
648        .enumerate()
649        .filter_map(|(j, (si, vi))| {
650            let spec = specs.get(*si)?;
651            if spec.kind != ColKind::Leaf {
652                return None;
653            }
654            let mut parts: Vec<String> =
655                spec.path.iter().enumerate().map(|(lv, it)| col_items.get(lv).map(|fi| fi.label(*it)).unwrap_or_default()).collect();
656            if let Some(vi) = vi
657                && (k > 1 || parts.is_empty())
658            {
659                parts.push(val_name(*vi));
660            } else if parts.is_empty() {
661                parts.push("Total".into());
662            }
663            Some(((l + j) as u32, parts.join(" - ")))
664        })
665        .filter(|_| k > 0)
666        .collect();
667    let label_col = if row_levels == 0 {
668        None
669    } else if compact {
670        Some(0)
671    } else {
672        Some(row_levels.saturating_sub(1) as u32)
673    };
674    for line in &lines {
675        let rk = match line.kind {
676            LineKind::Item(b) => RowKind::Item(b),
677            LineKind::Subtotal => RowKind::Subtotal,
678            LineKind::Grand => RowKind::Grand,
679        };
680        let ck = match line.kind {
681            LineKind::Item(_) => (CellKind::RowLabel, CellKind::Data),
682            LineKind::Subtotal => (CellKind::Subtotal, CellKind::Subtotal),
683            LineKind::Grand => (CellKind::GrandTotal, CellKind::GrandTotal),
684        };
685        let r = g.push_row(rk)?;
686        if line.leaf && k > 0 {
687            chart_rows.push(r as u32);
688        }
689        for (c, v, indent, fmt) in &line.labels {
690            g.set(r, *c, v.clone(), ck.0, *indent, fmt.clone());
691        }
692        if !line.values {
693            continue;
694        }
695        let rp = line.path.as_slice();
696        for (j, (si, col_vi)) in data_cols.iter().enumerate() {
697            let line_vi = col_vi.or(line.vi);
698            let (Some(spec), Some(vi)) = (specs.get(*si), &line_vi) else { continue };
699            let Some((_, vdef)) = vals.get(*vi) else { continue };
700            let cp = spec.path.as_slice();
701            let Some(raw) = lookup.get(rp, cp, *vi) else { continue };
702            let context = ShowContext { row_path: rp, col_path: cp, value_index: *vi, level0: &level0, siblings: &siblings };
703            let shown = show_value(vdef, raw, &lookup, &context);
704            if let Some(pr) = probe.as_deref_mut() {
705                if pr.at == Some((r, l + j)) {
706                    pr.hit = Some((rp.to_vec(), cp.to_vec()));
707                }
708                if let Some(cells) = pr.cells.as_mut() {
709                    cells.insert((r, l + j));
710                }
711            }
712            g.set(r, l + j, shown, ck.1, 0, data_fmt.get(*vi).cloned().flatten());
713        }
714    }
715    Ok(Output { width: g.width as u32, grid: g.rows, row_kinds: g.kinds, body_offset, chart_rows, chart_cols, label_col })
716}