From 5422f2c3e8318817eabe1db2946779978d1e522a Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 30 May 2026 08:57:12 -0400 Subject: [PATCH] Draft PPTX chart-import spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/CHART_IMPORT_FEATURE.md | 372 +++++++++++++++++++++++++++++++++++ 1 file changed, 372 insertions(+) create mode 100644 docs/CHART_IMPORT_FEATURE.md diff --git a/docs/CHART_IMPORT_FEATURE.md b/docs/CHART_IMPORT_FEATURE.md new file mode 100644 index 0000000..926cab8 --- /dev/null +++ b/docs/CHART_IMPORT_FEATURE.md @@ -0,0 +1,372 @@ +# 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:barChart` with `c:barDir val="col"` or + `val="bar"`). Single or multiple series. Clustered grouping only. +- Read category labels from `c:cat/c:multiLvlStrRef/c:multiLvlStrCache` + or `c:cat/c:strRef/c:strCache`. Read numeric values from + `c: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 of `PPTX_IMPORT_FEATURE.md` can refine this with the + chart's own `c:layout/c:manualLayout` later. +- Emit each bar as a `rect` shape 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 a + `kind: '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 one `kind: 'path'` polyline). Markers (`c:marker`) emit + small `circle`/`rect` shape 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 on `c: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:plotArea` containing 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`: + +- `convertGraphicFrame` first 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 `chartRels` to `ctx` (same shape as `slideRels`) 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 `` 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: + +1. Explicit `c:ser/c:spPr/a:solidFill/a:srgbClr` (most decks). +2. Explicit `c:ser/c:spPr/a:solidFill/a:schemeClr` — resolves via + the slide's theme (the existing `convertColor(node, theme)` + helper handles this). +3. 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: `). 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 += ` +- `report.imported.text += ` +- `report.placeholders.charts` stays 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): + +1. **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. +2. **Single-series, 6-category column chart** with very different + values (1, 10, 100, 1000, …) to test axis-scale math. +3. **Empty / missing strCache** — chart that references a + spreadsheet but has no cached data. Expected: fall through to + placeholder + `CHART_TYPE_UNSUPPORTED` warning. +4. **3-slice pie chart** (Phase 2 milestone). +5. **2-series line chart** (Phase 3 milestone). +6. **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 `convertGraphicFrame` so chart URIs try the native + builder first. +- Synthetic test 1 + real-deck slide 10 must pass. + +### Phase 2 — pie / doughnut + +- Add `buildPieChartLayers`. +- Add `sliceArcPath` helper. +- Synthetic test 4 must pass. + +### Phase 3 — line + +- Add `buildLineChartLayers`. +- Markers if `c:marker/c:symbol` is 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 `walkSpTree` per Phase 5 of + `PPTX_IMPORT_FEATURE.md`). For now chart sub-layers will be + individual editable layers with the `Chart 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.pptx` produces slide 10 + with 8 native bar shape layers in `#5E735D` and `#B05A44`, plus + 4 `Q1..Q4` text 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`).