@openpresentation/opf-pptx 0.11.5 → 0.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # OPF PPTX
2
2
 
3
- Version 0.11.5 writes the slide tag run as a theme color reference (`a:schemeClr accent1`) where the deck theme holds the primary there (no API change; the color is unchanged). Version 0.11.4 re-imports quote payloads (each native quote line carries an `OPF_QUOTE_V1` tag) and slide-image payloads from an unchanged export, writes the theme's `a:ea` and `a:cs` only for slots a script font is selected for, and requires core `^0.11.3` (the 70 legacy gallery layout ids and their geometry; install it with renderer 0.11.6 and editor 0.10.4 so export and preview resolve the same core; no API removed). Version 0.11.3 adds the opt-in `toPptx({ chartex: 'native' })` export of the seven chartex chart types and always imports chartex charts (no API removed; the default output is unchanged). Version 0.11.2 exports the classic chart types as native chart constructs and writes named and default table colours, text pairs and hyperlink colours as theme references (no API or dependency change; resolved colours are unchanged). Version 0.11.1 exports `design.watermark` as a native picture (it re-imports) and embeds byte-identical media parts once, with no API or dependency change. Version 0.11.0 requires core `^0.11.2` (cover slides are centered between header and footer furniture) and the optional `@openpresentation/opf-render` peer `^0.11.0`; install them together so export and preview resolve the same core. Version 0.10.0 required core `^0.11.1` and the peer `^0.10.0`. It adds native slide-number and date fields, socials, slide-image treatments, theme color schemes, script-slot fonts and RTL, re-import of design and metadata references, native underline and current-body formatting on import, and UTC-canonical `zipDate` (see the changelog for the intentional contract changes).
3
+ Version 0.11.6 exports the treemap, histogram, pareto, box-and-whisker, waterfall and funnel charts as native chartex parts by default (`toPptx({chartex: 'auto'})`; `world` stays a clustered column with `chart-data-adapted`; pass `chartex: 'fallback'` for the previous output) and gives chartex text the deck's label colour and font (no API removed). Version 0.11.5 writes the slide tag run as a theme color reference (`a:schemeClr accent1`) where the deck theme holds the primary there (no API change; the color is unchanged). Version 0.11.4 re-imports quote payloads (each native quote line carries an `OPF_QUOTE_V1` tag) and slide-image payloads from an unchanged export, writes the theme's `a:ea` and `a:cs` only for slots a script font is selected for, and requires core `^0.11.3` (the 70 legacy gallery layout ids and their geometry; install it with renderer 0.11.6 and editor 0.10.4 so export and preview resolve the same core; no API removed). Version 0.11.3 adds the opt-in `toPptx({ chartex: 'native' })` export of the seven chartex chart types and always imports chartex charts (no API removed; the default output is unchanged). Version 0.11.2 exports the classic chart types as native chart constructs and writes named and default table colours, text pairs and hyperlink colours as theme references (no API or dependency change; resolved colours are unchanged). Version 0.11.1 exports `design.watermark` as a native picture (it re-imports) and embeds byte-identical media parts once, with no API or dependency change. Version 0.11.0 requires core `^0.11.2` (cover slides are centered between header and footer furniture) and the optional `@openpresentation/opf-render` peer `^0.11.0`; install them together so export and preview resolve the same core. Version 0.10.0 required core `^0.11.1` and the peer `^0.10.0`. It adds native slide-number and date fields, socials, slide-image treatments, theme color schemes, script-slot fonts and RTL, re-import of design and metadata references, native underline and current-body formatting on import, and UTC-canonical `zipDate` (see the changelog for the intentional contract changes).
4
4
 
5
5
  Version 0.9.1 kept core `^0.11.0` and raised the optional `@openpresentation/opf-render` peer to `^0.9.0` so it coexists with editor 0.8.0. ColorRef / `variables` on content colors still hex-resolve through core `resolveColorRef()` before PptxGenJS `srgbClr` export. Unrecognized run colors such as `color:'invalid'` still validate and fall back to the theme text color. Native DrawingML `schemeClr` and theme `clrScheme` writes, and native `p:hf` headers/footers, are not in this release. Import still flattens theme colors to hex. Metric, quote and timeline layout placeholders and the corrected text-bullet contract from 0.8.1 are retained.
6
6
 
@@ -80,8 +80,8 @@ The first exporter keeps the public API stable while using `pptxgenjs` internall
80
80
 
81
81
  - `Slide.title`, `Slide.subtitle`, and `Slide.tag` become editable text boxes, not PowerPoint master placeholders.
82
82
  - Root payloads, `blocks[]`, and promoted region keys become editable slide objects in deterministic regions. Promoted keys use the OPF 3x3 region vocabulary (`top`, `middle`, `bottom`, `left`, `center`, `right`).
83
- - Text, lists, metrics, quotes, timelines, code, tables, and inline-data charts are emitted as editable PowerPoint text, table, and chart objects. Content ColorRef values (hex, scheme slots/roles, and `var:<id>`) resolve through core `resolveColorRef()`. Slot and role names become native `a:schemeClr` references when the exported theme holds that exact color; hex, `var:<id>` and slide-override colors stay `a:srgbClr` (see [Theme color scheme](#theme-color-scheme)). Each classic chart type the core catalog keeps (one per Aspose.Slides `ChartType`: clustered/stacked/100% stacked column and bar, line and stacked line with or without markers, area/stacked/100% area, pie, doughnut, scatter with markers, radar/radar with markers/filled radar) is written as its exact native construct; deprecated core ids export as their replacement; other ids keep the previous best-effort mapping. The chartex types (treemap, histogram, pareto, box & whisker, waterfall, funnel, map) export by default as clustered columns and report `chart-data-adapted` (`chartex-fallback`); with `chartex: "native"` they are written as native Office 2016 chartex parts (see [Chartex charts](#chartex-charts-ff-22b), opt-in pending native PowerPoint confirmation). A pie or doughnut chart (and, natively, a one-series chartex construct) plots only its first series and reports `chart-data-adapted` (`series-dropped`) when it has more.
84
- - A chart whose data is one column of values (a histogram or dot plot) has no category column. It exports as a native chart and reports `chart-data-adapted`: by default a histogram is binned into equal-width bins (Sturges' count, at most 50) and written as a column chart of the counts (`histogram-binned`; the binned counts do not round-trip), and any other chart type plots the values against their row numbers (`row-numbers`). With `chartex: "native"` a histogram or Pareto chart bins the values itself (PowerPoint's histogram with an explicit automatic bin count, no diagnostic) and re-imports as authored. Cells parse as in every chart ("12%", "$5" and "1,234" count) and cells that hold no number are skipped, not plotted as 0. Chart data that cannot be plotted (no numbers, no rows, or an external data source), an empty table and unsupported content keep a placeholder frame with a plain-language description (never a dump of the source data or URLs) and report `chart-data-unplottable` or `content-placeholder` with a `reason`; content is never dropped without a diagnostic.
83
+ - Text, lists, metrics, quotes, timelines, code, tables, and inline-data charts are emitted as editable PowerPoint text, table, and chart objects. Content ColorRef values (hex, scheme slots/roles, and `var:<id>`) resolve through core `resolveColorRef()`. Slot and role names become native `a:schemeClr` references when the exported theme holds that exact color; hex, `var:<id>` and slide-override colors stay `a:srgbClr` (see [Theme color scheme](#theme-color-scheme)). Each classic chart type the core catalog keeps (one per Aspose.Slides `ChartType`: clustered/stacked/100% stacked column and bar, line and stacked line with or without markers, area/stacked/100% area, pie, doughnut, scatter with markers, radar/radar with markers/filled radar) is written as its exact native construct; deprecated core ids export as their replacement; other ids keep the previous best-effort mapping. The chartex types treemap, histogram, pareto, box & whisker, waterfall and funnel are written as native Office 2016 chartex parts (confirmed in desktop PowerPoint on 2026-09-30; see [Chartex charts](#chartex-charts-ff-22b)); the map (`world`) stays a clustered column chart by default and reports `chart-data-adapted` (`chartex-fallback`) until PowerPoint accepts its regionMap part (`chartex: "native"` writes it anyway, `chartex: "fallback"` writes clustered columns for every chartex id). A pie or doughnut chart, or a one-series chartex construct, plots only its first series and reports `chart-data-adapted` (`series-dropped`) when it has more.
84
+ - A chart whose data is one column of values (a histogram or dot plot) has no category column. A histogram or Pareto chart bins the values itself (PowerPoint's histogram with an explicit automatic bin count, no diagnostic) and re-imports as authored; any other chart type plots the values against their row numbers and reports `chart-data-adapted` (`row-numbers`). With `chartex: "fallback"` a histogram is binned into equal-width bins (Sturges' count, at most 50) and written as a column chart of the counts (`histogram-binned`; the binned counts do not round-trip). Cells parse as in every chart ("12%", "$5" and "1,234" count) and cells that hold no number are skipped, not plotted as 0. Chart data that cannot be plotted (no numbers, no rows, or an external data source), an empty table and unsupported content keep a placeholder frame with a plain-language description (never a dump of the source data or URLs) and report `chart-data-unplottable` or `content-placeholder` with a `reason`; content is never dropped without a diagnostic.
85
85
  - Image assets are embedded only when supplied as data URIs, local paths, or host-resolved bytes/paths. Remote asset URLs are never fetched by the runtime path.
86
86
  - ZIP entries, generated chart/workbook part names, core-property timestamps, and nested chart workbook timestamps are normalized for reproducible bytes. [Export determinism](docs/export-determinism.md) lists the tested time zone, locale, clock and host-font controls and each known variance (WebP conversion, font registries, ICU segmentation, timestamps).
87
87
  - The package names only the document's chosen fonts. Chart text (data labels, axes, legend, titles) uses the chart slide's body font in `latin`/`ea`/`cs`; each embedded chart workbook uses the same fonts in its styles and theme; run `pitchFamily` follows the font scheme type (monospace is fixed pitch, serif is roman); and `docProps/app.xml` "Fonts Used" lists the fonts the package actually uses. The theme keeps PptxGenJS's per-script supplements (`THEME_SCRIPT_SUPPLEMENTS`) and empty `ea`/`cs` slots unless a script font was selected for that slot (see [Languages, right-to-left text and script fonts](#languages-right-to-left-text-and-script-fonts)).
@@ -133,9 +133,9 @@ There is no AI classification pass in the OSS runtime. Hosts can run optional cl
133
133
 
134
134
  ## Chartex charts (FF-22b)
135
135
 
136
- **Opt-in.** `toPptx(document, {chartex: "native"})` enables the native chartex export described here. The default, `chartex: "fallback"`, keeps the clustered column export of the seven chartex ids byte for byte (`test/fixtures/chartex-fallback-main.json` pins it) with the `chartex-fallback` and `histogram-binned` diagnostics, because a malformed chartex part would make PowerPoint offer to repair the whole deck, and the parts are not yet confirmed in native PowerPoint (no Open XML SDK validation either). The default flips once the program's native check of the FF-22b deck set passes. Import of chartex parts is always on.
136
+ **Modes.** `toPptx(document, {chartex})` takes `"auto"` (default), `"native"` or `"fallback"`. The native check of 2026-09-30 (desktop PowerPoint, read-only, the program's FF-22b deck set) opened every chartex deck without a repair prompt and reported the native `Chart.ChartType` for treemap (117), histogram (118, by value and by category), pareto (122), box-and-whisker (121), waterfall (119) and funnel (123); for `world` PowerPoint took the `mc:Fallback` (51, clustered column) when the choice required `cx5`. The recheck of the same day showed that PowerPoint accepts the regionMap part when the choice requires `cx4` (the 2016/5/10 region-map namespace; `cx3`, `cx4` and `cx6` reported ChartType 140, `cx5` and `cx8` fell back) but, with no `cx:geoCache`, shows "There was a problem getting the information for your map chart" and draws nothing until it can fetch map data online. Supervisor decision: `"auto"` writes the chartex parts for the six confirmed constructs and keeps `world` on the clustered column chart, which always renders, with `chart-data-adapted` (`chartex-fallback`); `"native"` writes every chartex part, the map included with `Requires="cx4"` and the `chart-map-geodata` diagnostic (provider data is never fabricated); `"fallback"` writes clustered columns only, byte for byte as before FF-22b (`test/fixtures/chartex-fallback-main.json` pins it, with `chartex-fallback` and `histogram-binned`). The same check showed chartex labels drawn in the chart style's theme grey and box lines in black on a dark theme, so every chartex text element and the style part now carry the deck's label colour and font explicitly, and box series carry a line in the label colour. Import of chartex parts is always on.
137
137
 
138
- With `chartex: "native"`, the seven kept chart types with no ECMA-376 construct are written as Office 2016 chartex parts (`ppt/charts/chartExN.xml`, `cx:chartSpace`, content type `application/vnd.ms-office.chartex+xml`), each with its chart style and colour style parts (`styleN.xml`, `colorsN.xml`) and the embedded workbook the data came from. PptxGenJS cannot write these parts, so the exporter first writes the classic clustered column chart of the same data and then post-processes the package (`src/chartex.js`): the slide's chart frame becomes an `mc:AlternateContent` whose `mc:Choice` (requiring the construct's chartex namespace) references the chartex part and whose `mc:Fallback` keeps the classic chart frame, so a reader without chartex support still shows the data as columns. Both parts share the workbook. `cx:series uniqueId` values are derived from the chart number, so the bytes stay deterministic.
138
+ The seven kept chart types with no ECMA-376 construct are written as Office 2016 chartex parts (`ppt/charts/chartExN.xml`, `cx:chartSpace`, content type `application/vnd.ms-office.chartex+xml`), each with its chart style and colour style parts (`styleN.xml`, `colorsN.xml`) and the embedded workbook the data came from. PptxGenJS cannot write these parts, so the exporter first writes the classic clustered column chart of the same data and then post-processes the package (`src/chartex.js`): the slide's chart frame becomes an `mc:AlternateContent` whose `mc:Choice` (requiring the construct's chartex namespace) references the chartex part and whose `mc:Fallback` keeps the classic chart frame, so a reader without chartex support still shows the data as columns. Both parts share the workbook. `cx:series uniqueId` values are derived from the chart number, so the bytes stay deterministic.
139
139
 
140
140
  | OPF id | `cx:series layoutId` | Data (category-major) | Notes |
141
141
  |---|---|---|---|
@@ -145,7 +145,7 @@ With `chartex: "native"`, the seven kept chart types with no ECMA-376 construct
145
145
  | `box-and-whisker` | `boxWhisker` | `[Category, S1, S2, ...]`, every series | rows with the same category form one box per series; exclusive quartiles, mean markers, outliers |
146
146
  | `waterfall` | `waterfall` | `[Category, Value]`, first series | connector lines; increases and decreases in the first two palette colours; no subtotals |
147
147
  | `funnel` | `funnel` | `[Category, Value]`, first series | value data labels |
148
- | `world` | `regionMap` | `[Region, Value]`, first series | no `cx:geoCache`: PowerPoint matches the region names and fetches the shapes from Bing Maps when the deck is opened online, and reports `chart-map-geodata`; offline, or where Office map data is disabled, the map shows no regions |
148
+ | `world` | `regionMap` (`chartex: "native"` only; `Requires="cx4"`) | `[Region, Value]`, first series | no `cx:geoCache`: PowerPoint accepts the part (ChartType 140) and must fetch the region shapes from its online map service; until it does it shows "There was a problem getting the information for your map chart" and draws nothing; reported as `chart-map-geodata`. By default (`"auto"`) the map stays a clustered column chart |
149
149
 
150
150
  The chart area, label colour, label font (`latin`/`ea`/`cs` in the chart's body font) and series colours follow the classic charts. Re-import (`fromPptx`) reads the chartex part first: the layoutIds name the OPF id (an owned `paretoLine` is `pareto`), `cx:strDim type="cat"` restores the categories and each `cx:numDim` a series, under the same 100,000-point and 1,000,000-cell bounds as classic chart caches; a lone value column comes back as authored. A chartex part with no OPF construct (sunburst) falls back to the `mc:Fallback` chart, or to a text placeholder when PowerPoint's own text fallback is all there is. Native PowerPoint rendering of each construct is confirmed through the program's bounded native sample, not by this package's tests.
151
151
 
@@ -39,7 +39,11 @@ export const CHART_TYPES = Object.freeze({
39
39
  'box-and-whisker': chartex('boxWhisker', 'BoxAndWhisker', { requires: 'cx1', series: Infinity, axes: true }),
40
40
  waterfall: chartex('waterfall', 'Waterfall', { requires: 'cx1', series: 1, axes: true }),
41
41
  funnel: chartex('funnel', 'Funnel', { requires: 'cx2', series: 1, axes: true }),
42
- world: chartex('regionMap', 'Map', { requires: 'cx5', dimension: 'colorVal', series: 1 }),
42
+ // Native check 2026-09-30: PowerPoint accepts the regionMap part only when the choice requires cx4 (2016/5/10, the
43
+ // region-map extension namespace; cx5 and cx8 fall back), reports ChartType 140, and then draws nothing without a
44
+ // populated cx:geoCache ("There was a problem getting the information for your map chart"), so the map stays on the
45
+ // clustered column fallback in 'auto' mode and is written only with `chartex: 'native'`.
46
+ world: chartex('regionMap', 'Map', { requires: 'cx4', dimension: 'colorVal', series: 1, unconfirmed: true }),
43
47
  });
44
48
 
45
49
  function chartex(layoutId, aspose, extra) {
package/dist/chartex.js CHANGED
@@ -115,9 +115,20 @@ function layoutProperties(spec, series, hasCategories) {
115
115
  }
116
116
  }
117
117
 
118
- function dataLabels(spec) {
119
- if (spec.layoutId === 'treemap') return '<cx:dataLabels pos="inEnd"><cx:visibility seriesName="0" categoryName="1" value="0"/></cx:dataLabels>';
120
- if (spec.layoutId === 'funnel') return '<cx:dataLabels pos="ctr"><cx:visibility seriesName="0" categoryName="0" value="1"/></cx:dataLabels>';
118
+ // Text properties for every chartex text element. PowerPoint resolves chartex
119
+ // label colours from the chart style part and each element's own cx:txPr, not
120
+ // from the chartSpace txPr alone (native check 2026-09-30: labels came out in
121
+ // the style's tx1 grey on a dark theme), so every axis, data label set and
122
+ // legend carries the deck's label colour and font explicitly, like the classic
123
+ // chart path writes into every c:txPr.
124
+ function textProperties(labelColor, font) {
125
+ const face = escapeXml(font);
126
+ return `<cx:txPr><a:bodyPr/><a:lstStyle/><a:p><a:pPr><a:defRPr sz="900">${solidFill(labelColor)}<a:latin typeface="${face}"/><a:ea typeface="${face}"/><a:cs typeface="${face}"/></a:defRPr></a:pPr><a:endParaRPr lang="en-US"/></a:p></cx:txPr>`;
127
+ }
128
+
129
+ function dataLabels(spec, text) {
130
+ if (spec.layoutId === 'treemap') return `<cx:dataLabels pos="ctr">${text}<cx:visibility seriesName="0" categoryName="1" value="0"/></cx:dataLabels>`;
131
+ if (spec.layoutId === 'funnel') return `<cx:dataLabels pos="ctr">${text}<cx:visibility seriesName="0" categoryName="0" value="1"/></cx:dataLabels>`;
121
132
  return '';
122
133
  }
123
134
 
@@ -129,14 +140,15 @@ export function chartexPointColors(layoutId, values, palette) {
129
140
  return values.map(() => null);
130
141
  }
131
142
 
132
- function axes(spec, gridColor) {
143
+ function axes(spec, gridColor, text) {
133
144
  if (!spec.axes) return '';
134
145
  const gridlines = `<cx:majorGridlines><cx:spPr><a:ln w="9525"><a:solidFill><a:srgbClr val="${gridColor}"><a:alpha val="70000"/></a:srgbClr></a:solidFill></a:ln></cx:spPr></cx:majorGridlines>`;
135
146
  const gapWidth = {clusteredColumn: '0.06', paretoLine: '0.06', boxWhisker: '1', waterfall: '0.5', funnel: '0.06'}[spec.layoutId] ?? '1';
136
- const category = `<cx:axis id="0"><cx:catScaling gapWidth="${gapWidth}"/><cx:tickLabels/></cx:axis>`;
137
- if (spec.layoutId === 'funnel') return `${category}<cx:axis id="1" hidden="1"><cx:valScaling/><cx:tickLabels/></cx:axis>`;
138
- const value = `<cx:axis id="1"><cx:valScaling/>${gridlines}<cx:tickLabels/></cx:axis>`;
139
- const percentage = spec.layoutId === 'paretoLine' ? '<cx:axis id="2"><cx:valScaling max="1" min="0"/><cx:units unit="percentage"/><cx:tickLabels/></cx:axis>' : '';
147
+ // CT_Axis order: scaling, units, majorGridlines, tickLabels, spPr, txPr.
148
+ const category = `<cx:axis id="0"><cx:catScaling gapWidth="${gapWidth}"/><cx:tickLabels/>${text}</cx:axis>`;
149
+ if (spec.layoutId === 'funnel') return `${category}<cx:axis id="1" hidden="1"><cx:valScaling/><cx:tickLabels/>${text}</cx:axis>`;
150
+ const value = `<cx:axis id="1"><cx:valScaling/>${gridlines}<cx:tickLabels/>${text}</cx:axis>`;
151
+ const percentage = spec.layoutId === 'paretoLine' ? `<cx:axis id="2"><cx:valScaling max="1" min="0"/><cx:units unit="percentage"/><cx:tickLabels/>${text}</cx:axis>` : '';
140
152
  return `${category}${value}${percentage}`;
141
153
  }
142
154
 
@@ -157,29 +169,30 @@ export function chartexPartXml({spec, series, hasCategories, number, workbookRel
157
169
  const points = entry.values.map((value, row) => value === null || !Number.isFinite(value) ? '' : `<cx:pt idx="${row}">${numberText(value)}</cx:pt>`).join('');
158
170
  return `<cx:data id="${index}">${categories}<cx:numDim type="${spec.dimension}"><cx:f>${range(columnLetters(index + 2))}</cx:f><cx:lvl ptCount="${rows}" formatCode="General">${points}</cx:lvl></cx:numDim></cx:data>`;
159
171
  }).join('');
172
+ const text = textProperties(labelColor, font);
160
173
  const plotted = series.map((entry, index) => {
161
174
  const color = palette[index % palette.length];
162
175
  const letter = columnLetters(index + 2);
163
176
  const pointFills = chartexPointColors(spec.layoutId, entry.values, palette)
164
177
  .map((pointColor, row) => pointColor ? `<cx:dataPt idx="${row}"><cx:spPr>${solidFill(pointColor)}</cx:spPr></cx:dataPt>` : '').join('');
178
+ // Box whiskers, median lines and mean markers are drawn with the series line; without one PowerPoint used black (native check 2026-09-30).
179
+ const line = spec.layoutId === 'boxWhisker' ? `<a:ln w="9525">${solidFill(labelColor)}</a:ln>` : '';
165
180
  return `<cx:series layoutId="${spec.owner ?? spec.layoutId}" uniqueId="${seriesUniqueId(number, index)}">` +
166
181
  `<cx:tx><cx:txData><cx:f>Sheet1!$${letter}$1</cx:f><cx:v>${escapeXml(entry.name)}</cx:v></cx:txData></cx:tx>` +
167
- `<cx:spPr>${solidFill(color)}</cx:spPr>${pointFills}${dataLabels(spec)}<cx:dataId val="${index}"/>${layoutProperties(spec, entry, hasCategories)}` +
182
+ `<cx:spPr>${solidFill(color)}${line}</cx:spPr>${pointFills}${dataLabels(spec, text)}<cx:dataId val="${index}"/>${layoutProperties(spec, entry, hasCategories)}` +
168
183
  (spec.axes ? '<cx:axisId val="0"/><cx:axisId val="1"/>' : '') + '</cx:series>';
169
184
  });
170
185
  if (spec.owner) {
171
186
  // The Office Pareto chart: the cumulative-percentage line owned by the binned columns, on the percentage axis.
172
187
  plotted.push(`<cx:series layoutId="${spec.layoutId}" ownerIdx="0" uniqueId="${seriesUniqueId(number, series.length)}"><cx:spPr><a:ln w="19050"><a:solidFill><a:srgbClr val="${palette[1 % palette.length]}"/></a:solidFill></a:ln></cx:spPr><cx:axisId val="0"/><cx:axisId val="2"/></cx:series>`);
173
188
  }
174
- const legend = spec.layoutId === 'boxWhisker' && series.length > 1 ? '<cx:legend pos="r" align="ctr" overlay="0"/>' : '';
189
+ const legend = spec.layoutId === 'boxWhisker' && series.length > 1 ? `<cx:legend pos="r" align="ctr" overlay="0">${text}</cx:legend>` : '';
175
190
  const alpha = fill.transparency > 0 ? Math.round((100 - fill.transparency) * 1000) : undefined;
176
- const face = escapeXml(font);
177
191
  return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
178
192
  `<cx:chartSpace xmlns:a="${NS.a}" xmlns:r="${NS.r}" xmlns:cx="${NS.cx}">` +
179
193
  `<cx:chartData><cx:externalData r:id="${workbookRelId}" cx:autoUpdate="0"/>${data}</cx:chartData>` +
180
- `<cx:chart><cx:plotArea><cx:plotAreaRegion>${plotted.join('')}</cx:plotAreaRegion>${axes(spec, gridColor)}</cx:plotArea>${legend}</cx:chart>` +
181
- `<cx:spPr>${solidFill(fill.color, alpha)}<a:ln><a:noFill/></a:ln></cx:spPr>` +
182
- `<cx:txPr><a:bodyPr/><a:lstStyle/><a:p><a:pPr><a:defRPr sz="900">${solidFill(labelColor)}<a:latin typeface="${face}"/><a:ea typeface="${face}"/><a:cs typeface="${face}"/></a:defRPr></a:pPr><a:endParaRPr lang="en-US"/></a:p></cx:txPr>` +
194
+ `<cx:chart><cx:plotArea><cx:plotAreaRegion>${plotted.join('')}</cx:plotAreaRegion>${axes(spec, gridColor, text)}</cx:plotArea>${legend}</cx:chart>` +
195
+ `<cx:spPr>${solidFill(fill.color, alpha)}<a:ln><a:noFill/></a:ln></cx:spPr>${text}` +
183
196
  '</cx:chartSpace>';
184
197
  }
185
198
 
@@ -201,11 +214,14 @@ const STYLE_ENTRIES = [
201
214
  'dataPointMarkerLayout', 'dataPointWireframe', 'dataTable', 'downBar', 'dropLine', 'errorBar', 'floor', 'gridlineMajor', 'gridlineMinor', 'hiLoLine',
202
215
  'leaderLine', 'legend', 'plotArea', 'plotArea3D', 'seriesAxis', 'seriesLine', 'title', 'trendline', 'trendlineLabel', 'upBar', 'valueAxis', 'wall',
203
216
  ];
204
- export function chartStyleXml() {
205
- const textColor = '<a:schemeClr val="tx1"><a:lumMod val="65000"/><a:lumOff val="35000"/></a:schemeClr>';
206
- const faintLine = '<a:ln w="9525" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="tx1"><a:lumMod val="15000"/><a:lumOff val="85000"/></a:schemeClr></a:solidFill><a:round/></a:ln>';
207
- const entry = (name, {fillIdx = '0', fillColor = '', fontColor = '<a:schemeClr val="tx1"/>', spPr = '', size = '', mods = ''} = {}) =>
208
- `<cs:${name}${mods ? ` mods="${mods}"` : ''}><cs:lnRef idx="0"/><cs:fillRef idx="${fillIdx}">${fillColor}</cs:fillRef><cs:effectRef idx="0"/><cs:fontRef idx="minor">${fontColor}</cs:fontRef>${spPr ? `<cs:spPr>${spPr}</cs:spPr>` : ''}${size ? `<cs:defRPr sz="${size}" kern="1200"/>` : ''}</cs:${name}>`;
217
+ export function chartStyleXml({labelColor = '000000', gridColor = '000000', font = 'Aptos'} = {}) {
218
+ // Text and chrome colours are the deck's label and border colours (what the classic chart path writes), not the
219
+ // theme's tx1: PowerPoint applies the style part to chartex labels that carry no cx:txPr of their own.
220
+ const textColor = `<a:srgbClr val="${labelColor}"/>`;
221
+ const face = escapeXml(font);
222
+ const faintLine = `<a:ln w="9525" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:srgbClr val="${gridColor}"><a:alpha val="70000"/></a:srgbClr></a:solidFill><a:round/></a:ln>`;
223
+ const entry = (name, {fillIdx = '0', fillColor = '', fontColor = textColor, spPr = '', size = '', mods = ''} = {}) =>
224
+ `<cs:${name}${mods ? ` mods="${mods}"` : ''}><cs:lnRef idx="0"/><cs:fillRef idx="${fillIdx}">${fillColor}</cs:fillRef><cs:effectRef idx="0"/><cs:fontRef idx="minor">${fontColor}</cs:fontRef>${spPr ? `<cs:spPr>${spPr}</cs:spPr>` : ''}${size ? `<cs:defRPr sz="${size}" kern="1200">${solidFill(labelColor)}<a:latin typeface="${face}"/><a:ea typeface="${face}"/><a:cs typeface="${face}"/></cs:defRPr>` : ''}</cs:${name}>`;
209
225
  const entries = STYLE_ENTRIES.map(name => {
210
226
  switch (name) {
211
227
  case 'dataPointMarkerLayout': return '<cs:dataPointMarkerLayout symbol="circle" size="5"/>';
@@ -278,7 +294,7 @@ export function attachChartexParts(entries, chartex, parseRelationships) {
278
294
  const base = `ppt/charts/chartEx${number}.xml`;
279
295
  entries[base] = encoder.encode(chartexPartXml({...chart, number, workbookRelId: 'rId1'}));
280
296
  entries[`ppt/charts/_rels/chartEx${number}.xml.rels`] = encoder.encode(chartexRelationshipsXml({workbookTarget: workbook.target, number}));
281
- entries[`ppt/charts/style${number}.xml`] = encoder.encode(chartStyleXml());
297
+ entries[`ppt/charts/style${number}.xml`] = encoder.encode(chartStyleXml({labelColor: chart.labelColor, gridColor: chart.gridColor, font: chart.font}));
282
298
  entries[`ppt/charts/colors${number}.xml`] = encoder.encode(chartColorStyleXml());
283
299
  overrides.push(`<Override PartName="/${base}" ContentType="${CHARTEX_CONTENT_TYPES.chart}"/>`,
284
300
  `<Override PartName="/ppt/charts/style${number}.xml" ContentType="${CHARTEX_CONTENT_TYPES.style}"/>`,
package/dist/index.d.ts CHANGED
@@ -47,7 +47,7 @@ export interface WatermarkNotExportedDiagnostic { code: "watermark-not-exported"
47
47
  * (`histogram-binned`).
48
48
  */
49
49
  export interface ChartDataAdaptedDiagnostic { code: "chart-data-adapted"; path: string; message: string; adaptation: "histogram-binned" | "row-numbers" | "series-dropped" | "chartex-fallback" }
50
- /** With `chartex: "native"`, a map chart (`world`) is exported as a chartex regionMap without cached geography: PowerPoint fetches the region shapes from Bing Maps online, so the map may show no regions offline. */
50
+ /** With `chartex: "native"`, a map chart (`world`) is exported as a chartex regionMap (`Requires="cx4"`) without cached geography: PowerPoint must fetch the region shapes from its online map service and shows "There was a problem getting the information for your map chart" with an empty chart until it does. */
51
51
  export interface ChartMapGeodataDiagnostic { code: "chart-map-geodata"; path: string; message: string }
52
52
 
53
53
  export interface ToPptxOptions {
@@ -55,12 +55,15 @@ export interface ToPptxOptions {
55
55
  imageFormat?: "compatible" | "preserve";
56
56
  /**
57
57
  * How the chartex chart types (treemap, histogram, pareto, box-and-whisker, waterfall, funnel, world) are exported.
58
- * "fallback" (default) writes the clustered column chart of the data and reports `chart-data-adapted` (`chartex-fallback`,
59
- * `histogram-binned`). "native" writes Office 2016 chartex parts (`cx:chartSpace`, style parts, an `mc:AlternateContent`
60
- * frame with the clustered column chart as fallback). Native chartex is opt-in pending native PowerPoint confirmation (FF-22b);
61
- * the default flips once that check passes. Import of chartex parts is always on.
58
+ * "auto" (default) writes Office 2016 chartex parts (`cx:chartSpace`, style parts, an `mc:AlternateContent` frame with the
59
+ * clustered column chart as fallback) for the constructs desktop PowerPoint confirmed natively on 2026-09-30 (treemap,
60
+ * histogram, pareto, box-and-whisker, waterfall, funnel) and keeps `world` on the clustered column chart with
61
+ * `chart-data-adapted` (`chartex-fallback`) because the regionMap part, though accepted with `Requires="cx4"`, draws
62
+ * nothing until PowerPoint fetches map data online. "native" writes every chartex part, the map included (reports
63
+ * `chart-map-geodata`). "fallback" writes clustered columns only
64
+ * (`chartex-fallback`, `histogram-binned`), as releases before FF-22b did. Import of chartex parts is always on.
62
65
  */
63
- chartex?: "native" | "fallback";
66
+ chartex?: "auto" | "native" | "fallback";
64
67
  /**
65
68
  * OPF_DOCUMENT_V1 / OPF_SLIDE_V1 customer-data tags that let fromPptx restore
66
69
  * catalog references, layout ids and authoring metadata (docs/document-roundtrip.md).
package/dist/index.js CHANGED
@@ -229,12 +229,13 @@ export async function toPptx(input, options = {}) {
229
229
  context.chartFonts = new Map();
230
230
  context.chartex = new Map();
231
231
  context.imageFormat = options.imageFormat ?? "compatible";
232
- // Native chartex parts are opt-in until the native PowerPoint check confirms them (FF-22b); the default keeps the
233
- // clustered column export of the chartex chart types.
234
- if (options.chartex !== undefined && !['native', 'fallback'].includes(options.chartex)) {
235
- throw new OPFPptxError('invalid-chartex-mode', 'chartex must be native or fallback.', {path: 'options.chartex'});
232
+ // Chartex export mode (FF-22b): 'auto' (default) writes native chartex parts for the constructs the native PowerPoint
233
+ // check confirmed (treemap, histogram, pareto, box-and-whisker, waterfall, funnel) and the clustered column fallback
234
+ // for the unconfirmed map; 'native' writes every chartex part; 'fallback' writes clustered columns only.
235
+ if (options.chartex !== undefined && !['auto', 'native', 'fallback'].includes(options.chartex)) {
236
+ throw new OPFPptxError('invalid-chartex-mode', 'chartex must be auto, native or fallback.', {path: 'options.chartex'});
236
237
  }
237
- context.chartexMode = options.chartex ?? 'fallback';
238
+ context.chartexMode = options.chartex ?? 'auto';
238
239
  Object.assign(context, exportTheme(presentation, context));
239
240
  context.reportedFontSchemes = new Set();
240
241
  // Document references and metadata tags (FF-32, docs/document-roundtrip.md).
@@ -1694,7 +1695,7 @@ function addChartPayload(slide, chart, region, context, options = {}, path = "ch
1694
1695
  const series = chartData.series.map((entry) => ({name: entry.name, labels: [...entry.labels], values: [...entry.values]}));
1695
1696
  context.chartex.set(objectName, {spec: chartData.chartex, series, hasCategories: chartData.hasCategories, fill, labelColor, gridColor: context.colors.border, font: context.fonts.body, palette});
1696
1697
  if (chartData.chartex.layoutId === 'regionMap') {
1697
- options.onDiagnostic?.({code: "chart-map-geodata", path, message: `The '${stringifyText(chart.type)}' chart is exported as a native PowerPoint map (chartex regionMap) without cached geography: PowerPoint matches the category names to regions and fetches their shapes from Bing Maps when the deck is opened online, so the map shows no regions offline or where Office map data is disabled. The clustered column fallback shows the same values in other readers.`});
1698
+ options.onDiagnostic?.({code: "chart-map-geodata", path, message: `The '${stringifyText(chart.type)}' chart is exported as a native PowerPoint map (chartex regionMap) without cached geography (no cx:geoCache; provider data is never fabricated): PowerPoint must fetch the region shapes from its online map service when the deck is opened, and until it does it shows "There was a problem getting the information for your map chart" and draws nothing. The clustered column fallback shows the same values in readers without chartex support.`});
1698
1699
  }
1699
1700
  }
1700
1701
  const percent = chartData.spec.grouping === 'percentStacked';
@@ -2232,7 +2233,7 @@ function textRuns(value, context, fallbackFontSize) {
2232
2233
  * into equal-width bins and exported as a column chart of the counts, every other type plots the values against
2233
2234
  * their row numbers. `adapted` names that transformation so it is reported, never silent.
2234
2235
  */
2235
- function toPptxChartData(chart, chartexMode = 'fallback') {
2236
+ function toPptxChartData(chart, chartexMode = 'auto') {
2236
2237
  const data = chart?.data;
2237
2238
  const unplottable = (reason, summary, message) => ({reason, summary, message: `${message} No native chart was exported; a placeholder frame stands in for it.`});
2238
2239
  if (!data || !Array.isArray(data.columns) || !Array.isArray(data.rows)) {
@@ -2241,17 +2242,21 @@ function toPptxChartData(chart, chartexMode = 'fallback') {
2241
2242
  if (data.rows.length === 0) return unplottable("no-rows", "The chart has no data rows.", "The chart data has no rows.");
2242
2243
  if (data.columns.length === 0) return unplottable("no-columns", "The chart has no data columns.", "The chart data has no columns.");
2243
2244
  const resolved = resolveChartType(chart.type).spec;
2244
- // A chartex type (treemap, histogram, pareto, box & whisker, waterfall, funnel, map) has no classic construct. With
2245
- // `chartex: 'native'` it is written as its cx:chartSpace part and the clustered column chart of the same data is its
2246
- // mc:Fallback (src/chartex.js); by default (pending the native PowerPoint check, FF-22b) it exports as the clustered
2247
- // column chart alone and reports chartex-fallback, never silently.
2248
- const native = chartexMode === 'native';
2245
+ // A chartex type (treemap, histogram, pareto, box & whisker, waterfall, funnel, map) has no classic construct. Natively
2246
+ // it is written as its cx:chartSpace part and the clustered column chart of the same data is its mc:Fallback
2247
+ // (src/chartex.js). 'auto' (the default) does that for the constructs the native PowerPoint check confirmed and keeps
2248
+ // the unconfirmed map on the clustered column chart alone; 'fallback' keeps every chartex id there. The fallback is
2249
+ // reported as chartex-fallback, never silent.
2250
+ const native = chartexMode === 'native' || (chartexMode === 'auto' && resolved.family === 'chartex' && !resolved.unconfirmed);
2249
2251
  const chartex = resolved.family === 'chartex' && native ? resolved : null;
2250
2252
  const spec = resolved.family === 'chartex' ? CHARTEX_FALLBACK : resolved;
2251
2253
  const typeName = stringifyText(chart.type);
2252
2254
  const adaptations = [];
2253
2255
  if (resolved.family === 'chartex' && !native) {
2254
- adaptations.push({adaptation: "chartex-fallback", message: `The '${typeName}' chart is a PowerPoint extension (chartex) chart that this exporter does not write; its data is exported as a native clustered column chart instead.`});
2256
+ const reason = chartexMode === 'fallback'
2257
+ ? 'that this exporter does not write'
2258
+ : 'that PowerPoint has not yet accepted natively from this exporter (pass chartex: \'native\' to write its chartex part)';
2259
+ adaptations.push({adaptation: "chartex-fallback", message: `The '${typeName}' chart is a PowerPoint extension (chartex) chart ${reason}; its data is exported as a native clustered column chart instead.`});
2255
2260
  }
2256
2261
  const mapped = {type: spec.pptx, spec, chartex, barDir: spec.barDir, barGrouping: spec.pptx === 'bar' || spec.pptx === 'area' ? spec.grouping : undefined};
2257
2262
  if (data.columns.length === 1) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpresentation/opf-pptx",
3
- "version": "0.11.5",
3
+ "version": "0.11.6",
4
4
  "description": "Pure local OPF to PPTX export and PPTX to OPF import tooling.",
5
5
  "license": "MIT",
6
6
  "type": "module",