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
8pub const NOT_A_VALUE_CELL: &str = "select a value cell in a PivotTable";
11
12pub 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 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
28pub 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
39fn 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
60pub(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 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 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 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 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 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 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 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 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 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 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 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 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 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}