Skip to content

fix(msexcel): read the series of every chart plot - #4812

Merged
dolfim-ibm merged 7 commits into
docling-project:mainfrom
DanielNg0729:fix/msexcel-combo-chart-series
Oct 19, 2026
Merged

dolfim-ibm merged 7 commits into
docling-project:mainfrom
DanielNg0729:fix/msexcel-combo-chart-series

Conversation

@DanielNg0729

Copy link
Copy Markdown
Member

Hi maintainers, this PR fixes Excel combination charts, which lose the series of every plot after the first one. It is the Excel follow-up to #4773 (issue #4772), which fixed the same problem for PowerPoint.

Problem

openpyxl's read_chart returns the first plot of a chart as the chart object (chart = plot._charts[0]). chart.series holds only the series of that plot. The other plots are only in chart._charts. For a "Clustered Column - Line on Secondary Axis" chart, the backend on main therefore:

  • drops the line series from the data table: 4 of 6 series are kept,
  • classifies the chart by its first plot only (bar_chart),
  • with render_chart_images=True, copies only the data of the first plot into the workbook that LibreOffice draws. openpyxl writes every plot into that workbook, so the line plot is drawn without its data.

The chart data also has two smaller problems that #4773 fixed for PowerPoint:

  • openpyxl reads a date cell as a datetime, so date categories come out as 2012-01-01 00:00:00.
  • The top-left cell of the data table is always empty, so the category axis title is lost.

Changes

This PR changes the Excel backend only.

  • _chart_plots returns every plot in chart._charts, in drawing order. openpyxl always sets _charts, so a chart with one plot gives a list of one.
  • The chart gets one classification prediction per plot type. The back-most plot comes first, as in the PowerPoint backend. The main prediction does not change, so a serializer that reads only the first prediction sees the same chart type as before.
  • The series of every plot become columns of the data table, in drawing order. The categories still come from the first series that has them.
  • The render workbook copies the data ranges of every plot.
  • Date cells in the chart data are written in ISO 8601: 2012-01-01, or 2012-01-01T12:30:00 when the cell has a time of day. The sheet tables do not change.
  • The title of the category axis goes in the top-left cell of the data table. For a scatter chart, this is the title of the x axis. A pie or doughnut chart has no axis, so its top-left cell stays empty.
  • _chart_title_text(chart) becomes _title_text(title), so the chart title and the axis title use the same code. The chart captions do not change.
  • The "PowerPoint charts" section of docs/usage/advanced_options.md now covers Excel charts too.

The value axis titles and the plot type of each series are not in the output. TabularChartMetaField has no place for them (see the question in #4772).

Change in the reference data

Three fixtures have charts with a titled category axis. Their data tables now have the axis title in the top-left cell. No other value changes. The .itxt files do not change. The second commit contains only this change.

Fixture Charts Top-left cell before Top-left cell after
xlsx_01.xlsx line chart, scatter chart (empty) col-1
xlsx_03_chartsheet.xlsx bar chart "Wild Duck Observations by Year" (empty) Year
xlsx_04_inflated.xlsx line chart, scatter chart (empty) col-1

No existing fixture has a combination chart or date categories, so the new tests make their own workbook with openpyxl.

Tests

The new file tests/test_backend_msexcel_charts.py follows tests/test_backend_pptx_charts.py. It saves a column chart with a line plot on a secondary axis, date categories and a category axis title.

  • test_combo_chart_lists_each_plot_type_and_keeps_all_series: the predictions are bar_chart, line_chart, and the table has all 6 series.
  • test_date_categories_are_iso_dates: the categories are 2012-01-01, 2012-02-01, ...
  • test_category_axis_title_heads_the_category_column: the top-left cell is the axis title.
  • test_render_workbook_holds_the_data_of_every_plot: the render workbook has the line series data. This test does not need LibreOffice.
  • test_chart_cell_text: a date keeps its time of day only when it has one.

All of these tests fail on main. test_chartsheet in tests/test_backend_msexcel.py now also checks that the top-left cell is Year.

Checklist:

  • Documentation has been updated, if necessary.
  • Examples have been added, if necessary.
  • Tests have been added, if necessary.

openpyxl returns the first plot of a combination chart as the chart, so
chart.series held only that plot's series. A column + line chart lost
its line series, and it was classified by the column plot only.

Read every plot in chart._charts: give one prediction per plot type,
back-most first, and put the series of every plot in the data table.
The LibreOffice render workbook now also copies the data of every plot.

Write date cells in the chart data as ISO 8601 dates instead of
"2012-01-01 00:00:00", and put the category axis title in the
top-left cell of the data table.

Signed-off-by: Daniel Nguyen <danielnguyenh07@gmail.com>
The data tables of the charts in xlsx_01, xlsx_03_chartsheet and
xlsx_04_inflated now have the category axis title in the top-left
cell ("col-1" and "Year"). No other value changes.

Signed-off-by: Daniel Nguyen <danielnguyenh07@gmail.com>
Signed-off-by: Daniel Nguyen <danielnguyenh07@gmail.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ DCO Check Passed

Thanks @DanielNg0729, all your commits are properly signed off. 🎉

@mergify

mergify Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Merge Protections

🟢 All 2 merge protections satisfied — ready to merge.

Show 2 satisfied protections

🟢 Enforce conventional commit

Make sure that we follow https://www.conventionalcommits.org/en/v1.0.0/

  • title ~= ^(fix|feat|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\(.+\))?(!)?:

🟢 Require two reviewer for test updates

When test data is updated, we require two reviewers

  • #approved-reviews-by >= 2

@github-actions github-actions Bot added ai:ci-safe AI triage found no CI-safety concern (advisory) bug Something isn't working table structure xlsx issue related to xlsx backend labels Oct 14, 2026
@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

AI triage (advisory)

Checked commit fcf7954c05. The analysis read the diff only. It did not run code from this pull request.

The PR makes the Excel backend read the series of every plot of a combination chart. It adds one classification prediction per plot type, ISO 8601 dates and the category axis title in the chart table. It moves the shared chart table helper to docling/backend/utils/charts.py and adds a cell cap for chart ranges.

CI safety: no concern found. A maintainer must still approve CI.

3 related pull request(s) in the same code
PR State Signal Assessment
#4811 open also references issue #4772; changes the same code related: This open PR changes how the PowerPoint backend reads chart plots and series, including 3-D charts. This PR changes the Excel backend. The two PRs overlap in mspowerpoint_backend.py, because this PR moves _build_chart_table, which the other PR still uses. Both can be merged, but they will conflict.
#4773 merged referenced as #4773; also references issue #4772 related: This merged PR fixed combination charts for PowerPoint. This PR applies the same fix to Excel and moves the shared helpers out of the PowerPoint backend. The Excel change is not in the merged PR.
#4628 open changes the same code related: This open PR formats cell text with number formats in msexcel_backend.py, including in _resolve_reference. This PR changes the same function to write dates in ISO 8601. The two PRs fix different problems, but they will conflict in that function.

Reference data changes

Change Files
tables 3
text 3
  • tables tests/data/xlsx/groundtruth/xlsx_01.xlsx.md: +16 / -16 lines; Markdown table rows changed
  • tables tests/data/xlsx/groundtruth/xlsx_03_chartsheet.xlsx.md: +8 / -8 lines; Markdown table rows changed
  • tables tests/data/xlsx/groundtruth/xlsx_04_inflated.xlsx.md: +16 / -16 lines; Markdown table rows changed
  • text tests/data/xlsx/groundtruth/xlsx_01.xlsx.json: item attributes changed
  • text tests/data/xlsx/groundtruth/xlsx_03_chartsheet.xlsx.json: item attributes changed
  • text tests/data/xlsx/groundtruth/xlsx_04_inflated.xlsx.json: item attributes changed

Assessment: expected. The code change puts the category axis title in the top-left cell of the chart table. It also writes dates in ISO 8601 and can add columns for other plots. These changes match the changed table rows and item attributes in the three XLSX reference files. The summary does not show the exact cell values, so this verdict comes from the kind of change.

The ai:* labels are advisory. They do not approve CI and they do not add tests:full. A new push runs this check again.

@github-actions github-actions Bot added the ai:review-lgtm AI first review found no blocker or major issue (advisory) label Oct 14, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI first review (advisory): no blocker found

This review covers the full diff at f4a7dac001. It read the code and did not run it.

I found no defect that should stop the merge. I read the new plot loop in docling/backend/msexcel_backend.py, the openpyxl reader and plot-area code, and the new tests. I also checked the render-workbook change and the axis lookup for pie, scatter and 3D plots.

A maintainer makes the merge decision. This review never approves the pull request.

@codecov

codecov Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.57576% with 23 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
docling/backend/msexcel_backend.py 76.13% 8 Missing and 13 partials ⚠️
docling/backend/utils/charts.py 95.12% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

dolfim-ibm
dolfim-ibm previously approved these changes Oct 14, 2026

@dolfim-ibm dolfim-ibm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@dolfim-ibm

Copy link
Copy Markdown
Member

No blocker. One security point (shared with main, but this PR widens it) and a few suggestions.

Unbounded chart ranges (shared with main). _resolve_reference and _copy_reference_into read every cell in a chart range. There is no size limit, and openpyxl creates a cell object for each one.

  • On main, a 6 KB .xlsx with one bar series that refers to $B$1:$B$1000000 uses 7.2 GB RSS and takes 50 s.
  • With this PR, the same range on a secondary line plot is now read too. main ignores that plot (0.2 s). This PR takes 169 s, uses 13 GB RSS, and makes a table of 1,000,000 rows.

The PowerPoint backend trims tables and caps them with _MAX_CHART_TABLE_CELLS. The Excel backend has neither. A limit on the range size before the cell loop in both functions, with the same cap, would close this. It can go in this PR or in a follow-up, because the same input already reaches it through the first plot.

Suggestions:

  • The table layout and the ISO date formatting duplicate the PowerPoint helpers (_build_chart_table, _serial_to_iso). This duplication is why the Excel path has no cap. Sharing them would keep the backends in sync.
  • _title_text reads only rich text. An axis title linked to a cell (tx.strRef) gives an empty top-left cell, but PowerPoint reads the cached value in that case.
  • test_chart_cell_text and test_render_workbook_holds_the_data_of_every_plot test private helpers. This is acceptable, but tests through the converter output are preferred.

The behavior matches the PowerPoint backend: one prediction per plot type, the main label from the back-most plot, the axis title in the top-left cell, and ISO dates. Classification and the top-left cell are also correct for 3-D, radar, stock, surface, bubble and bar-of-pie charts.

Move _build_chart_table, _last_populated_row and _MAX_CHART_TABLE_CELLS
from the PowerPoint backend to docling/backend/utils/charts.py, as
build_chart_table and MAX_CHART_TABLE_CELLS. Add datetime_to_iso, which
_serial_to_iso now uses. The Excel backend uses the same helpers in the
next commits. The PowerPoint output does not change.

Signed-off-by: Daniel Nguyen <danielnguyenh07@gmail.com>
Replace the tests of the private helpers _chart_cell_text and
_build_standalone_chart_workbook with tests of the converter output.
A stand-in for LibreOffice keeps the workbook that it gets, so the render
test runs without LibreOffice. The date test now also covers a date with
a time of day.

Signed-off-by: Daniel Nguyen <danielnguyenh07@gmail.com>
A chart range had no size limit. A 6 KB file whose ranges end at row
1,000,000 made the backend read every cell and build a table of
1,000,000 rows. A secondary plot, which is now read, doubled the cost.

- A range without an end row, or with more cells than its share, stops
  at the last used row of its sheet. The last used row is found once
  per sheet.
- The ranges of a chart share MAX_CHART_TABLE_CELLS equally, so a chart
  reads at most that many cells. A range that is cut logs a warning.
- The data table is built with build_chart_table, which trims trailing
  empty rows and caps the table, as in the PowerPoint backend. Dates use
  datetime_to_iso.
- A whole-column range such as $B:$B now reads the used rows. Before,
  it raised an error and the chart was lost.

Signed-off-by: Daniel Nguyen <danielnguyenh07@gmail.com>
A title that is linked to a cell keeps its text in the cell cache
(c:strRef), and openpyxl then gives it an empty rich text. The category
axis title was therefore empty. Read the cache first, as the PowerPoint
backend does. The chart title uses the same code, so a linked chart
title now also becomes the caption.

Signed-off-by: Daniel Nguyen <danielnguyenh07@gmail.com>
@github-actions github-actions Bot removed ai:ci-safe AI triage found no CI-safety concern (advisory) ai:review-lgtm AI first review found no blocker or major issue (advisory) labels Oct 14, 2026
@DanielNg0729

Copy link
Copy Markdown
Member Author

hi Dr @dolfim-ibm, thank you very much for the review, i have added some commit to resolve it. Thanks!

@github-actions github-actions Bot added the ai:ci-safe AI triage found no CI-safety concern (advisory) label Oct 14, 2026

@PeterStaar-IBM PeterStaar-IBM left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm!

@dolfim-ibm
dolfim-ibm merged commit 399b983 into docling-project:main Oct 19, 2026
33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai:ci-safe AI triage found no CI-safety concern (advisory) bug Something isn't working table structure xlsx issue related to xlsx backend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants