Metadata-Version: 2.4
Name: docxchart
Version: 0.4.1
Summary: Edit the data behind Word charts without disturbing the formatting.
Author-email: Max Kemmler <106467576+Kemmler1@users.noreply.github.com>
License: MIT
Project-URL: Homepage, https://github.com/Kemmler1/docxchart
Project-URL: Repository, https://github.com/Kemmler1/docxchart
Project-URL: Issues, https://github.com/Kemmler1/docxchart/issues
Keywords: docx,word,chart,ooxml,openxml,office
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business
Classifier: Topic :: Text Processing :: Markup
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: lxml>=4.9
Requires-Dist: openpyxl>=3.1
Dynamic: license-file

# docxchart

Edit the data behind Word charts without disturbing the formatting.

Extract every chart's values from a `.docx` to JSON, edit the JSON, apply it
back. Fills, label positions, axis settings and theme bindings are never
touched.

**Status:** early. Handles same-length value edits on standard category charts,
and setting the input cells of embedded Excel worksheets (including dropdowns).
Anything it can't handle raises rather than guessing — see [Limitations](#limitations).

## Install

```bash
pip install docxchart
```

Requires Python 3.11+.

## CLI

```bash
docxchart list    report.docx
docxchart extract report.docx -o data.json
docxchart apply   report.docx --data data.json -o report-updated.docx
```

| Command | |
|---|---|
| `list FILE` | chart keys, titles, and shape |
| `extract FILE [-o OUT]` | write chart data to JSON (default `data.json`) |
| `apply FILE --data JSON [-o OUT]` | write edited data back to a new docx |
| `--sidecar` | on `extract`, also emit the internal manifest |

`apply` never writes in place. Omit `-o` and it errors.

## Data format

```json
{
  "_docxchart": {
    "source": "report.docx",
    "basedOn": "sha256:1f3a…",
    "extractedAt": "2026-07-29T09:14:22Z",
    "formatVersion": "1.0"
  },
  "charts": [
    {
      "key": "revenue_by_quarter",
      "_meta": {
        "title": "Revenue by Quarter",
        "type": "barChart",
        "keyStable": true,
        "readOnly": true
      },
      "series": [
        { "name": "2024", "formatCode": "#,##0" },
        { "name": "2025", "formatCode": "#,##0" }
      ],
      "rows": [
        { "category": "Q1", "2024": 120,  "2025": 130 },
        { "category": "Q2", "2024": null, "2025": 110 },
        { "category": "Q3", "2024": 95,   "2025": 99  }
      ]
    }
  ]
}
```

| Field | |
|---|---|
| `key` | chart identifier (see [Naming](#naming)) |
| `_meta` | read-only. `apply` errors if changed. |
| `series` | display order of the series. Order matters for legend and stacking. |
| `rows` | one row per category. Series values keyed by series name. |
| `basedOn` | hash of the source docx. `apply` refuses if the file has changed since extract. |

`null` is a genuine blank in the chart, rendered as a gap. It is **not** zero.
Omitting a key means the same thing.

Dates are stored as Excel serial numbers with a `formatCode`, and are passed
through unconverted.

## Python API

```python
from docxchart import ChartDoc

doc = ChartDoc.open("report.docx")

list(doc.charts)
# ['revenue_by_quarter', 'headcount']

chart = doc.charts["revenue_by_quarter"]
chart.table
# ChartTable(
#     categories=['Q1', 'Q2', 'Q3'],
#     series=[Series(name='2024', values=[120, None, 95])],
# )

chart.table.series[0].values = [130, 110, 99]   # values (same count)
chart.table.series[0].name   = "FY2024"          # series name / legend text
chart.table.categories[1]    = "Q2 (revised)"    # category label (same count)
chart.table.hide_legend_entry(3)                 # suppress an empty series' legend entry

chart.table.insert_category(1, "Q1.5")           # add a row (every series gets a blank)
chart.table.series[0].values[1] = 88             # then fill it
chart.table.delete_category(4)                    # remove a row

doc.validate()               # -> list[Finding]
doc.save("report-updated.docx")
```

`ChartTable` and `Series` are plain dataclasses. No XML is exposed.

Category **labels** are editable (the count is fixed — adding/removing rows is
not supported). A label write fans out to every series' `c:cat` and is mirrored
into the embedded workbook; `None` is a genuine blank, and labels round-trip
byte-exactly (trailing spaces and footnote markers are preserved).

`hide_legend_entry(index)` / `show_legend_entry(index)` (or
`series.legend_hidden = True`) suppress or restore a series' legend entry via
`c:legendEntry/c:delete` — useful when a template carries a spare, empty series
that would otherwise show a blank rater in the legend.

Category rows can be **added and removed** with `insert_category(index, label)`
and `delete_category(index)` (programmatic path only). This resizes every
series' cache, rewrites the `c:f` ranges, inserts/deletes the embedded-workbook
row, and shifts per-point formatting (`c:dPt`/`c:dLbl`). A chart carrying overlay
shapes (`c:userShapes`) is **refused** — a resize moves the plot area out from
under absolutely-positioned annotations — unless you pass `force=True`.

## Embedded worksheets

Some documents embed a live Excel worksheet as an **OLE object** (an
`Excel.Sheet` object) rather than a chart — often a small "raw scores in,
converted results out" table. `docxchart` can set the **input cells** of these
programmatically, alongside charts.

```
docxchart list    report.docx      # embedded worksheets show up too
docxchart extract report.docx -o data.json
docxchart apply   report.docx --data data.json -o report-updated.docx
```

`extract` adds a `worksheets` section:

```json
{
  "worksheets": [
    {
      "key": "worksheet_1",
      "_meta": { "progId": "Excel.Sheet.12", "sheet": "Sheet1", "readOnly": true },
      "cells":    { "A1": 40, "A2": 20, "B1": "NEUROTICISM" },
      "_derived": { "C1": "=NORM.DIST(A1, 50, 10, TRUE) * 100" }
    }
  ]
}
```

- `cells` are the **constant** (non-formula) cells — the editable inputs.
- `_derived` lists the formula cells for context. It is **read-only**: those
  values are computed by Excel, not by you. `apply` errors if you change one, or
  if you put a formula cell in `cells`.
- `_dropdowns` (when present) lists each **list-validated** cell and its allowed
  values. You still set the value in `cells`, but it must be one of the options —
  `apply` refuses anything else (e.g. a CliftonStrengths cell that only accepts
  the 34 theme names).

```python
doc = ChartDoc.open("report.docx")
ws = doc.worksheets["worksheet_1"]
ws.set_cell("A1", 63)                 # one input cell
ws.set_cells({"A2": 11, "A3": 50})    # several
doc.save("report-updated.docx")
```

Only cell **values** change; conditional formatting, formulas, number formats
and every other byte are preserved (the inner `.xlsx` is edited surgically, not
reserialized).

**One thing to know about the display.** What Word draws for an embedded object
is a cached picture, and the formula cells (`C`/`D`/`E`…) are recomputed by
Excel — neither refreshes just from editing the data. After `apply`, **activate
the object in Word once** (double-click it, or update fields / print) and Excel
recomputes the formulas and regenerates the picture. `docxchart` sets
`fullCalcOnLoad` so that recompute is guaranteed when it happens; it cannot
regenerate the picture itself (that needs an Office engine).

Editing input cells that don't exist yet (extending the input range) is not
supported yet — it raises rather than guessing a new cell's formatting.

To inspect one of these documents — inputs, formulas, dropdown options, and the
cached preview picture — and to see for yourself that editing the data leaves
the picture untouched until Word re-renders:

```bash
python scripts/probe_worksheet.py report.docx
python scripts/probe_worksheet.py report.docx --set C2=Strategic -o out.docx
```

## Naming

Charts are identified by their **Alt Text**, which survives Word re-saves,
insertions and deletions — unlike file paths and relationship IDs.

1. Right-click the chart → **Edit Alt Text**
2. Enter a key, e.g. `revenue_by_quarter`

Without Alt Text, the key falls back to the chart's title, slugified. This
works, but the key changes if anyone edits the title. `list` marks these as
unstable. Duplicate titles are suffixed by document order: `revenue`,
`revenue-2`.

## Limitations

Unsupported cases raise `UnsupportedChart` or `UnsupportedEdit` with a reason.
Nothing is written on a partial or best-guess basis.

Not supported:

- Adding or removing categories via the JSON `apply` path — use the
  programmatic `insert_category` / `delete_category` instead (row resize is
  refused on charts with `c:userShapes` unless forced)
- Adding or removing series
- Scatter and bubble charts
- `chartEx` types: waterfall, treemap, sunburst, funnel, histogram, box &
  whisker, map
- Multi-level category axes
- Charts in headers, footers or footnotes
- Charts bound to an external data source
- Any change to appearance — colours, fonts, chart type, axis config and layout
  are read-only (suppressing a legend *entry* is supported; other legend
  styling is not)

Charts must be created in Word. `docxchart` only rebinds data on charts that
already exist.

## Notes

Word renders from the chart's cached values, not from the embedded workbook, but
the workbook is what you see when you click *Edit Data*. `docxchart` always
writes both, so the two never disagree.

Parts of the package that weren't changed come out byte-identical.

## License

MIT — see [LICENSE](LICENSE).
