From 457023a2c690664cde77bc441fc965dbc8b773bd Mon Sep 17 00:00:00 2001 From: Elias H Aronsson Date: Fri, 3 Jul 2026 20:11:42 -0700 Subject: [PATCH] Chart: emit single-level c:strRef categories and relative chart rel targets Bar/line/area charts wrote single-level string categories as c:multiLvlStrRef, and the slide->chart relationship Target as an absolute /ppt/charts/chartN.xml. Both are valid OOXML that PowerPoint tolerates, but they differ from what PowerPoint itself produces (and from the pie/doughnut category code, which already uses c:strRef). Consequences in other consumers: Google Slides and LibreOffice mis-render the category axis, and strict parsers (e.g. pptxtojson) fail to read the labels or drop the chart entirely. - gen-charts.ts: emit single-level c:strRef when there is one category level; fall back to c:multiLvlStrRef only for genuine multi-level categories. - gen-xml.ts: relativize the chart relationship Target in the slide rels to ../charts/chartN.xml, leaving the absolute Content_Types PartName (which requires the absolute form) unchanged. No change to how PowerPoint renders these charts. --- src/gen-charts.ts | 13 +++++++++++++ src/gen-xml.ts | 7 ++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/gen-charts.ts b/src/gen-charts.ts index 37f27396b..53a49cc23 100644 --- a/src/gen-charts.ts +++ b/src/gen-charts.ts @@ -955,6 +955,19 @@ function makeChartType (chartType: CHART_NAME, data: IOptsChartData[], opts: ICh obj.labels[0].forEach((label, idx) => (strXml += `${encodeXmlEntities(label)}`)) strXml += ' ' strXml += ' ' + } else if (obj.labels.length === 1) { + // Single-level string categories: emit the idiomatic single-level `c:strRef` + // (same form used by the pie/doughnut chart types below). A `c:multiLvlStrRef` + // with a single level is valid but non-idiomatic: PowerPoint tolerates it, but + // Google Slides / LibreOffice mis-render the category axis and strict parsers + // (e.g. pptxtojson) fail to read the labels. + strXml += ' ' + strXml += ` Sheet1!$A$2:$A$${obj.labels[0].length + 1}` + strXml += ' ' + strXml += ` ` + obj.labels[0].forEach((label, idx) => (strXml += `${encodeXmlEntities(label)}`)) + strXml += ' ' + strXml += ' ' } else { strXml += ' ' strXml += ` Sheet1!$A$2:$${getExcelColName(obj.labels.length)}$${obj.labels[0].length + 1}` diff --git a/src/gen-xml.ts b/src/gen-xml.ts index f1e3ecd9f..d3ea3390b 100644 --- a/src/gen-xml.ts +++ b/src/gen-xml.ts @@ -783,7 +783,12 @@ function slideObjectRelationsToXml (slide: PresSlide | SlideLayout, defaultRels: }) ; (slide._relsChart || []).forEach((rel: ISlideRelChart) => { lastRid = Math.max(lastRid, rel.rId) - strXml += `` + // Relationship targets are relative to the slide part, e.g. "../charts/chartN.xml" (what + // PowerPoint writes). The stored `rel.Target` is the absolute "/ppt/charts/chartN.xml" form, + // which is reused verbatim as the absolute Content_Types PartName. Absolute relationship + // targets are valid but non-idiomatic and are mishandled by stricter consumers (e.g. + // pptxtojson resolves them to an invalid path and drops the chart), so relativize here only. + strXml += `` }) ; (slide._relsMedia || []).forEach((rel: ISlideRelMedia) => { const relRid = rel.rId.toString()