node-pptx-templater 1.0.6 → 1.0.7

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
@@ -35,6 +35,7 @@ You design your layouts, tables, fonts, brand styles, charts, and animations ins
35
35
  * 🛡️ **Hardened XML Security**: Fully immune to Billion Laughs (XML bombs), XXE (XML External Entity Injection), and parser crashes due to oversized expansions.
36
36
  * 🧩 **Text Run Fragmentation Healing**: Solves the split-tag issue. PowerPoint splits `{{placeholders}}` into fragmented `<a:r>` nodes; our parser unifies and replaces them while keeping original formatting intact.
37
37
  * 📊 **Excel Cache Synchronized Charting**: Supports Bar, Line, Pie, Doughnut, Area, and Scatter charts. Not only updates the visual XML coordinates but also synchronizes data points inside the underlying embedded Excel spreadsheets (`ppt/embeddings/`) to bypass PowerPoint's "Update Data" warnings.
38
+ * 🏷️ **Chart Data Labels & Value From Cells**: Configure custom arrays, dynamic category maps, rich templates, positions (insideEnd, bestFit, center), and custom styles (fonts, colors, weight) and serialize them directly to the underlying Excel backing sheet.
38
39
  * 📋 **DrawingML Table Cell Merging**: Easily configure horizontal spans (`gridSpan`/`hMerge`), vertical spans (`rowSpan`/`vMerge`), and rectangular blocks. Injects unique `rowId` hashes to maintain relationship integrity.
39
40
  * 🥞 **Z-Order Layer Stacking**: Reorder shapes, images, charts, and tables programmatically. Simulates PowerPoint's "Bring to Front" and "Send to Back" commands directly in the slide's `<p:spTree>`.
40
41
  * 🎛️ **Slide Management**: Duplicate, delete, reorder slides, or import slides from external decks with automatic media and theme deduplication.
@@ -80,6 +81,15 @@ async function generateReport() {
80
81
  ]
81
82
  });
82
83
 
84
+ // 3b. Configure custom chart data labels with templates and styling
85
+ ppt.useSlide(2)
86
+ .updateDataLabels('revenue-chart', {
87
+ series: 0,
88
+ template: '{category}: {value}',
89
+ position: 'insideEnd',
90
+ labelStyle: { fontFamily: 'Arial', fontSize: 10, bold: true }
91
+ });
92
+
83
93
  // 4. Update table structure with cell merges and colors on Slide 3
84
94
  ppt.useSlide(3)
85
95
  .updateTable('performance-table', [
@@ -330,7 +340,7 @@ ppt.useSlide(1).getTables(());
330
340
  ### Charts API
331
341
 
332
342
  #### `updateChart(chartId, data)`
333
- Updates chart data in the selected slide(s). Finds charts by their name/ID and updates categories, series, and values. Preserves original chart styles, themes, and formatting.
343
+ Updates chart data in the selected slide(s). Finds charts by their name/ID and updates categories, series, and values. Preserves original chart styles, themes, and formatting. Supports inline custom data labels by passing objects in the format `{ data: number, label: string }` instead of numbers.
334
344
 
335
345
  * **Arguments**:
336
346
  * `chartId` (`string`): Chart name or relationship ID.
@@ -338,7 +348,7 @@ Updates chart data in the selected slide(s). Finds charts by their name/ID and u
338
348
  * `data.categories` (`string[]`): Category labels (X-axis).
339
349
  * `data.series` (`SeriesData[]`): Data series array.
340
350
  * `data.series[].name` (`string`): Series name.
341
- * `data.series[].values` (`number[]`): Data values.
351
+ * `data.series[].values` (`number[]|object[]`): Data values (numbers or label objects).
342
352
  * **Returns**: `PPTXTemplater` - this (chainable)
343
353
 
344
354
  ```javascript
@@ -402,6 +412,40 @@ Delegates core actions to slide element sub-managers.
402
412
  ppt.useSlide(1).updateChartCategories(());
403
413
  ```
404
414
 
415
+ #### `updateDataLabels(())`
416
+ Delegates core actions to slide element sub-managers.
417
+
418
+ * **Returns**: `PPTXTemplater` - The fluent engine instance.
419
+
420
+ ```javascript
421
+ ppt.useSlide(1).updateDataLabels('SalesChart', {
422
+ series: 0,
423
+ labels: ['Excellent', 'Good', 'Poor']
424
+ });
425
+ ```
426
+
427
+ #### `getDataLabels(())`
428
+ Delegates core actions to slide element sub-managers.
429
+
430
+ * **Returns**: `PPTXTemplater` - The fluent engine instance.
431
+
432
+ ```javascript
433
+ const labels = await ppt.useSlide(1).getDataLabels('SalesChart', { series: 0 });
434
+ console.log(labels); // [{ point: 0, value: 'Excellent' }, ...]
435
+ ```
436
+
437
+ #### `validateDataLabels(())`
438
+ Delegates core actions to slide element sub-managers.
439
+
440
+ * **Returns**: `PPTXTemplater` - The fluent engine instance.
441
+
442
+ ```javascript
443
+ const result = await ppt.useSlide(1).validateDataLabels('SalesChart', {
444
+ labels: ['High', 'Low']
445
+ });
446
+ console.log(result.valid);
447
+ ```
448
+
405
449
  #### `getCharts(())`
406
450
  Delegates core actions to slide element sub-managers.
407
451
 
@@ -1249,6 +1293,93 @@ ppt.useSlide(1).create(());
1249
1293
 
1250
1294
  ---
1251
1295
 
1296
+ ## 📊 Chart Data Labels & Value From Cells
1297
+
1298
+ PPTXForge supports advanced, PowerPoint-compatible chart data labels. You can customize label data sources, templates, positioning, and visual styles.
1299
+
1300
+ ### 1. Value From Cells (Excel Synchronization)
1301
+ Pull dynamic data labels directly from worksheet range cells inside the backing Excel spreadsheet. This is Excel's native "Value From Cells" feature, reconstructed programmatically inside the `.pptx` XML and `.xlsx` worksheets.
1302
+
1303
+ ```javascript
1304
+ ppt.useSlide(1).updateDataLabels('SalesChart', {
1305
+ series: 0,
1306
+ labelsFromCells: 'Sheet1!D2:D5'
1307
+ });
1308
+ ```
1309
+
1310
+ ### 2. Literal Arrays
1311
+ Override data labels for specific points in a series with a static list of strings:
1312
+
1313
+ ```javascript
1314
+ ppt.useSlide(1).updateDataLabels('SalesChart', {
1315
+ series: 0,
1316
+ labels: ['Top Performance', 'Met Target', 'Action Required', 'At Risk']
1317
+ });
1318
+ ```
1319
+
1320
+ ### 3. Dynamic Label Templates
1321
+ Combine values, category names, percentages, and custom label strings to format annotations:
1322
+
1323
+ ```javascript
1324
+ ppt.useSlide(1).updateDataLabels('SalesChart', {
1325
+ series: 0,
1326
+ template: '{category}: {value} ({percentage}%)'
1327
+ });
1328
+ ```
1329
+ * **Variables**: `{category}`, `{value}`, `{percentage}`, `{series}`, `{customLabel}`
1330
+
1331
+ ### 4. Label Positions
1332
+ Set label alignment to any of PowerPoint's standard values:
1333
+
1334
+ ```javascript
1335
+ ppt.useSlide(1).updateDataLabels('SalesChart', {
1336
+ series: 0,
1337
+ position: 'insideEnd' // 'center', 'insideEnd', 'insideBase', 'outsideEnd', 'bestFit', 'left', 'right', 'top', 'bottom'
1338
+ });
1339
+ ```
1340
+
1341
+ ### 5. Custom Label Styling
1342
+ Format your data labels using custom fonts, sizes, colors, and weight properties:
1343
+
1344
+ ```javascript
1345
+ ppt.useSlide(1).updateDataLabels('SalesChart', {
1346
+ series: 0,
1347
+ labels: ['High', 'Medium', 'Low'],
1348
+ labelStyle: {
1349
+ fontFamily: 'Century Gothic',
1350
+ fontSize: 12,
1351
+ color: '#0055A5',
1352
+ bold: true,
1353
+ italic: true,
1354
+ underline: true
1355
+ }
1356
+ });
1357
+ ```
1358
+
1359
+ ### 6. Inline Custom Data Labels (`updateChart`)
1360
+ You can define custom data labels inline within the standard `updateChart()` series `values` array using objects in the format `{ data: number, label: string }`. This avoids calling separate styling or update methods:
1361
+
1362
+ ```javascript
1363
+ ppt.useSlide(1).updateChart('RevenueChart', {
1364
+ categories: ['Q1', 'Q2', 'Q3', 'Q4'],
1365
+ series: [
1366
+ {
1367
+ name: 'Product A',
1368
+ values: [
1369
+ { data: 145, label: 'Q1: 145 (Low)' },
1370
+ { data: 210, label: 'Q2: 210 (Med)' },
1371
+ { data: 190, label: 'Q3: 190 (Med)' },
1372
+ { data: 250, label: 'Q4: 250 (High)' }
1373
+ ]
1374
+ }
1375
+ ]
1376
+ });
1377
+ ```
1378
+
1379
+ To preserve PowerPoint integrity, the engine ensures that if one value contains a label, all values in that series must have labels, and the label properties must be string values.
1380
+
1381
+ ---
1382
+
1252
1383
  ## 🔒 Advanced XML Security
1253
1384
 
1254
1385
  PPTXForge uses a secure XML parser that defends your application servers against common XML vulnerabilities.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-pptx-templater",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "High-performance, low-level PowerPoint (PPTX) OpenXML template engine for Node.js. Dynamically replace text, insert images, update charts (with Excel workbook data caching), and merge table cells without PowerPoint corruption or Repair Mode prompts.",
5
5
  "main": "./src/index.js",
6
6
  "type": "commonjs",
@@ -359,20 +359,34 @@ class PPTXTemplater {
359
359
  * Updates chart data in the selected slide(s).
360
360
  * Finds charts by their name/ID and updates categories, series, and values.
361
361
  * Preserves original chart styles, themes, and formatting.
362
+ * Supports inline custom data labels by passing objects in the format `{ data: number, label: string }` instead of numbers.
362
363
  *
363
364
  * @param {string} chartId - Chart name or relationship ID.
364
365
  * @param {ChartData} data - New chart data.
365
366
  * @param {string[]} data.categories - Category labels (X-axis).
366
367
  * @param {SeriesData[]} data.series - Data series array.
367
368
  * @param {string} data.series[].name - Series name.
368
- * @param {number[]} data.series[].values - Data values.
369
+ * @param {number[]|object[]} data.series[].values - Data values (numbers or label objects).
369
370
  * @returns {PPTXTemplater} this (chainable)
370
371
  *
371
372
  * @example
373
+ * // Simple numeric values:
372
374
  * ppt.updateChart('sales-chart', {
373
375
  * categories: ['Jan', 'Feb', 'Mar'],
374
376
  * series: [{ name: 'Revenue', values: [120, 150, 180] }]
375
377
  * });
378
+ *
379
+ * // Custom inline data labels:
380
+ * ppt.updateChart('sales-chart', {
381
+ * categories: ['Q1', 'Q2'],
382
+ * series: [{
383
+ * name: 'Revenue',
384
+ * values: [
385
+ * { data: 120, label: '120 (40%)' },
386
+ * { data: 180, label: '180 (60%)' }
387
+ * ]
388
+ * }]
389
+ * });
376
390
  */
377
391
  updateChart(chartId, data) {
378
392
  this.#assertLoaded()
@@ -1320,6 +1334,43 @@ class PPTXTemplater {
1320
1334
  return this
1321
1335
  }
1322
1336
 
1337
+ updateDataLabels(chartId, options) {
1338
+ this.#assertLoaded()
1339
+ const targetIndices = this.#getTargetSlideIndices()
1340
+ for (const idx of targetIndices) {
1341
+ this.#chartManager.updateDataLabels(
1342
+ idx,
1343
+ chartId,
1344
+ options,
1345
+ this.#slideManager,
1346
+ this.#relationshipManager
1347
+ )
1348
+ }
1349
+ return this
1350
+ }
1351
+
1352
+ async getDataLabels(chartId, options = {}) {
1353
+ this.#assertLoaded()
1354
+ const targetIndices = this.#getTargetSlideIndices()
1355
+ if (targetIndices.length === 0) return []
1356
+ const idx = targetIndices[0]
1357
+ return this.#chartManager.getDataLabels(
1358
+ idx,
1359
+ chartId,
1360
+ options,
1361
+ this.#slideManager,
1362
+ this.#relationshipManager
1363
+ )
1364
+ }
1365
+
1366
+ async validateDataLabels(chartId, options = {}) {
1367
+ this.#assertLoaded()
1368
+ const targetIndices = this.#getTargetSlideIndices()
1369
+ if (targetIndices.length === 0) return { valid: true, errors: [] }
1370
+ const idx = targetIndices[0]
1371
+ return ValidationEngine.validateDataLabels(this, idx, chartId, options)
1372
+ }
1373
+
1323
1374
  getCharts() {
1324
1375
  this.#assertLoaded()
1325
1376
  const targetIndices = this.#getTargetSlideIndices()
@@ -318,6 +318,99 @@ class ValidationEngine {
318
318
  warnings,
319
319
  }
320
320
  }
321
+
322
+ /**
323
+ * Validates data label configurations for a chart.
324
+ *
325
+ * @param {PPTXTemplater} ppt
326
+ * @param {number} slideIndex
327
+ * @param {string} chartId
328
+ * @param {Object} options
329
+ * @returns {Promise<Object>} report
330
+ */
331
+ static async validateDataLabels(ppt, slideIndex, chartId, options = {}) {
332
+ const errors = []
333
+ const warnings = []
334
+
335
+ try {
336
+ const chartInfo = ppt.chartManager.findChartInSlide(
337
+ slideIndex,
338
+ chartId,
339
+ ppt.slideManager,
340
+ ppt.relationshipManager
341
+ )
342
+
343
+ if (!chartInfo) {
344
+ errors.push(`Chart "${chartId}" not found in slide ${slideIndex}`)
345
+ return { valid: false, errors, warnings }
346
+ }
347
+
348
+ const chartType = await ppt.chartManager.getChartTypeAsync(
349
+ slideIndex,
350
+ chartId,
351
+ ppt.slideManager,
352
+ ppt.relationshipManager
353
+ )
354
+
355
+ const supportedTypes = [
356
+ 'bar',
357
+ 'column',
358
+ 'line',
359
+ 'pie',
360
+ 'doughnut',
361
+ 'area',
362
+ 'scatter',
363
+ 'combo',
364
+ 'unknown',
365
+ ]
366
+ if (!supportedTypes.includes(chartType)) {
367
+ errors.push(`Unsupported chart type "${chartType}" for data labels`)
368
+ }
369
+
370
+ const xml = await ppt.zipManager.readFile(chartInfo.zipPath)
371
+ let ptsCount = 0
372
+ const catMatch = /<c:cat>([\s\S]*?)<\/c:cat>/.exec(xml)
373
+ const valMatch = /<c:val>([\s\S]*?)<\/c:val>/.exec(xml)
374
+ const targetBlock = catMatch ? catMatch[1] : valMatch ? valMatch[1] : ''
375
+ const ptCountMatch = /<c:ptCount val="(\d+)"\/>/.exec(targetBlock)
376
+ if (ptCountMatch) {
377
+ ptsCount = parseInt(ptCountMatch[1], 10)
378
+ }
379
+
380
+ if (options.labels) {
381
+ if (ptsCount > 0 && options.labels.length !== ptsCount) {
382
+ errors.push(
383
+ `Label count (${options.labels.length}) does not match chart data points count (${ptsCount})`
384
+ )
385
+ }
386
+
387
+ options.labels.forEach((lbl, i) => {
388
+ if (lbl === null || lbl === undefined || String(lbl).trim() === '') {
389
+ warnings.push(`Label at index ${i} is empty`)
390
+ }
391
+ })
392
+ }
393
+
394
+ if (options.labelsFromCells) {
395
+ const range = options.labelsFromCells
396
+ const parts = range.split('!')
397
+ const rangePart = parts.length > 1 ? parts[1] : parts[0]
398
+
399
+ const rangeRegex = /^\$?[A-Z]+\$?\d+(?::\$?[A-Z]+\$?\d+)?$/i
400
+ if (!rangeRegex.test(rangePart)) {
401
+ errors.push(`Invalid range format: "${options.labelsFromCells}"`)
402
+ }
403
+ }
404
+ } catch (err) {
405
+ errors.push(`Data labels validation error: ${err.message}`)
406
+ }
407
+
408
+ return {
409
+ valid: errors.length === 0,
410
+ errors,
411
+ warnings,
412
+ }
413
+ }
321
414
  }
322
415
 
323
416
  module.exports = { ValidationEngine }