Skip to content

PPTX export: a workbook for edited and new charts, updated in place when the data changes - #241

Open
pierrevbc wants to merge 3 commits into
storytold:mainfrom
pierrevbc:chart-workbook
Open

pierrevbc wants to merge 3 commits into
storytold:mainfrom
pierrevbc:chart-workbook

Conversation

@pierrevbc

@pierrevbc pierrevbc commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Since #355, an unchanged chart keeps the embedded workbook it was read with (and the other parts
its relationships reach, in Chart.kept) when the presentation is saved as PPTX. What is still
lost:

  • Any edit drops the workbook. Every chart command (chart.type, chart.data,
    chart.options) clears Chart.raw, so after a title change, a new legend or one changed value
    the chart is no longer "unchanged" and is saved without c:externalData or a workbook. Edit Data
    in other presentation apps has nothing to open, and the workbook's other sheets, formulas and
    formatting are gone.
  • Charts made in DeckCraft have no workbook at all.

Reproduce on main with a public file, not for committing:

  1. features/steps/test_files/cht-charts.pptx from scanny/python-pptx (commit 278b47b), or
    test-data/slideshow/aascu.org_hbcu_leadershipsummit_cooper_.pptx from apache/poi (slides 6
    and 7, one chart each).
  2. deckcraft-cli run --in cht-charts.pptx --cmd 'chart.options={"title":"New"}' --export out.pptx
    (any chart command), or insert a chart and export.
  3. unzip -l out.pptx | grep embeddings prints nothing for that chart; its chartN.xml has no
    c:externalData.

Design

The workbook stays where #355 put it: in Chart.kept, the package parts an imported chart points
at. No new media item, no new field for it in the model. ChartKept::workbook() finds the kept
workbook part (the internal package or oleObject target of the chart's relationships).

On export, Exp::chart_rel:

  1. Unchanged chart (its kept part still describes the model): Charts: keep the embedded workbook when a chart is saved unchanged (#335) #355's path, untouched. The
    original part goes back with every kept part under fresh keptN names.
  2. Edited chart whose data still fits its original ranges (Chart.source, commit 2): the
    chart part is written from the model with its original formulas, and the kept workbook is
    written next to it: byte for byte when only the presentation changed (title, type, legend…),
    with only the changed cells rewritten when the data changed.
  3. Anything else (a chart made in DeckCraft, series or points added or removed…): a workbook is
    generated from the chart data, laid out where the generated chart part's formulas point, with
    its c:externalData.

An Office 2016 chart's plain-chart fallback gets no workbook, as with #355: it belongs to the
chartEx part.

What changes

Two commits, then a separate one adding my entry to contributors/people.toml
([people.pierrevbc], display name only).

1. pptx: write a workbook for edited and new charts

  • Model. ChartKept::workbook().
  • Export. Cases 1 and 3 above: an edited or new chart gets a minimal generated workbook (one
    sheet, Sheet1, inline strings; categories in column A, one column per series, series names on
    row 1) under ppt/embeddings/workbook{n}.xlsx, a package relationship and
    c:externalData.
  • Layout fix. Scatter series with different X values, and bubble charts with several series,
    get columns of their own. Before, every scatter series pointed at column A for its X values and
    bubble sizes used col(i + 2), which overlapped the next series' values.
  • Charts: keep the embedded workbook when a chart is saved unchanged (#335) #355's tests in chart_embed.rs: the fixtures strip the workbook the writer now generates
    for a new chart (to stand for a file from an app that writes none);
    an_edited_chart_does_not_write_a_stale_workbook now checks that the stale bytes are not
    written and that the chart gets one workbook of its own data.

2. pptx: update chart workbooks in place instead of rebuilding them

  • Model. Chart.source: Option<ChartSource>: for each series of an imported chart, the
    formulas of its name, categories (or X values), values and bubble sizes (c:f), and the data the
    chart part cached for them. Commands leave it alone, unlike raw. Skipped when absent, so
    existing .deckcraft files serialize as before.
  • Export, data unchanged. When the data still fits the original ranges (same series and point
    counts, same category or XY family, formulas present), the chart part is written with its
    original formulas and the kept workbook is written back byte for byte.
  • Export, data changed. Only the cells whose data differs from what was read are rewritten in
    the kept workbook (crates/pptx/src/write/xlsx.rs, quick-xml and zip, no new dependency, no
    file or network I/O). Every other zip entry is copied with its compressed bytes. In the
    workbook:
    • a cell keeps its style; text is written as an inline string;
    • a new header in a table renames the table column, which Excel requires;
    • a value typed over a formula replaces it, and calcChain.xml (with its relationship and
      content type) is dropped so Excel does not repair the file; Excel rebuilds it;
    • fullCalcOnLoad="1" is set on calcPr when the workbook holds formulas, so formulas
      elsewhere follow the new values.
  • Fallback to a generated workbook (case 3, with a warning in the log) when: series or points
    were added or removed; a category chart became a scatter or bubble chart (or back); the chart
    part held literal data; a formula is not one row or column of one sheet (unions, names, other
    workbooks); a target cell holds a shared or array formula; a new table column name is empty,
    clashes with another column, or appears in a structured reference; a linked workbook's data
    changed; the workbook is an older OLE embedding.

How it was tested

cargo xtask ci passes on this branch, on main at 9859da3 (all 7 steps: fmt, pdf-deps, clippy
-D warnings, 1,687 tests passed and 6 ignored, assets, layers, wasm32 check). On Windows the
deckcraft-ui-egui lib tests sometimes crash with an access violation when run in parallel, on
main as well; the run above used RUST_TEST_THREADS=1.

New and changed tests, all on decks and workbooks built in code:

  • write::chart unit tests: for 11 chart variants (column, stacked bar, line, pie, radar, combo,
    scatter with shared and with separate X values, single-series bubble, multi-series bubble with
    shared and with separate X values), every cached value in the chart part equals the workbook
    cell its formula points to; an unchanged chart keeps its part; edited data leaves that path.
  • write::xlsx unit tests: formula parsing (quoted sheet names, one row or one column only),
    cells replaced and inserted in order (including new rows and an empty sheetData), shared
    formulas and implicit positions refused, calcPr set or inserted at its schema position, table
    columns renamed.
  • tests/chart_workbook.rs, on a workbook built by the test (a Chart data sheet with a table, a
    total in F2, a formula in D5, a Notes sheet with a formula into the first one, shared strings,
    a calculation chain): title, legend and type changed keep the workbook byte for byte; one value
    changed rewrites only that cell and sets fullCalcOnLoad; a renamed series renames the table
    column; a value over a formula drops the formula and the calculation chain; added series or
    points and a clashing series name rebuild the workbook; a scatter chart updates in place; a chart
    made in DeckCraft gets a generated workbook, updated in place after a re-import.
  • tests/roundtrip.rs and tests/chart_embed.rs (Charts: keep the embedded workbook when a chart is saved unchanged (#335) #355): workbook bytes across round trips,
    unchanged charts still keep their parts under keptN names, the chartEx fallback has none.

Checked by hand earlier on the same logic (0.5.0, public decks above, not committed): changing the
title and legend keeps every workbook byte for byte; changing one value changes one cell of one
sheet, every other workbook entry (thumbnail included) staying identical; openpyxl and LibreOffice
read the updated workbooks.

Not verified: PowerPoint's Edit Data and Excel's recalculation on the updated workbooks. That needs
a black-box check on a machine with PowerPoint (AGENTS.md §2).

Known limits

  • The workbook lives in Chart.kept, which a .deckcraft save doesn't store (Charts: keep the embedded workbook when a chart is saved unchanged (#335) #355): a deck saved
    as .deckcraft and reopened has no kept workbook, so an export writes a generated one.
  • The fallback cases above still lose the original workbook's extra sheets, formulas and
    formatting, and an edited chart drops the other kept parts (styles, drawings), as with Charts: keep the embedded workbook when a chart is saved unchanged (#335) #355.
  • An edited chart part is rebuilt from the model: options the model doesn't hold are lost, as
    before. Only the workbook is kept.
  • A changed category is written as text, even over a number or date cell.
  • Structured references are found by their text ([Name], [@Name]); escaped names in formulas
    may be missed. dimension and row spans hints are not updated.

Related PRs

Main keeps an unchanged chart's embedded workbook (storytold#355), but a chart made
in DeckCraft, or one whose data or options changed, was written with no
workbook: no part under ppt/embeddings/, no package relationship, no
c:externalData. PowerPoint's Edit Data had nothing to open.

- Export: an unchanged chart still goes back with its kept parts; an
  edited or new chart gets a workbook generated from its data, laid out
  where the chart part's formulas point (Sheet1, categories in A, one
  column per series), with its c:externalData.
- Scatter series with different X values and multi-series bubble charts
  get their own columns instead of overlapping ranges.
- ChartKept::workbook finds the kept workbook part.
- The plain-chart fallback of an Office 2016 chart gets none: the
  workbook belongs to the chartEx part.
Every chart command clears the original chart part, so any edit (a new
title, another chart type, a legend) made the export drop the chart's
workbook for a generated one: extra sheets, formulas and formatting were
lost even when the data had not changed.

- Model: Chart.source, the series formulas of the imported chart part
  (name, categories or X values, values, bubble sizes) with the data as
  read. Commands keep it.
- Export: when the data still fits the original ranges, the chart part
  is written with its original formulas. Unchanged data keeps the kept
  workbook byte for byte; changed data rewrites only the changed cells
  in it. Every other zip entry is copied as is.
- Cells keep their style; a new name in a table header renames the
  table column; a value typed over a formula replaces it and drops the
  calculation chain; fullCalcOnLoad is set when the workbook holds
  formulas.
- Series or points added or removed, a category chart turned into a
  scatter chart (or back), literal data, shared or array formulas, a
  table column name that would clash or is used by a structured
  reference: the workbook is generated from the chart data, as before.
@pierrevbc pierrevbc changed the title PPTX export: keep chart workbooks, and update them in place when the data changes PPTX export: a workbook for edited and new charts, updated in place when the data changes Oct 15, 2026
anishguduru pushed a commit to anishguduru/deckcraft that referenced this pull request Oct 16, 2026
…om (storytold#241)

- The wheel turns pages in single-page view: one per notch, one per swipe.
- The rail's Page display menu offers Acrobat's view choices.
- Preferences: default page display and default zoom.
- Fit page and Fit height no longer jump across mixed page sizes.
@Harrison-Booth

Copy link
Copy Markdown
Contributor

An embedded workbook would let users edit chart data in PowerPoint, closing the high-priority chart interoperability gap. If you would rather not, I can open a PR that credits you, keeps your commits and shows a git range-diff against this one, so you'll still get credit and have visible work.

This branch has not been deployed

No deployments
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.

2 participants