PIVOTBY(row_fields,col_fields,values,function,[field_headers],[row_total_depth],[row_sort_order],[col_total_depth],[col_sort_order],[filter_array],[relative_to]) groups records along row and column keys, applies an aggregation callback and spills the cross-tabulation. Its optional arguments control headers, row/column totals and sorting, filtering, and the comparison set for a two-argument callback.
Try it
Put regions east,east,west,west in B2:B5, periods Q1,Q2,Q1,Q2 in C2:C5 and amounts 3,7,5,11 in D2:D5. =PIVOTBY(B2:B5,C2:C5,D2:D5,LAMBDA(v,SUM(v)),0,0,,0) returns east with Q1=3, Q2=7, and west with Q1=5, Q2=11, without totals.
A two-argument share callback can use the whole table as its denominator:
The final 2 selects the grand-total comparison set. East/Q1 is 3/26, about 0.1153846154. With relative_to zero its column denominator is 3+5=8, giving 0.375; with one its row denominator is 3+7=10, giving 0.3. Percentage formatting changes their display, not those fractions.
Quirks
Header inference and spill space matter. Totals have separate row and column depth controls, including negative depths for totals before details. Relative_to applies only to two-argument callbacks: the public page lists column, row, grand, parent-column and parent-row comparison sets. Empty intersections are not automatically numeric zeros in the current recorded route.
Optional argument
What it controls
field_headers
Whether the first source row is data or headings, and which headings appear. Modes are shared with GROUPBY.
row_total_depth, col_total_depth
Independent grand totals and hierarchy subtotals; 0 omits that axis's totals.
The row and column field arguments both describe source columns. They are not already shaped row/column headers to cross-multiply. This formula returns a spill array; it does not create an Excel PivotTable object.
How to implement
Validate row alignment of both key tables, value columns and filter flags. Resolve header policy, group typed row and column tuples, sort each axis, and invoke the callback on values at each intersection. Generate subtotal/grand-total cells from their source-row sets. For a two-argument callable choose the second set using relative_to, preserving the grouping level. The current source retains separate scope limits for parent totals, multiple value columns and boxed array results; a basic hash-map sum does not implement these branches. The example has one row in each intersection, so it isolates layout from reduction.
Rust code
Show the CalcFunc source of this function
Quoted from the public repository DnaCalc/DnaCalc, crates calc-func and calc-fml, at commit 9e1884369a67. Comments are the source's own. A kernel shared with other functions appears once, on its module page.
/// The lines of an axis whose sorted `groups` have `fields` fields, at total depth `depth`: the/// groups, a subtotal after (before, for a negative depth) each run of groups sharing their first/// k fields for each k below the depth, and the grand total over `all`.fnlines(
site: &Site<'_>,
groups: &[(Vec<CalcValue>, Vec<usize>, Vec<CalcValue>)],
fields: usize,
depth: i64,
all: &[usize],
) -> Step<Vec<Line>> {
let levels = depth.unsigned_abs() as usize;
let before = depth < 0;
letmut labels = vec![text(""); fields.max(1)];
labels[0] = text(if levels >= 2 { "Grand Total" } else { "Total" });
let grand = Line {
labels,
members: all.to_vec(),
grand: true,
subtotal: false,
};
letmut out = Vec::new();
subtotalled(site, groups, 0, levels, before, fields, &mut out)?;
match depth {
d if d > 0 => out.push(grand),
d if d < 0 => out.insert(0, grand),
_ => {}
}
Ok(out)
}
/// The lines of `groups`, which share their first `level` fields, with a subtotal for each run of/// them sharing field `level` while `level + 1` is below `levels`.fnsubtotalled(
site: &Site<'_>,
groups: &[(Vec<CalcValue>, Vec<usize>, Vec<CalcValue>)],
level: usize,
levels: usize,
before: bool,
fields: usize,
out: &mutVec<Line>,
) -> Step<()> {
if level + 1 >= levels {
for g in groups {
out.push(Line {
labels: g.0.clone(),
members: g.1.clone(),
grand: false,
subtotal: false,
});
}
returnOk(());
}
letmut at = 0;
while at < groups.len() {
letmut end = at + 1;
while end < groups.len() && same(site, &groups[end].0[level], &groups[at].0[level])? {
end += 1;
}
let run = &groups[at..end];
letmut labels = vec![text(""); fields];
labels[..=level].clone_from_slice(&run[0].0[..=level]);
let subtotal = Line {
labels,
members: run.iter().flat_map(|g| g.1.iter().copied()).collect(),
grand: false,
subtotal: true,
};
if before {
out.push(subtotal);
subtotalled(site, run, level + 1, levels, before, fields, out)?;
} else {
subtotalled(site, run, level + 1, levels, before, fields, out)?;
out.push(subtotal);
}
at = end;
}
Ok(())
}
No implementation has been published with a Handbook test suite for this function.
Version differences
The implementation comments retain Excel 16.0 build 20527 COM readings on 30 October 2026, including fifteen depth controls and ten multi-value/subtotal controls. They identify behaviour on that build and input scope. No same-input earlier-build comparison is recorded here.
Excel 16.0 build 20527
Windows through COM; bitness and CPU not stated in these comments
Implementation comments retain 15 depth controls and 10 multi-value/subtotal controls on 30 October 2026. Empty intersections, parent comparison sets and boxed callback arrays retain their separately stated scopes; no cross-build difference is established.
Alternatives
GROUPBY has only row groups; PERCENTOF calculates a share within a supplied comparison set; SUMIFS can build a fixed cross-tab manually.