Skip to content

PivotTables: files with TRUE/FALSE sources open in Excel, spaced headers keep their fields, report filters, date groups, PivotCharts and Show Values As match Excel 365 - #1067

Closed
iluebbe wants to merge 13 commits into
storytold:mainfrom
iluebbe:fix/pivot-excel-layout

Conversation

@iluebbe

@iluebbe iluebbe commented Oct 13, 2026

Copy link
Copy Markdown

Problem

I checked PivotTables against Excel 365 on Windows (German UI) in two directions:

  • Made in GridCraft: 93 pivots from GridCraft, one feature per file, opened in Excel and refreshed there.
  • Made in Excel: 57 pivots from Excel and 7 real files from Apache POI's test data. Each was refreshed, edited and saved in GridCraft, then opened again in Excel.

I found no existing issue or PR for the bugs below; overlaps with open PRs are listed at the end. Labels are English here; German Excel shows Zeilenbeschriftungen, Gesamtergebnis and so on.

Excel can't open the file, or fields disappear:

  1. TRUE/FALSE or error values anywhere in the source.
    • Excel 365 can't open the saved file, not even with repair. The column doesn't have to be used in the pivot.
    • Repro: range.setValues of Name, Flag, Sales / a, true, 1 / b, false, 2 / a, true, 3, then insert.pivotTable {"source":"Sheet1!A1:C4","rows":["Name"],"values":["Sales"]}. The same file without Flag opens.
    • GridCraft wrote containsString="0" on a booleans or errors field, and containsMixedTypes="1" on text plus booleans. Excel writes neither.
    • It also hits a plain open and save of an Excel file whose pivot source has such a column.
  2. A source header with a leading or trailing space ("Region ", "Sales ").
    • The field disappears on refresh and even on a plain open and save, without a warning. A pivot with rows Region and values S = Sum of Sales and Q = Sum of Qty refreshes to just Q / 6.
    • POI's LIBRE_OFFICE-116306-0.xlsx keeps 8 of its 14 value fields after convert; the 6 dropped ones end in a space.

Wrong numbers or layout after a round trip:

  1. A single-item report filter from an Excel file is read as a different item, and saved back that way. Excel filters Qty = 3; GridCraft reads ["2"], so the report shows other values (North 80 → east 87.5).
  2. Date groups lose their grouping in Excel. This happens with groups by years, quarters, months or days whose date column has blank cells. After a GridCraft save, Excel's refresh shows every single date: A3:B17 grows to A3:B24.
  3. Days are in text order. A field grouped by days shows 01. Aug, 01. Feb, 01. Jan, 15. Jan after a refresh in Excel; GridCraft shows date order.
  4. Two value fields and no column field (the default for Sum and Average of a field).
    • Excel 365 shows an extra Values caption row that GridCraft's saved layout doesn't have, so the report grows by a row when Excel lays it out.
    • With anything in the row just below, Excel shows the whole PivotTable as one #SPILL! cell.
  5. PivotCharts become ordinary charts. insert.pivotChart wrote <c:pivotSource><c:name>Sheet2!PivotTable1</c:name>. Excel binds a chart only to a name that starts with a workbook in brackets ([Book.xlsx]Sheet2!PivotTable1), so Chart.PivotLayout was Nothing.

Values that differ from Excel's own refresh:

  1. An item whose values are all blank shows empty cells in Excel, for every function. GridCraft showed 0 for Sum, Count, Max, Min and Product, and #DIV/0! for Average, StdDev and Var.
  2. Show Values As.
    • % of Grand, Column and Row Total show 0 % where an intersection has no data; GridCraft left the cell empty.
    • Running Total carries into such cells (West / Pear: Excel 5, GridCraft empty).
    • Running Total's Grand Total row is empty in Excel; GridCraft showed the plain totals 30 / 5 / 35.
  3. Rank Largest to Smallest uses dense ranks in Excel. Values 5, 4, 3, 3, 4 rank 1, 2, 3, 3, 2 in Excel and 1, 2, 4, 4, 2 in GridCraft.
  4. Caption cells.
    • With column fields only, Excel puts the value caption in the data row's label cell (A5) and leaves A3 empty; GridCraft did the opposite.
    • With Σ Values on rows and no column field, Excel leaves the data column's header empty; GridCraft wrote Total.
  5. Count Numbers is captioned "Count of Units" by Excel and by GridCraft's own xlsx fallback, but "Count Numbers of Units" by new GridCraft value fields.

Cause and fix

One commit per item:

commit item cause → fix
ebe4742 1 sharedItems flags counted booleans and errors as kinds of their own (kinds()). They now count as string-like, as in Excel's caches.
49a4f72 2 Source headers were trimmed (read_source, header_text), but field names from the file kept their spaces, so nothing matched. Headers now keep their spaces; typing a field name without them still finds the field.
718e13b 3 The reader used pageField@item as an index into the cache's shared items. It indexes the pivot field's <items>, whose x is the shared item. The writer was already right.
b46332a 4 containsSemiMixedTypes="0" was written on grouped date fields even next to containsBlank="1"; now only when the field has no blanks.
86e2b70 5 Every axis field was written with sortType="ascending", which Excel applies to day labels as text. Ascending date groups are now written without it, as Excel writes them, and read back as ascending.
fa844ac 6 Excel 365 writes x14:pivotTableDefinition hideValuesRow="1" in its own new pivots and doesn't show the Values row then. GridCraft now writes that flag and the location offsets Excel writes, so Excel lays the pivot out as GridCraft does. GridCraft's layout is unchanged.
cee9a27 7 The PivotChart link now carries the file name ([name.xlsx]Sheet!PivotTable, from write_xlsx_named); the reader still drops it.
6e27bb4 8 Acc::result returned 0 or #DIV/0! for an accumulator that saw no value. Such cells are now empty; as a percentage or running total they count as 0.
fc99483 9 The data-cell loop skipped intersections without an aggregate, and the grand line took the plain total. Now percentages show 0 there, the running total carries over, and its Grand Total line stays empty.
6e4e130 10 The rank counted every greater sibling; it now counts distinct greater values.
6c65d3d 11 Caption cells for pivots without row fields, with values on rows, and in outline layout follow Excel. A hidden header row leaves no empty caption row.
5f9581d 12 Captions and function names lived in both the engine and xlsx. They now live in one table on model::PivotFunc.
e87e250 docs: gaps.md §1 / §8 and target-app-parity.md note that GridCraft-made pivots are now checked in Excel 365.

Testing

  • Regression tests: each commit has its own, in crates/engine and crates/xlsx, using the cases above.
  • Commands:
    • cargo fmt --all -- --check.
    • cargo clippy -p gridcraft-model -p gridcraft-xlsx -p gridcraft-engine -p gridcraft-chart -p gridcraft-cli --all-targets -- -D warnings.
    • cargo test -p gridcraft-model -p gridcraft-xlsx -p gridcraft-engine: 527 pass.
    • cargo xtask layers, assets and wasm pass.
    • cargo check passes for each commit.
  • Excel 365:
    • Every repro above opens in Excel without repair, and Excel's refresh gives the same cells as GridCraft (#SPILL! case included).
    • PivotCharts have PivotLayout set.
    • A pivot Excel made and GridCraft refreshed matches Excel's own refresh of the original.
    • The 93-pivot matrix: 44 of 90 files matched Excel's refresh on main, 76 with the first version of these fixes. I didn't rerun the whole matrix after the rework of item 6; I re-checked the affected cases one by one in Excel instead.

Not fixed here:

Overlaps

#722, #474, #498 and #719 change the same five files (engine/src/cmd/pivot.rs, engine/src/pivot.rs, model/src/features.rs, xlsx/src/pivot.rs, xlsx/src/tests/pivot_tests.rs). #684 changes engine/src/pivot.rs, model/src/features.rs and xlsx/src/chart.rs.

#722 also reads and writes hideValuesRow (as show_values_row), and it adds the Values row for outline and tabular layouts. Whichever PR lands second should keep a single show_values_row that this PR's writer sets to hidden for new pivots.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SAgnkqJiy3v24Hp25dQLRj

iluebbe and others added 13 commits October 13, 2026 13:44
…GridCraft lays them out

With two or more value fields and no column field, GridCraft lays out the value captions as the
header row. Excel 365 does the same for its own PivotTables, because they turn "Show the Values
row" off (x14:pivotTableDefinition hideValuesRow="1"). GridCraft's files didn't say so, so Excel
added a "Values" caption row on refresh: the report moved down a row, and anything in the row
below GridCraft's report left Excel showing only #SPILL! in its place.

The writer now turns the Values row off as Excel does, and writes the location Excel writes for
such a report (firstHeaderRow 0, firstDataRow 1). With headers off, the caption row above column
fields is left out when it would be empty, as in Excel (firstHeaderRow 0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAgnkqJiy3v24Hp25dQLRj
… in Excel

When every value of an item is blank, Excel 365 leaves its value cells empty for every
function. GridCraft showed 0 for Sum, Count, Max, Min and Product and #DIV/0! for Average,
StdDev and Var, which reads as data that is 0 or as an error.

Shown as a share of a total or as a running total, such an item still counts as 0, as Excel
shows it (0 % and the carried running total). Calculated fields keep reading a blank sum as 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAgnkqJiy3v24Hp25dQLRj
…a, as in Excel

Where a row item and a column item have no records together, Excel 365 shows 0 % for
% of Grand Total, % of Column Total and % of Row Total, and carries the running total into
the cell (0 when nothing came before). GridCraft left these cells empty, because the
Show Values As rules ran only for intersections with an aggregate. For Running Total, Excel
also leaves the Grand Total line empty; GridCraft filled it with the plain totals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAgnkqJiy3v24Hp25dQLRj
Every row and column field was written with sortType="ascending", the default of a new field.
For a field grouped by days, Excel 365 reads that as an A-to-Z sort of the item labels, so a
GridCraft pivot opened and refreshed in Excel showed 01. Aug, 01. Feb, 01. Jan, 15. Jan. Excel's
own date groups have no sortType and keep the items' date order.

A date group sorted ascending is now written without sortType (its items are written in date
order), and a date group without sortType reads back as ascending. Descending and the sort of
other fields are written as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAgnkqJiy3v24Hp25dQLRj
A PivotChart names its PivotTable in <c:pivotSource><c:name>. GridCraft wrote it as
Sheet2!PivotTable1; Excel 365 binds the chart only when the name starts with a workbook in
brackets ([Book.xlsx]Sheet2!PivotTable1), and otherwise opens it as an ordinary chart over fixed
cells (Chart.PivotLayout is Nothing).

The writer now puts the saved file's name in front (passed down from the save as
write_xlsx_named), or a placeholder when there is none: Excel accepts any bracketed name. The
reader drops the workbook part, so the model keeps Sheet2!PivotTable1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAgnkqJiy3v24Hp25dQLRj
…on rows, and in outline, as in Excel

Laid out as Excel 365 refreshes them:
- Column fields and one value field, no row field: the value's caption labels the data line
  (A5), not the top-left cell (A3).
- Column fields and two or more value fields, no row field: there is no label column; the
  report starts with the first column item (firstDataCol 0).
- Σ Values on rows without a column field: the data column has no header. GridCraft wrote
  "Total" there; Excel leaves it empty after a refresh.
- Outline and tabular: the caption row names every column field, Σ Values as "Values"
  (B3 Product, C3 Values); GridCraft named only the first.
- With headers off, a caption row that would be empty is left out (columns only, one value).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAgnkqJiy3v24Hp25dQLRj
…rror values

A pivot cache field's <sharedItems> flags said containsString="0" for a column holding booleans
or errors, and containsMixedTypes="1" for text mixed with booleans. Excel 365 counts booleans and
errors as strings in these flags and can't open a file that contradicts its own items, not even
with repair; any such column in the source was enough, used in the pivot or not.

The flags are now written as Excel writes them: strings are text, booleans and errors;
containsString="0" and containsSemiMixedTypes="0" only without them, and mixed types only for
strings together with numbers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAgnkqJiy3v24Hp25dQLRj
… on refresh or save

A source header with leading or trailing spaces ("Region ") names its field with the spaces in
Excel, in the cache and in the layout. GridCraft trimmed headers when reading the source and when
writing the cache, while the fields read from an Excel file kept the spaces, so the layout no
longer found them: a refresh dropped the field, and even a plain open and save wrote the pivot
without its row fields and value fields.

Headers are now taken as they are, as Excel does. A name typed without the spaces
(pivot.addField {"field": "Region"}) still finds the field when only one header matches that way,
and is stored with the header's spelling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAgnkqJiy3v24Hp25dQLRj
pageField@item is an index into the pivot field's <items>; the item there names the shared item
with its x. GridCraft took it as an index into the cache's shared items, so a filter on Qty = 3
read back as Qty = 2 whenever Excel's items weren't in shared-item order, and an open and save
wrote that wrong item back, changing the report's values in Excel.

The reader now goes through the field's items. The writer was right already: it writes the items
in shared-item order, so its index and the shared item agree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAgnkqJiy3v24Hp25dQLRj
…in Excel

The cache field of a date grouping was always written with containsSemiMixedTypes="0", also
next to containsBlank="1". Excel 365 then dropped the grouping when it refreshed a GridCraft file
whose date column had a blank cell: a pivot by months showed every single date instead.

As Excel writes it, a date column with blanks is semi-mixed: containsSemiMixedTypes="0" is now
written only when the column has no blank cells.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAgnkqJiy3v24Hp25dQLRj
…, as in Excel

Excel 365 ranks densely: values 5, 4, 3, 3, 4 rank 1, 2, 3, 3, 2. GridCraft counted every larger
sibling, so the same values ranked 1, 2, 4, 4, 2. A value's rank is now one more than the number
of distinct larger values among its siblings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAgnkqJiy3v24Hp25dQLRj
…t Numbers → "Count of")

The caption word and the names of the summary functions lived in three places: the engine's
func_caption, the XLSX writer's func_caption/func_name/func_from, and the commands' func_param/
func_name. They had drifted: a new Count Numbers value field was captioned "Count Numbers of
Units" by the engine, while Excel 365 (and the XLSX reader's fallback) say "Count of Units"
(German Excel: "Anzahl von Units", the same as for Count).

PivotFunc now holds them: caption() with Excel's words for all 11 functions, name() for commands
and listings, file_name() for dataField@subtotal, and parse() for all of these plus the aliases
the commands accepted. The engine and the XLSX reader and writer use them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAgnkqJiy3v24Hp25dQLRj
gaps.md §1 and §8 and the PivotTables row of target-app-parity.md said the PivotTable round trip
was never checked in Excel. PivotTables made in GridCraft, one feature per file, have now been
opened and refreshed in Excel 365; the differences that turned up are fixed in this branch. No
score change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAgnkqJiy3v24Hp25dQLRj
@iluebbe

iluebbe commented Oct 15, 2026

Copy link
Copy Markdown
Author

Closing this pull request. The problem, Excel's measurements and a fix approach are now in #1447 and #1448.

@iluebbe iluebbe closed this Oct 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant