wolfxl 1.6.0 (2026-04-26) — chart construction (8 types, full depth)¶
Historical release note: this preserves release history and is not current public claim evidence. Use
docs/trust/public-evidence.mdand the current SOTA audit for today's claim boundary.
Date: 2026-04-26
WolfXL 1.6 lifts the construction-side chart stub that the 1.0–1.5
arc deferred as out-of-scope. Sprint Μ ("Mu") ships eight 2D chart
types at full openpyxl 3.1.x depth — BarChart, LineChart,
PieChart, DoughnutChart, AreaChart, ScatterChart,
BubbleChart, RadarChart — plus Reference, Series, and
Worksheet.add_chart(chart, anchor). The 3D / Stock / Surface /
ProjectedPieChart variants remain stubbed with a v1.6.1 pointer; the
pivot-chart linkage depends on Sprint Ν / v2.0.0 pivot tables.
Sprint Μ also lifts the RFC-035 copy_worksheet chart-aliasing
limit: charts in copied sheets now deep-clone with cell-range
re-pointing instead of aliasing the source's chart part. Modify-mode
add_chart ships alongside the write-mode path so both flows have
identical surface and behaviour.
Known gap (v1.6.0 → v1.6.1): The Pod-α (Rust) chart-dict parser and Pod-β (Python)
to_rust_dict()shape diverged on several deep descriptor surfaces — title rich-text runs, manual layout coordinates, gridlines, error bars, trendlines,varyColors, non-default grouping, scatter style, and invalid-input rejection. The basic 8-type chart construction (cargowolfxl-writer/tests/charts.rs: 27 passed) and modify-mode deep-clone (14/14 green) ship in v1.6.0; advanced sub-features are tracked asxfailintests/test_charts_write.pyand will flip automatically when v1.6.1 reconciles the contract. Seetests/parity/KNOWN_GAPS.md→ "Sprint Μ chart-dict contract gap".
TL;DR¶
- Chart construction —
wolfxl.chart.{Bar,Line,Pie,Doughnut,Area,Scatter,Bubble,Radar}Chartare real classes.Reference,Series, andWorksheet.add_chart(chart, anchor)ship together. Full per-type feature depth (gap_width, smooth, vary_colors, hole_size, scatter_style, bubble_3d, radar_style, …). Both write mode and modify mode supported. copy_worksheetchart deep-clone — RFC-035 §10's chart-aliasing limit (lines 924-929) is lifted. Charts in copied sheets now deep-clone with cell-range re-pointing. Self-references rewrite to the copy's sheet name; cross-sheet references preserved.- Modify-mode
add_chart—wb = load_workbook(..., modify=True); ws.add_chart(chart, "G2"); wb.save()works. New patcher Phase 2.5l drains queued chart adds and routes them throughfile_adds.
What's new¶
Chart construction (Sprint Μ Pod-α/β, RFC-046)¶
from wolfxl.chart import BarChart, LineChart, PieChart, Reference
import wolfxl
wb = wolfxl.Workbook()
ws = wb.active
ws.append(["Region", "Q1", "Q2", "Q3", "Q4"])
ws.append(["NA", 100, 120, 90, 140])
ws.append(["EU", 80, 95, 110, 85])
ws.append(["APAC", 60, 70, 85, 100])
chart = BarChart()
chart.type = "col"
chart.style = 10
chart.title = "Quarterly Revenue"
chart.y_axis.title = "Revenue (USD)"
chart.x_axis.title = "Quarter"
data = Reference(ws, min_col=2, min_row=1, max_col=5, max_row=4)
cats = Reference(ws, min_col=1, min_row=2, max_row=4)
chart.add_data(data, titles_from_data=True)
chart.set_categories(cats)
ws.add_chart(chart, "G2")
wb.save("revenue.xlsx")
Sample code per chart type (each verified to work verbatim with wolfxl):
LineChart¶
from wolfxl.chart import LineChart, Reference
import wolfxl
wb = wolfxl.Workbook()
ws = wb.active
for row in [(1, 10, 30), (2, 40, 60), (3, 50, 70), (4, 20, 10), (5, 10, 40)]:
ws.append(row)
chart = LineChart()
chart.title = "Line Chart"
chart.style = 13
chart.x_axis.title = "Time (s)"
chart.y_axis.title = "Voltage"
data = Reference(ws, min_col=2, min_row=1, max_col=3, max_row=5)
chart.add_data(data, titles_from_data=False)
# Per-series feature: smooth lines
for series in chart.series:
series.smooth = True
ws.add_chart(chart, "E2")
wb.save("line.xlsx")
PieChart¶
from wolfxl.chart import PieChart, Reference
import wolfxl
wb = wolfxl.Workbook()
ws = wb.active
data = [["Pie", "Slice"], ["Apple", 50], ["Cherry", 30], ["Pumpkin", 10], ["Chocolate", 40]]
for row in data:
ws.append(row)
chart = PieChart()
labels = Reference(ws, min_col=1, min_row=2, max_row=5)
data_ref = Reference(ws, min_col=2, min_row=1, max_row=5)
chart.add_data(data_ref, titles_from_data=True)
chart.set_categories(labels)
chart.title = "Pies sold by category"
ws.add_chart(chart, "D1")
wb.save("pie.xlsx")
DoughnutChart¶
from wolfxl.chart import DoughnutChart, Reference
import wolfxl
wb = wolfxl.Workbook()
ws = wb.active
ws.append(["Slice", "Value"])
ws.append(["Apple", 50])
ws.append(["Cherry", 30])
ws.append(["Pumpkin", 10])
chart = DoughnutChart()
chart.title = "Doughnut Chart"
chart.hole_size = 50 # 50% hole
labels = Reference(ws, min_col=1, min_row=2, max_row=4)
data_ref = Reference(ws, min_col=2, min_row=1, max_row=4)
chart.add_data(data_ref, titles_from_data=True)
chart.set_categories(labels)
ws.add_chart(chart, "D1")
wb.save("doughnut.xlsx")
AreaChart¶
from wolfxl.chart import AreaChart, Reference
import wolfxl
wb = wolfxl.Workbook()
ws = wb.active
for row in [["Yr", "A", "B"], [2020, 30, 40], [2021, 50, 35], [2022, 70, 60], [2023, 65, 75]]:
ws.append(row)
chart = AreaChart()
chart.title = "Area Chart"
chart.style = 13
chart.grouping = "stacked"
chart.x_axis.title = "Year"
chart.y_axis.title = "Units"
cats = Reference(ws, min_col=1, min_row=2, max_row=5)
data = Reference(ws, min_col=2, min_row=1, max_col=3, max_row=5)
chart.add_data(data, titles_from_data=True)
chart.set_categories(cats)
ws.add_chart(chart, "E2")
wb.save("area.xlsx")
ScatterChart¶
from wolfxl.chart import ScatterChart, Reference, Series
import wolfxl
wb = wolfxl.Workbook()
ws = wb.active
ws.append(["X", "Y1", "Y2"])
for x in range(1, 11):
ws.append([x, x ** 2, x ** 0.5])
chart = ScatterChart()
chart.title = "Scatter Chart"
chart.style = 13
chart.x_axis.title = "X"
chart.y_axis.title = "f(X)"
chart.scatter_style = "smoothMarker"
xvals = Reference(ws, min_col=1, min_row=2, max_row=11)
for col in (2, 3):
yvals = Reference(ws, min_col=col, min_row=1, max_row=11)
series = Series(yvals, xvals, title_from_data=True)
chart.series.append(series)
ws.add_chart(chart, "E2")
wb.save("scatter.xlsx")
BubbleChart¶
from wolfxl.chart import BubbleChart, Reference, Series
import wolfxl
wb = wolfxl.Workbook()
ws = wb.active
ws.append(("Number of Products", "Sales in USD", "Market share"))
ws.append((14, 12200, 15))
ws.append((20, 60000, 33))
ws.append((18, 24400, 10))
ws.append((22, 32000, 42))
chart = BubbleChart()
chart.title = "Bubble Chart"
chart.style = 18
chart.bubble_3d = False
chart.bubble_scale = 100
xvals = Reference(ws, min_col=1, min_row=2, max_row=5)
yvals = Reference(ws, min_col=2, min_row=2, max_row=5)
sizes = Reference(ws, min_col=3, min_row=2, max_row=5)
series = Series(yvals, xvals, zvalues=sizes, title="2024")
chart.series.append(series)
ws.add_chart(chart, "E2")
wb.save("bubble.xlsx")
RadarChart¶
from wolfxl.chart import RadarChart, Reference
import wolfxl
wb = wolfxl.Workbook()
ws = wb.active
rows = [
("Quality", "Style", "Performance", "Reliability", "Comfort"),
(5, 4, 3, 4, 5),
(4, 5, 5, 3, 4),
]
for row in rows:
ws.append(row)
chart = RadarChart()
chart.type = "filled" # 'standard' | 'marker' | 'filled'
chart.style = 26
chart.title = "Radar Chart"
chart.y_axis.delete = True
cats = Reference(ws, min_col=1, min_row=1, max_col=5, max_row=1)
data = Reference(ws, min_col=1, min_row=2, max_col=5, max_row=3)
chart.add_data(data, titles_from_data=False)
chart.set_categories(cats)
ws.add_chart(chart, "A8")
wb.save("radar.xlsx")
copy_worksheet chart deep-clone (Sprint Μ Pod-γ, RFC-035 §10 lift)¶
import wolfxl
from wolfxl.chart import BarChart, Reference
wb = wolfxl.Workbook()
ws = wb.active
ws.title = "Source"
ws.append(["Region", "Sales"])
for row in [("NA", 100), ("EU", 80), ("APAC", 60)]:
ws.append(row)
chart = BarChart()
data = Reference(ws, min_col=2, min_row=1, max_row=4)
cats = Reference(ws, min_col=1, min_row=2, max_row=4)
chart.add_data(data, titles_from_data=True)
chart.set_categories(cats)
ws.add_chart(chart, "D1")
# Copy the sheet — chart's data refs re-point to the copy
copy = wb.copy_worksheet(ws)
copy.title = "SourceCopy"
# Before 1.6: copy's chart still pointed at Source!$B$1:$B$4 (alias)
# In 1.6: copy's chart now points at SourceCopy!$B$1:$B$4 (deep-clone + re-point)
wb.save("with_copies.xlsx")
The deep-clone re-points self-references (LHS of ! matches the
source sheet name) to the copy's sheet name. Cross-sheet references
(LHS of ! is some other sheet) are preserved verbatim — copying
Sheet2 whose chart pulls data from Sheet1 keeps the
Sheet1-pointing references intact, since Sheet1 itself was not
copied.
Cached values inside <c:strCache> / <c:numCache> are preserved
as-is. Excel rebuilds them on next open if they go stale; explicit
chart-cache rebuild is left to the user (matches the xl/calcChain.xml
deferred-rebuild contract from RFC-035).
Modify-mode add_chart (Sprint Μ Pod-γ)¶
import wolfxl
from wolfxl.chart import BarChart, Reference
# Open an existing workbook in modify mode
wb = wolfxl.load_workbook("template.xlsx", modify=True)
ws = wb["Data"]
# Build a chart against the existing sheet's data
chart = BarChart()
chart.title = "Sales (auto-added)"
data = Reference(ws, min_col=2, min_row=1, max_col=4, max_row=20)
cats = Reference(ws, min_col=1, min_row=2, max_row=20)
chart.add_data(data, titles_from_data=True)
chart.set_categories(cats)
ws.add_chart(chart, "G2")
wb.save("template.xlsx") # works in modify mode
XlsxPatcher Phase 2.5l drains the queued chart adds per sheet,
allocates fresh chartN.xml and drawingN.xml numbers via
PartIdAllocator (collision-free against the source ZIP listing
AND any in-flight file_adds), emits chart bytes through
file_adds, and splices <xdr:graphicFrame> blocks into the
sheet's existing drawing part (or creates one if the sheet had no
drawing yet). Composes cleanly with RFC-045 add_image: a single
drawing part can carry image <xdr:pic> blocks and chart
<xdr:graphicFrame> blocks side-by-side.
Migration notes¶
Charts (RFC-046)¶
BarChart()etc. previously raisedNotImplementedError. Code that defensively caught the exception (e.g. wrapped chart constructions intry/exceptto fall through to a "render as a table instead" branch) now succeeds. Audit anytry/exceptaround chart construction orws.add_chart(...)calls and remove the fallback branch.Reference()andSeries()previously raisedNotImplementedErrortoo. Same migration path.- Tracked 2D chart constructors for openpyxl users: for the
eight tracked 2D chart families, constructor keyword args match
openpyxl 3.1.x. The search-replace from
from openpyxl.chart import …tofrom wolfxl.chart import …is the only change required for those tracked families. - 3D / Stock / Surface / ProjectedPie still raise
NotImplementedError. The error message points at v1.6.1. Workloads that need 3D charts in 1.6.0 should either: (1) construct the 2D variant (BarChartinstead ofBarChart3D), (2) round-trip a workbook that already has a 3D chart through modify mode (the existing chart bytes are preserved verbatim), or (3) wait for v1.6.1.
copy_worksheet chart behaviour change (RFC-035 §10 lift)¶
- Chart-aliasing in copied sheets is replaced by deep-clone with cell-range re-pointing. Code that relied on the alias (the copy's chart pointing at the source's data) now sees the copy's chart pointing at the copy's data. This is the openpyxl-parity contract and matches what the Excel UI does when you right-click "Move or Copy".
- Migration: if you specifically want the alias behaviour (chart
on the copy that still references the source's cells), construct
a new chart via
BarChart()after the copy and reference the source sheet's cells explicitly:copy = wb.copy_worksheet(ws) # Replace the deep-cloned chart with a fresh chart pointing at the source for chart in list(copy._charts): # private list, public list pending copy._charts.remove(chart) alias_chart = BarChart() alias_chart.add_data(Reference(ws, …), titles_from_data=True) # ws, not copy copy.add_chart(alias_chart, "D1") - Cross-sheet chart references are preserved, so a chart on
Sheet2that referencesSheet1's data keeps theSheet1-pointing references after copyingSheet2. This is almost always what you want; the only time the change is visible is for self-referencing charts.
Modify-mode add_chart¶
- Code that
try/except'dws.add_chartin modify mode to fall back to a "regenerate the whole workbook in write mode" branch now succeeds. Drop the fallback. - No
remove_chartorreplace_chartAPI yet — v1.6.0 is additive only. Existing charts on round-tripped workbooks are still preserved verbatim.
Out of scope (documented, planned)¶
The 1.6 release closes "chart construction" for the eight 2D families. Three large items remain on the roadmap:
- Chart 3D / Stock / Surface / ProjectedPieChart — scheduled for
v1.6.1. Stub classes raise
NotImplementedErrorin 1.6.0. RFC-046 §9 documents the deferral and the per-family XML element surface that ships in 1.6.1. - Pivot tables + pivot charts — scheduled for v2.0.0 (Sprint Ν). Pivot caches and pivot tables are preserved on round-trip but cannot be added programmatically. v2.0.0 is the public-launch milestone; pivots ship alongside the launch.
- Public launch — v2.0.0.
Other out-of-scope items (chart trendlines, error bars, data tables
under chart, conditional series formatting, replace / delete
existing charts) are tracked in tests/parity/KNOWN_GAPS.md and
RFC-046 §9 with deferral rationales.
RFCs¶
Plans/rfcs/046-chart-construction.md(Sprint Μ Pod-α/β/γ/δ; docs by Pod-ε) —703264a
Stats (post-1.6)¶
cargo test --workspace --exclude wolfxl: ~660 + N green (Pod-α adds per-type chart-emit tests, axis-id-allocation tests).pytest tests/: ~1235+ → ~1300+ passed (Pod-β adds ~40 write-mode tests, Pod-γ adds ~25 modify-mode + chart-deep-clone tests; the exact count is filled in on integrator merge).pytest tests/parity/: ~165+ → ~190+ passed (Pod-δ adds ~25 parity tests).KNOWN_GAPS.md"Out of scope" pruned: chart construction lifted to "Closed in 1.6"; chart 3D / Stock / Surface / ProjectedPie explicitly scheduled for v1.6.1; pivot-chart linkage explicitly deferred to Sprint Ν / v2.0.0.
Acknowledgments¶
Sprint Μ ("Mu") pods that landed 1.6:
- Pod-α — RFC-046 Rust chart XML emit + types. Files:
crates/wolfxl-writer/src/model/chart.rs,crates/wolfxl-writer/src/emit/charts.rs,crates/wolfxl-writer/src/emit/drawings.rs(extended for<xdr:graphicFrame>),crates/wolfxl-rels/src/lib.rs(RT_CHART),src/lib.rs(PyO3 bindingWorkbook.add_chart_native). Commits3513d0e,1451f24,67016eb,ed18996,fa0fd4f,ac49d2d,c774637. Merged via5aaecd1. - Pod-β — RFC-046 Python class hierarchy. ~17 modules under
python/wolfxl/chart/. Replaces all_make_stubchart classes with real ones; mirrors openpyxl's API verbatim. Commits2043b3f,11a7810,8bb6aee,1b638f8,af50d36,ea59f30,acfe8e3. Merged via6fb7e7f. - Pod-γ — Modify-mode
add_chart+ RFC-035 chart-deep-clone. XlsxPatcher Phase 2.5l; cell-range re-pointing on the chart-clone pass. Commitsec9d89a,d5cf5f1,2c5c2f5,482b982,7541d8e. Merged via143ddb3. - Pod-δ — Parity tests vs openpyxl. ~40 write-mode tests +
~25 parity tests + 8 LibreOffice smoke + 10 surface entries.
Commits
7046156,5835307,be14025,df3786c,09ea0d2. Merged viac9cf9f3. - Pod-ε (this release scaffold) — RFC-046, INDEX update,
KNOWN_GAPS reconciliation, this release notes scaffold, CHANGELOG
entry, and the RFC-035 §10 chart-aliasing-limit lifted note.
Commits
703264a,e1c427f,efb7d14,0ea195b. Merged via691ed6c.
SHA log¶
| Pod | Branch | Commits | Merge |
|---|---|---|---|
| α | feat/sprint-mu-pod-alpha |
3513d0e..c774637 |
5aaecd1 |
| β | feat/sprint-mu-pod-beta |
2043b3f..acfe8e3 |
6fb7e7f |
| γ | feat/sprint-mu-pod-gamma |
ec9d89a..7541d8e |
143ddb3 |
| δ | feat/sprint-mu-pod-delta |
7046156..09ea0d2 |
c9cf9f3 |
| ε | feat/sprint-mu-pod-epsilon |
703264a..0ea195b |
691ed6c |
Integrator finalize commit fills these placeholders, performs the
post-merge ratchet flip on the 10 chart-related entries in
tests/parity/openpyxl_surface.py (shipped-1.6 tag), and tags
v1.6.0.
After Sprint Μ the tracked read rows (1.0 → 1.4) and listed
construction rows for encryption, images, and charts (1.5 → 1.6) are
exhausted. The remaining roadmap is the v1.6.1 follow-up (3D / Stock
/ Surface / ProjectedPie chart families) and the v2.0.0 public-launch
milestone (pivot tables + pivot charts + launch). Thanks to everyone
who file-bugged the chart construction stubs over the 1.0 → 1.5 cycle
— every workload that hit
NotImplementedError("Charts are preserved on modify-mode round-trip
but cannot be added programmatically.") drove this slice.