Repository navigation
Conversation
…re read, drawn and saved A typed c:numFmt (sourceLinked="0") on a value or category/X axis, or in a series or group c:dLbls, was dropped on import and every label followed its source cells. The model keeps them (Chart.axis_number_formats for x, y and the secondary y; Series.label_number_format), the reader maps each value axis to the group that uses it (Excel may list the secondary axis first), the writer emits them, and the renderer uses them over the source format. PivotChart refresh keeps label formats by series position.
added 7 commits
October 13, 2026 22:38
- Labels that show percentages, bubble sizes or scatter X values but not the values keep no format: it was typed for those (Driver-Safety-Scorecard: 0.0% on a percent-only doughnut). - A deleted value axis keeps no format (ELD-Unverified-Logs: #,##0 on a hidden 100% axis). - Scatter and bubble X/Y axes are the group's own, found through its axIds. - PivotChart refresh keeps label formats for series it drops and brings back.
GridCraft reads lineChart and areaChart groups with grouping="percentStacked" as plain stacked and draws their absolute values; only the column family has a 100% variant. A number format typed for the 0%-100% axis of such a chart (Excel saves `0.0%` with sourceLinked="0" once the decimals are changed) was applied to those absolute values, so the axis read "2000000.0%", and saving wrote it onto a plain stacked chart. Modelling 100% stacked line and area would take a new chart kind, writer support, normalisation in the renderer and Insert gallery entries, which is a feature of its own. The reader now drops the typed value axis format of any axis a percentStacked line or area group uses, the same way it already does for deleted axes, so the axis follows the source cells and nothing is saved for it.
…shes A refresh rebuilds a PivotChart's series and handed the typed data label formats back by position. When a filter hid an item that wasn't last, every later series took the format of the series before it, and saving wrote the wrong formats. Excel ties this formatting to the pivot items. The formats are now keyed by series name, which is the series' pivot items. They are collected before the refresh rewrites the report, so series named by the report's header cells (PivotCharts read from a file) resolve to the old headers, and the remembered formats of series a refresh dropped are kept by name too.
… items
A PivotChart read from a file names each series by the report's header
cells. With two column levels (a column field and the value fields, say) the
name spans two cells, and Excel writes the outer item only above its first
column. The formats were matched on display strings: the reference evaluated
to the non-blank cells joined by spaces ("Apples" or "Apples Sum of Sales"),
while the rebuilt series is named "Apples - Sum of Sales", so the first
refresh after opening dropped every such format.
The report now hands each PivotChart series its list of pivot items, and the
remembered formats are keyed by that list. A referenced name is read as a
list of header cells, top to bottom, a blank cell taking the nearest label to
its left within the old report. Keys match item by item, as GridCraft's
joined literal name, or in any order, since Excel can put the value field
above the column items.
…ep column items in order Excel names a PivotChart series by its header cells from the report's top row, the "Column Labels" caption, down to the last column-item row, with blank rows under a collapsed item. Those names are now read without the caption row, a blank cell's left-fill stays inside the data columns and inside the outer item's columns, and a chart whose names still match nothing keeps its formats by position when the series count is unchanged. The order-insensitive item match gave "Yes - No" the format typed on "No - Yes". Only the value field's name may now sit at another level, and a looser match only uses a key no rebuilt series has item by item.
…that show no pivot item The position fallback ran whenever no file-read series name matched a rebuilt series and the count was unchanged, which is also what a filter that swaps the shown items does: Apples' format went to Pears. It now runs only when the names show none of the pivot's row/column items (hidden ones included) or value names. The formats keyed by item are remembered on every path, so a hidden item gets its format back when it returns.
…lue fields and forget captions Value field names no longer block the position fallback (with two or more value fields every Excel series name ends in one), a name made only of value field names still does. After a fallback the formats are remembered under the rebuilt series' items, not the file's captions, so a later source item that reads like a caption doesn't take a format. The pivot's item set is read from the source only when no name matches and the series count is unchanged.
This was referenced Oct 16, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Number formats typed in Excel's Format Axis and Format Data Labels panes were lost: on import every axis and data label followed its source cells, and saving dropped them. In a set of 11 Excel-saved fleet reports, 10 such formats (6 axis, 4 data label) appear across 5 files: a duration axis (
[h]:mm) next to a percentage axis (0%) on a combo chart,#,##0value axes, and0;;;labels (positives only, so zero bars carry no label). GridCraft saved none of them, and drew the Idling Violations axes as0…1.0.What changed
crates/model/src/features.rs).Chart.axis_number_formats: AxisNumberFormats { x, y, y2 }(the X value axis of scatter and bubble charts, the value axis, the secondary value axis) andSeries.label_number_format. Both areOption<String>with#[serde(default)].Nonemeans "Linked to source", as before.crates/xlsx/src/chart.rs). Ac:numFmtcounts only withsourceLinked="0". Each value axis is matched to the chart group that uses it through the groups'c:axIds, not by file order: Excel can save the secondary value axis first (it does in the Idling Violations report). Scatter and bubble charts take X and Y from the group's own two value axes, X being the bottom/top one. Data labels come from the series'c:dLbls, otherwise from the group's. Three kinds of typed format are skipped because applying them would show something Excel doesn't: a deleted (hidden) axis's format, since GridCraft draws every axis; the value axis format of a 100% stacked line or area group, since GridCraft reads those as plain stacked and draws absolute values (a0.0%typed for the 0%–100% axis would read "2000000.0%"); and the format of labels that show percentages, bubble sizes or scatter X values but not the values, since it was typed for those.c:numFmt sourceLinked="0"on its axis, otherwise the linkedGeneralas before. A series with a label format gets its ownc:dLblscarrying thec:numFmtand the chart's show-value flag.crates/chart/src/render.rs,resolve.rs). Axis tick labels use the typed format for their axis, otherwise the source cells' format as before. Fixed-scale (100%) charts keep their0%unless a format is typed. Data labels use the series' typed format, otherwise the source format.chartFormat/pivotAreain the PivotTable definition. GridCraft keeps the link in memory only and doesn't read or write those parts, so after a reopen the formats are matched by the items the series' names show, and a format remembered for a series that was filtered out when the file was saved is lost.docs/chart-parity.md: the Data labels and Axis options rows and the round-trip note.Measured
Every chart part in the 11 reports was opened and saved with
gridcraft-cli run --in F --out O, and typed (sourceLinked="0") formats were counted in the saved XML: on value/category axes, and on the series' or group's ownc:dLbls(per-pointc:dLblformats aren't read, so they aren't counted). Remeasured on2e40b54with main's, this branch's and this branch-on-#685's (b19cd90) release builds:python3 measure.py <main cli> <branch cli> <branch+#685 cli>, a 36-line script that unzips each part and counts with regexes.The 2 axis formats that only #685 brings back belong to two bar charts in Driver-Safety-Scorecard that sit inside grouped shapes. Main and this branch both drop those charts (
grouped shapes are not supported). With this branch cherry-picked onto #685's head (no conflicts), the saved file has its 4 chart parts and both bar charts keep their#,##0axes.The 2 that are never kept are dropped on purpose:
#,##0on the deleted value axis of a 100% stacked chart. GridCraft draws that axis, and with this format it would read 0, 0, 0, 1, 1, 1 instead of 0%…100%.0.0%on doughnut labels that show percentages only. Applied to the values, it reads 3 as "300.0%".An offscreen snapshot (
crates/ui-egui/examples/snapshot.rs) of Idling Violations shows the left axis in[h]:mm(0:00…24:00) and the right axis in0%…100%. Main shows0…1.0on both. The plot itself stays empty until #684 lands: this PivotChart's series point at the PivotTable's corner cell. With #684 and this PR together, the chart plots its 15 cached points up to600:00with%labels on the line, as Excel shows it.Verification
crates/xlsx/src/tests/chart_number_format.rs:typed_formats_are_read_and_linked_ones_are_notreads an Excel-style combo chart with the secondary value axis listed first. The value axes come out as[h]:mmand0%, the linked category axis and the source-linked series labels stayNone, and a group-leveldLblsformat reaches the line series.typed_formats_round_tripsaves and reopens a combo chart (Y, secondary Y and per-series labels), a scatter chart (typed X and Y) and a pie chart (labels only), and checks every format reads back equal.a_percentage_label_format_is_not_applied_to_values,a_format_typed_for_sizes_or_x_values_is_not_applied_to_values,a_hidden_value_axis_keeps_no_typed_formatandscatter_axes_are_the_groups_own_and_x_comes_firstcover the review findings below. All four fail without the second commit.a_100_percent_line_or_area_axis_keeps_no_typed_formatreads a typed0.0%on the axis of a 100% stacked line and of a 100% stacked area, alone and on the secondary axis of a combo chart, and checks it isn't kept while the column group's#,##0is.crates/engine/src/cmd/pivot.rs(PivotChart label formats; all five fail on the previous head7b5596c):pivot_chart_label_formats_follow_their_itemsfilters column items out and back and checks each series gets its label format again; then hides the leading item (selected: [Pears, Plums]) and checks Pears and Plums keep their own formats, with series named by literals and by header-cell references shaped as Excel writes them: from the report's top row ("Column Labels" inB3), including three-row names with two value fields (caption, product, value; the product blank above its second column).pivot_chart_label_formats_read_collapsed_excel_headerscopies the shape of the Excel-saved forum-mso-de-104083 chart: names$B$3:$B$5with the colHeaderCaption "Datum" on top, the outer item, then a blank row under the collapsed item whose label column holds the rowHeaderCaption "KW". A refresh keeps the three#,##0.0formats.pivot_chart_label_formats_keep_column_items_in_order(Plan × Done, both Yes/No): only "No - Yes" has a format; after a refresh, and after a filter that drops it and brings it back, "Yes - No" still has none.pivot_chart_label_formats_match_values_above_column_items: names with Σ Values above the product ([value, product]) still match GridCraft's[product, value]series.pivot_chart_label_formats_fall_back_to_position_for_unknown_names: header captions that show none of the pivot's items keep the formats by position on a refresh that keeps the series count, and are dropped (not shifted) when the series change.pivot_chart_label_formats_fall_back_to_position_with_two_value_fields: with Sales and Units, each name ends in a value field's name; with the product captions renamed, the six formats are still kept by position (347ee2akept none).pivot_chart_label_formats_forget_captions_after_a_fallback: after a fallback over the captions Kiwis/Birnen/Pflaumen, one source record's product becomes Kiwis; the new Kiwis series gets no format (347ee2agave it Apples'0;;;, remembered under the caption), and the other three keep theirs.pivot_chart_label_formats_follow_items_a_filter_swaps(fails on2e40b54): a file-named series for Apples, typed0.0, then a filter showing Pears alone (same count) leaves Pears without a format, and clearing the filter gives Apples0.0back; with two of three regions shown, swapping one of them hands the new one no format.sc/qa/unit/data/xlsx/forum-mso-de-104083.xlsxfrom LibreOffice's test files (Excel 6, not committed: it's third-party). With#,##0.0typed on its three series' labels,gridcraft-cli run --in de-typed.xlsx --cmd 'pivot.refresh={"pivot":"PivotTable2"}' --out r.xlsxkeeps all three;7b5596ckept none. With0.0/0.00/0.000typed on 2015/2016/2017,pivot.sort(Datumdescending) saves 20170.000, 20160.00, 20150.0: the formats follow the items, not the positions.crates/chart/src/tests.rs:typed_axis_and_label_formats_override_the_sourcedraws a#,##0.0,"k"value axis,0;;;labels on one series (no label on its negative point, the other series keep the source$#,##0), and a combo chart's secondary axis in its own format while the primary keeps the source's. The chart fuzz test now also generates random label and axis formats.f883adb:cargo xtask cipasses all 8 steps (fmt, clippy-D warnings, test, assets, layers, wasm, parity, keytips).cargo fmt --all,cargo clippy -p gridcraft-engine --all-targets -- -D warningsandcargo test -p gridcraft-engine(294 lib tests plus the integration tests) pass.2e40b54,347ee2aandf883adbtouch only the engine crate anddocs/chart-parity.md.eprintln!inpivot_items(removed before the commit) and a 100,000-row Region × Date / Product pivot, refreshed five times with Excel-shaped names (Sheet2!$B$3:$B$4…; Excel names a series from the report's top row down to the row above the first data row) and0.0on each series (cargo test -p gridcraft-engine --lib <probe> -- --ignored --nocapture). Onf883adbthe set was built 0 times and the formats were kept. On347ee2ait was built on every refresh with these names too, because the set was built before any name was checked. Names that can't match, such as$B$3:$B$5(which takes inR0's subtotal row), still build it on each refresh. Wall times on this shared machine varied 0.2–2.5 s between runs of the same build, so no timing is claimed.gridcraft-cli checkon forum-mso-de-104083.xlsx and the 11 reports (gridcraft-cli check de.xlsx /tmp/gridcraft-try/sheets/downloads/*.xlsx) gives the same output with main's and this branch's (f883adb) release builds (diffof the two outputs is empty): 263,831 formula cells all matched, and both exit 1 on the same package round-trip differences in 9 of the 12 files (lostchartStyle/chartColorStylerelationships and the like), none from this PR. The real-file results above were rerun onf883adband are unchanged. ELD-Unverified-Logs' PivotChart keeps both its0;;;series formats throughselection.set={"sheet":"Graph","range":"A12"}thenpivot.refresh={"pivot":"PivotTable2"}, which rebuilds the chart's series as Verified/Unverified. Without the selection, that command refreshes Summary's PivotTable2 and doesn't touch the chart. Main's build saves none.0.0%, data labels#,##0.000, both with Linked to source unticked):c:dLblbecause the labels were formatted one by one. Saved by this PR (f883adb), the file opens with no repair prompt and the axis keeps0.0%, but the labels fall back to General: per-point formats aren't read (Not done).<c:numFmt formatCode="#,##0.000" sourceLinked="0"/>in the seriesc:dLbls(edited by hand from Excel's file), saved by this PR: it opens with no repair prompt and shows the labels as 0.120, 0.345, 0.500, 0.079 and 0.900. Format Data Labels > Number shows#,##0.000and Format Axis > Number shows0.0%, both with Linked to source unticked. Saved bymain(6e984bb), both formats are General.Review
Before the PR was opened, two review passes ran on the diff: one on the first commit, one on the fixes. The second commit addresses what they found:
0.0%on labels that show percentages only. The first commit applied it to the values ("300.0%"). The second pass found the same problem with labels that show only bubble sizes or scatter X values. Those formats are now skipped.axPos="l"on both scatter axes, so Y got X's format and title. They are now the group's own axes, found through itsaxIds.After it was opened, five adversarial review rounds ran on the pushed diff.
Round 1 (on
dff577b) found two defects, fixed in2363045,564789fand7b5596c:0.0%typed for their axis labelled absolute values ("2000000.0%") and was saved onto a plain stacked chart. That format is now skipped, like a deleted axis's. Modelling the 100% line and area variants (a new chart kind, writer, renderer normalisation, Insert gallery) is a separate feature.Round 2 (on
7b5596c) found two more defects in that item matching and four body claims the code didn't support. Fixed in2e40b54:dff577bkept them by position). The caption row is now skipped, a blank cell looks left no further than the first data column (and on inner rows no further than its outer item), and if no name matches at all, formats stay by position when the series count is unchanged.Round 3 (on
2e40b54) found one defect. Fixed in347ee2a:0.0went to Pears, and the remembered formats were dropped on that path, so Apples never got it back. The fallback now runs only when the names show none of the pivot's labels (hidden items included), and the formats keyed by item are remembered on every path.Round 4 (on
347ee2a) found two defects, a cost, and two claims that were too broad. Fixed inf883adb:2e40b54kept them). Only row and column items block the fallback now; a name made of value field names alone still does.0;;;. After a fallback only the rebuilt series' items are remembered (plus formats remembered earlier for dropped series).$B$3:$B$4shape included. It's now read only when no name matches a rebuilt series and the series count is unchanged (see Verification).Round 5 (on
f883adb) found no defects. Every round-3 and round-4 probe gives the same result. Charts with no column fields and one or two value fields, and with Σ Values on rows, match by item and never build the item set; so does a real Costliest-Assets chart (Summary!$B$4). It corrected two lines here: the ELD refresh command, which needs the chart's PivotTable selected first (rerun: both0;;;kept), and the round-4 cost note, which had said the reviewer's probe used names Excel doesn't write.Merging with #684
#684 adds
Series.number_format(the format of literal values) next to this PR'sSeries.label_number_format, so both touch everySeries { … }literal andread_chart. I applied the first two commits onto #684's then head (df4599d) to check. The conflicts were the two new fields side by side,read_chartreturning #684's(Chart, Vec<PivotSeriesCache>)(the new tests take.0), and the two doc rows. That combined tree passed fmt, clippy-D warnings, and the chart, xlsx, model and engine tests. #684 has moved since (its review branch is atef6776d) and this branch has five more commits; that merge hasn't been redone. Whichever lands second, I'll rebase it.Not done
chartFormat/pivotAreaandc:pivotFmtsaren't read or written.