Spec for the next big arc: replacing the current chart-placeholder behaviour (single image layer at PPTX_IMAGE_PLACEHOLDER_URL) with native rendering — bars / wedges / lines as Artstr shape layers, labels as text layers. Branch pptx-chart-spec. Scope decisions baked in: - Phase 1: clustered column + bar charts only. Bars as rect shape layers, category labels as text. Plot area derived via a fixed inset on the chart's graphicFrame bounding box (no c:manualLayout reading until Phase 4). - Phase 2: pie / doughnut. Slice geometry emitted as kind:'path' with SVG arc d-attributes. - Phase 3: line charts with optional markers. - Phase 4: title / legend / data labels / value-axis ticks / gridlines. - Phase 5: stacked + percent-stacked variants. - Deferred to placeholder: scatter, bubble, area, radar, 3D, combo charts, trendlines, error bars, per-point overrides. Architecture: - New src/pptx-importer-charts.js with the per-type builders, loaded as an addendum to pptx-importer.js (or inlined — TBD during implementation; the dependency budget is small enough that a single file might be cleaner). - convertGraphicFrame in pptx-importer.js gets a "try native before placeholder" branch: parse chart XML, dispatch on type, fall through to the existing placeholder + warning when the builder returns null. - All chart data read from c:strCache / c:numCache. The embedded ppt/embeddings/*.xlsx workbook is ignored — caches are reliable in OOXML output and reading XLSX is its own rabbit hole. Acceptance criteria for Phase 1: gallery_template_layout_set.pptx slide 10 imports with 8 native bar layers (#5E735D + #B05A44 clustered across Q1-Q4) plus 4 category-label text layers, matching what PowerPoint shows. Scatter / 3D / etc. still placeholder cleanly without crash.
12 KiB
Artstr Studio PPTX Chart Import
Native rendering of OOXML charts into Artstr layers
Status
Spec draft, not yet implemented. Follows on from
docs/PPTX_IMPORT_FEATURE.md, which currently lands every chart as
a single placeholder image layer (the UNSUPPORTED_CHART_PLACEHOLDER
path in §11.4 of that spec). This document scopes a phased
replacement that turns a real OOXML chart into a set of native
Artstr layers — shape layers for bars / wedges / segments, text
layers for axis labels / titles / legends.
1. Why this is its own arc
A chart in a PPTX package is its own sub-document:
ppt/slides/slide10.xml ← references the graphic frame
ppt/slides/_rels/slide10.xml.rels
ppt/charts/chart1.xml ← chart XML (this spec)
ppt/charts/_rels/chart1.xml.rels
ppt/embeddings/*.xlsx ← the underlying spreadsheet (cached)
The chart XML has its own ~30 element vocabulary (c:chartSpace,
c:barChart, c:pieChart, c:ser, c:cat, c:val, c:dLbls,
c:catAx, c:valAx, …) and its own internal coordinate system
(0–1 plot-area relative). Folding all of that into
pptx-importer.js alongside the slide-walker would balloon the
file; the chart converter gets its own helpers and is gated by the
existing convertGraphicFrame dispatch.
2. Scope
In scope for the first cut (Phase 1)
- Bar / column charts (
c:barChartwithc:barDir val="col"orval="bar"). Single or multiple series. Clustered grouping only. - Read category labels from
c:cat/c:multiLvlStrRef/c:multiLvlStrCacheorc:cat/c:strRef/c:strCache. Read numeric values fromc:val/c:numRef/c:numCache. Ignore the spreadsheet reference path — the cache is always present in OOXML output. - Per-series fill colour from
c:ser/c:spPr/a:solidFill/a:srgbClr, falling back to a small built-in palette when missing (matches the default Office theme order: accent1..6). - Compute the plot area inside the chart's bounding box (the
p:graphicFrame/p:xfrm) using a fixed inset for axis labels. Phase 6 ofPPTX_IMPORT_FEATURE.mdcan refine this with the chart's ownc:layout/c:manualLayoutlater. - Emit each bar as a
rectshape layer with the series fill. - Emit category axis labels (Q1, Q2, …) as text layers below the plot area.
- Skip value-axis tick labels, title, legend, data labels, gridlines in Phase 1 — they all land in Phase 4 polish.
Phase 2
- Pie / doughnut charts (
c:pieChart,c:doughnutChart). Each slice becomes a shape layer. The slice geometry is a wedge that Artstr doesn't natively support as a preset; rendered as akind: 'path'shape with an SVG arc-path d-attribute. Doughnut draws an inner-radius hole via even-odd fill rule on the path.
Phase 3
- Line charts (
c:lineChart). Each data point is a vertex; the series renders as a sequence of line shape layers between adjacent points (or onekind: 'path'polyline). Markers (c:marker) emit smallcircle/rectshape layers at each point.
Phase 4 — polish
- Chart title (
c:title/c:tx/c:rich/a:p/a:r). - Legend (
c:legend) — series name + colour swatch as paired text-layer + shape-layer. - Data labels (
c:dLbls) — value or category text above / inside each bar / wedge based onc:showVal/c:showCatName/c:showPercent. - Value-axis tick labels with auto-rounding to a nice step (5, 10, 20, 50, 100).
- Gridlines (
c:majorGridlines/c:minorGridlines) — thin shape layers at each tick.
Phase 5
- Stacked + percent-stacked variants of bar / column / line
(
c:grouping val="stacked"/"percentStacked"). - Stacked rendering is sum-aware: bar heights are the running sum per category and the per-series rect is the delta segment.
Out of scope (kept as placeholder images)
- Scatter (
c:scatterChart), bubble (c:bubbleChart), area (c:areaChart), radar (c:radarChart), surface (c:surface3DChart), stock (c:stockChart). - All 3D variants (
c:bar3DChart,c:line3DChart,c:pie3DChart). - Combo charts (
c:plotAreacontaining multiple different chart types). - Trendlines (
c:trendline), error bars (c:errBars). - Conditional formatting / per-point overrides (
c:dPt).
Anything in this list keeps the current
UNSUPPORTED_CHART_PLACEHOLDER behavior from §11.4 of the PPTX
spec.
3. Target architecture
convertGraphicFrame(gfNode, ctx)
uri === '…/chart'
├─ resolve r:id to ppt/charts/chartN.xml
├─ parseXml(chartText)
├─ chartImport.convertChart(chartDoc, gfBounds, ctx)
│ ├─ read plot area bounds from gfBounds (with insets)
│ ├─ dispatch on chart type:
│ │ barChart → buildBarChartLayers(plotArea, chartDoc, ctx)
│ │ pieChart → buildPieChartLayers(...)
│ │ lineChart → buildLineChartLayers(...)
│ │ else → null (fall through to placeholder)
│ └─ return Artstr layer array (or null)
└─ if null: emit the existing placeholder image as before.
New file: src/pptx-importer-charts.js — exports
window.ArtstrPptxImporter.charts with the per-chart-type
builders. Lazy-loaded inside the existing pptx-importer.js
flow on first chart encountered; not a separate user-facing
button.
Update pptx-importer.js:
convertGraphicFramefirst parses the chart XML and tries the native builder. If it returns layers, emit them in place of the placeholder. If it returns null (unsupported type), keep the current placeholder behaviour + warning.- Add
chartRelstoctx(same shape asslideRels) to resolve the chart-XML relationship from the slide rels file.
4. Coordinate system inside a chart
PPTX charts use a 0–1 relative coord system inside their bounding
box. The chart's bounding box on the slide is the
p:graphicFrame/p:xfrm/a:off + a:ext, which we already convert to
Artstr inches in convertGraphicFrame via readXfrm.
For Phase 1 we use a fixed inset model:
plotArea = {
x: chartBounds.x + chartBounds.w * 0.05,
y: chartBounds.y + chartBounds.h * 0.05,
w: chartBounds.w * 0.90,
h: chartBounds.h * 0.80, // leaves 15% for category labels below
};
Phase 4 will read c:plotArea/c:layout/c:manualLayout when present
to get the chart's actual plot-area placement.
5. Bar / column chart layout
Given a clustered column chart with N categories × S series, plot
area pa = { x, y, w, h }, and series values data[s][c]:
maxV = max over all data
catW = pa.w / N
gap = catW * 0.15 // ~15% gap between category groups
barW = (catW - gap) / S // S bars per category
for c in 0..N-1:
for s in 0..S-1:
h_bar = pa.h * data[s][c] / maxV
x_bar = pa.x + c * catW + gap/2 + s * barW
y_bar = pa.y + pa.h - h_bar
emit rect layer (x_bar, y_bar, barW, h_bar) filled with series colour
Bar charts (c:barDir val="bar") swap axes: categories on Y, value
extending in X. The same math applies with x/y swapped.
For "clustered" grouping only in Phase 1 — stacked and
percentStacked are Phase 5.
6. Pie chart layout (Phase 2)
Given a pie chart with K slices summing to T, plot area pa:
cx = pa.x + pa.w / 2
cy = pa.y + pa.h / 2
r = min(pa.w, pa.h) / 2 * 0.9 // 90% of half-min, leaves margin
angle = 0
for k in 0..K-1:
sweep = 2π * values[k] / T
d = sliceArcPath(cx, cy, r, angle, angle + sweep)
emit shape layer:
kind: 'path'
d: d
viewBox: { x: 0, y: 0, w: 100, h: 100*pa.h/pa.w }
fill: slice colour
angle += sweep
sliceArcPath builds an SVG <path d="..."> with M, L,
arc commands. Doughnut adds an inner-radius arc subtracted via
even-odd fill rule.
7. Series colour resolution
PPTX charts pick series colours in this order:
- Explicit
c:ser/c:spPr/a:solidFill/a:srgbClr(most decks). - Explicit
c:ser/c:spPr/a:solidFill/a:schemeClr— resolves via the slide's theme (the existingconvertColor(node, theme)helper handles this). - The theme's chart-style colour rotation (
accent1,accent2, …) when neither is specified — falls back to a built-in palette matching the Office default.
The user's gallery_template_layout_set.pptx chart uses option 1
(explicit #5E735D, #B05A44), so Phase 1 should resolve to the
exact colours seen in PowerPoint.
8. Layer naming
So users can find chart pieces in the layer panel:
Chart placeholder: Chart 1 ← (current; replaced)
Chart 1 / Plot background
Chart 1 / Bar [Q1, Baseline] ← series s, category c
Chart 1 / Bar [Q1, Target]
…
Chart 1 / Bar [Q4, Target]
Chart 1 / Category label: Q1
…
Chart 1 / Category label: Q4
Chart 1 matches the source c:cNvPr@name (or
Chart placeholder: <name>). Square brackets contain the
category + series for bars / line points, the slice name for pie
wedges.
9. Report integration
Reuse the existing report counters:
report.imported.shapes += <num shapes emitted from chart>report.imported.text += <num text layers from chart>report.placeholders.chartsstays as the unsupported-fallback counter. Successfully-imported native charts don't bump this.
New warning codes:
CHART_TYPE_UNSUPPORTED— chart type not yet handled (scatter, 3D, combo, etc.), falling back to placeholder.CHART_AXIS_LABELS_SKIPPED_PHASE1— value-axis ticks deferred to Phase 4.CHART_GRIDLINES_SKIPPED_PHASE1— same.
10. Test plan
Build synthetic charts in-browser via fflate.zipSync (the same
pattern as the existing /tmp/test_pptx_*.js harnesses):
- 2-series, 4-category clustered column chart, explicit srgbClr per series. Mirrors the user's test deck (slide 10). Expected: 8 rect shape layers + 4 category-label text layers.
- Single-series, 6-category column chart with very different values (1, 10, 100, 1000, …) to test axis-scale math.
- Empty / missing strCache — chart that references a
spreadsheet but has no cached data. Expected: fall through to
placeholder +
CHART_TYPE_UNSUPPORTEDwarning. - 3-slice pie chart (Phase 2 milestone).
- 2-series line chart (Phase 3 milestone).
- Scatter chart — expected: placeholder + warning, no crash.
Real-deck regression: gallery_template_layout_set.pptx slide 10's
chart should now appear with 8 bars (green + terracotta clustered
across Q1–Q4) and 4 category labels instead of the postimg
placeholder.
11. Implementation plan
Phase 1 — bar / column
- Create
src/pptx-importer-charts.js. - Implement
convertChart(chartDoc, chartBounds, ctx)dispatch. - Implement
buildBarChartLayers. - Read series / categories / values from cache.
- Wire into
convertGraphicFrameso chart URIs try the native builder first. - Synthetic test 1 + real-deck slide 10 must pass.
Phase 2 — pie / doughnut
- Add
buildPieChartLayers. - Add
sliceArcPathhelper. - Synthetic test 4 must pass.
Phase 3 — line
- Add
buildLineChartLayers. - Markers if
c:marker/c:symbolis set and!= none. - Synthetic test 5 must pass.
Phase 4 — polish
- Title, legend, data labels, value-axis ticks, gridlines.
Phase 5 — stacked variants
- Stacked + percent-stacked column / bar / line.
12. Open questions
- Group layers. Artstr doesn't expose a "layer group" primitive
today (PPTX groups are flattened by
walkSpTreeper Phase 5 ofPPTX_IMPORT_FEATURE.md). For now chart sub-layers will be individual editable layers with theChart N / …naming convention. If Artstr later adds groups, the importer can opt-in. - Editing after import. A user might want to "change a value" on an imported chart. Today that means moving a bar manually. True data-bound editing would require keeping the source values in a hidden layer-level field and re-deriving bars on change — out of scope for the initial arc.
- Animated reveals / build-by. PPTX charts can specify
per-series animation. Artstr doesn't render slide animations, so
these get ignored uniformly with the rest of
p:timing.
13. Acceptance criteria for first release (Phase 1 done)
- Importing
gallery_template_layout_set.pptxproduces slide 10 with 8 native bar shape layers in#5E735Dand#B05A44, plus 4Q1..Q4text labels, instead of a single placeholder image. - Synthetic 2-series 4-category test deck imports the same.
- Synthetic scatter / 3D / area decks still produce the placeholder image + warning (no regression on the existing unsupported-chart path).
- Pure-Node unit tests cover the cache-extraction helpers
(
readCategoryLabels,readSeriesValues,readSeriesColor).