node-pptx-templater 1.0.10 → 1.0.11

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
@@ -373,6 +373,68 @@ Repairs common chart corruption issues such as broken caches, missing embedded w
373
373
  ppt.useSlide(1).repairCharts();
374
374
  ```
375
375
 
376
+ #### `getChartLabelPositions(chartId)`
377
+ Retrieves the exact coordinate positions of all data labels for a chart on the active slide. Calculates absolute layout limits in EMUs (English Metric Units).
378
+
379
+ * **Arguments**:
380
+ * `chartId` (`string`):
381
+ * **Returns**: `Promise<Array<{series: string, category: string, seriesIndex: number, categoryIndex: number, value: number, x: number, y: number, width: number, height: number` -
382
+
383
+ ```javascript
384
+ const positions = await ppt.useSlide(1).getChartLabelPositions('SalesChart');
385
+ ```
386
+
387
+ #### `getChartBarPositions(chartId)`
388
+ Retrieves the exact coordinate positions of all bars/columns for a chart on the active slide. Calculates absolute layout limits in EMUs (English Metric Units).
389
+
390
+ * **Arguments**:
391
+ * `chartId` (`string`):
392
+ * **Returns**: `Promise<Array<{series: string, category: string, seriesIndex: number, categoryIndex: number, value: number, x: number, y: number, width: number, height: number` -
393
+
394
+ ```javascript
395
+ const bars = await ppt.useSlide(1).getChartBarPositions('SalesChart');
396
+ ```
397
+
398
+ #### `addTextAtPosition(options)`
399
+ Adds a textbox shape at a specific EMU coordinate position on targeted slides. Supports custom font styling and alignment configuration.
400
+
401
+ * **Arguments**:
402
+ * `options` (`Object`):
403
+ * `options.text` (`string`):
404
+ * `options.x` (`number`):
405
+ * `options.y` (`number`):
406
+ * `[options.width=1200000]` (`number`):
407
+ * `[options.height=300000]` (`number`):
408
+ * `[options.style]` (`Object`):
409
+ * **Returns**: `this` - The chainable presentation engine instance.
410
+
411
+ ```javascript
412
+ ppt.useSlide(1).addTextAtPosition({
413
+ text: 'Label',
414
+ x: 1000000,
415
+ y: 1000000
416
+ });
417
+ ```
418
+
419
+ #### `addTextNearChartLabel(options)`
420
+ Dynamically places textboxes next to a chart's data labels with vertical collision avoidance. Textboxes are positioned either on the left or right of the chart area, vertically aligned with their corresponding label.
421
+
422
+ * **Arguments**:
423
+ * `options` (`Object`):
424
+ * `options.chart` (`string`):
425
+ * `options.text` (`string|Function`):
426
+ * `[options.position='left']` (`'left'|'right'`):
427
+ * `[options.style]` (`Object`):
428
+ * **Returns**: `this` - The chainable presentation engine instance.
429
+
430
+ ```javascript
431
+ ppt.addTextNearChartLabel({
432
+ chart: 'SalesChart',
433
+ text: 'Series',
434
+ position: 'left'
435
+ });
436
+ ```
437
+
376
438
  #### `updateChartData(())`
377
439
  Delegates core actions to slide element sub-managers.
378
440
 
@@ -446,6 +508,31 @@ const result = await ppt.useSlide(1).validateDataLabels('SalesChart', {
446
508
  console.log(result.valid);
447
509
  ```
448
510
 
511
+ #### `validateChartLabels(())`
512
+ Delegates core actions to slide element sub-managers.
513
+
514
+ * **Returns**: `PPTXTemplater` - The fluent engine instance.
515
+
516
+ ```javascript
517
+ const result = await ppt.useSlide(1).validateChartLabels('SalesChart', {
518
+ labels: ['High', 'Low']
519
+ });
520
+ console.log(result.valid);
521
+ ```
522
+
523
+ #### `validateSeriesNameLabels(())`
524
+ Delegates core actions to slide element sub-managers.
525
+
526
+ * **Returns**: `PPTXTemplater` - The fluent engine instance.
527
+
528
+ ```javascript
529
+ const result = await ppt.useSlide(1).validateSeriesNameLabels('SalesChart', {
530
+ enabled: true,
531
+ position: 'left'
532
+ });
533
+ console.log(result.valid);
534
+ ```
535
+
449
536
  #### `getCharts(())`
450
537
  Delegates core actions to slide element sub-managers.
451
538
 
@@ -836,6 +923,38 @@ ppt.useSlide(1).getImages(());
836
923
 
837
924
  ### Shapes API
838
925
 
926
+ #### `updateShapePosition(shapeId, options = {})`
927
+ Updates the position and/or dimensions of an existing shape on targeted slides.
928
+
929
+ * **Arguments**:
930
+ * `shapeId` (`string`):
931
+ * `options` (`Object`):
932
+ * `[options.x]` (`number`):
933
+ * `[options.y]` (`number`):
934
+ * `[options.width]` (`number`):
935
+ * `[options.height]` (`number`):
936
+ * **Returns**: `this` - The chainable presentation engine instance.
937
+
938
+ ```javascript
939
+ ppt.useSlide(1).updateShapePosition('TitleShape', { x: 1000000, y: 1500000 });
940
+ ```
941
+
942
+ #### `updateTextBoxPosition(textBoxId, options = {})`
943
+ Updates the position and/or dimensions of an existing textbox on targeted slides.
944
+
945
+ * **Arguments**:
946
+ * `textBoxId` (`string`):
947
+ * `options` (`Object`):
948
+ * `[options.x]` (`number`):
949
+ * `[options.y]` (`number`):
950
+ * `[options.width]` (`number`):
951
+ * `[options.height]` (`number`):
952
+ * **Returns**: `this` - The chainable presentation engine instance.
953
+
954
+ ```javascript
955
+ ppt.useSlide(1).updateTextBoxPosition('TextBox 2', { x: 1000000, y: 1500000 });
956
+ ```
957
+
839
958
  #### `updateShapeText(())`
840
959
  Delegates core actions to slide element sub-managers.
841
960
 
@@ -1416,6 +1535,79 @@ ppt.useSlide(1).updateChart('RevenueChart', {
1416
1535
 
1417
1536
  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.
1418
1537
 
1538
+ ### 7. Label Style Inheritance & Series Names Inside Bars
1539
+
1540
+ #### Style Inheritance
1541
+ When custom labels (inline or via `updateDataLabels`) are generated, they inherit the styling properties (font family, font size, bold, italic, color, and alignment) defined in the template's `<c:txPr>` tag for the series. This ensures your custom labels match the branding and layout design from your PowerPoint template file.
1542
+
1543
+ #### Series Names Inside Bars (`showSeriesNameInBar`)
1544
+ For stacked bar charts, you can show the series name inside each segment (typically centered) to make them easily readable. To enable this, set `showSeriesNameInBar: true` in the chart options (globally) or on individual series:
1545
+
1546
+ ```javascript
1547
+ ppt.useSlide(1).updateChart('RevenueChart', {
1548
+ showSeriesNameInBar: true, // Show series name labels globally
1549
+ categories: ['Q1', 'Q2', 'Q3', 'Q4'],
1550
+ series: [
1551
+ { name: 'Product A', values: [100, 200, 300, 400] },
1552
+ { name: 'Product B', values: [150, 250, 350, 450], showSeriesNameInBar: true } // Or per-series
1553
+ ]
1554
+ });
1555
+ ```
1556
+
1557
+ #### Chart Labels Validation
1558
+ You can programmatically validate that the chart labels configured in a chart conform to your template structure:
1559
+
1560
+ ```javascript
1561
+ const report = await ppt.useSlide(1).validateChartLabels('RevenueChart', {
1562
+ labels: ['Custom label 1', 'Custom label 2'],
1563
+ showSeriesNameInBar: true
1564
+ });
1565
+
1566
+ if (!report.valid) {
1567
+ console.log('Errors:', report.errors);
1568
+ console.log('Warnings:', report.warnings);
1569
+ }
1570
+ ```
1571
+
1572
+ #### External Series Name Labels (`seriesNameLabels`)
1573
+ You can position the series names outside the chart area as separate text boxes (`<p:sp>`) aligned with each corresponding bar or stack. These external labels automatically inherit the styling (font family, font size, bold, italic, color) defined in the template's series properties.
1574
+
1575
+ Supported features:
1576
+ * **Position**: Can be set to `'left'` or `'right'` to align text boxes to the left or right of the chart.
1577
+ * **Auto-fit & Height Wrapping**: Automatically wraps text and calculates height if labels are longer than the available margin space.
1578
+ * **Collision Detection**: Prevent overlapping with slide bounds by shrinking the chart, and resolve vertical overlaps of labels by shifting them apart.
1579
+ * **Idempotency**: Safely clean up previous labels on multiple chart updates.
1580
+
1581
+ Example usage:
1582
+ ```javascript
1583
+ ppt.useSlide(1).updateChart('RevenueChart', {
1584
+ categories: ['Q1', 'Q2', 'Q3', 'Q4'],
1585
+ series: [
1586
+ { name: 'Product A', values: [100, 200, 300, 400] },
1587
+ { name: 'Product B', values: [150, 250, 350, 450] }
1588
+ ],
1589
+ seriesNameLabels: {
1590
+ enabled: true,
1591
+ position: 'left', // 'left' or 'right'
1592
+ autoFit: true // Automatically wrap and shrink layout if needed (default: true)
1593
+ }
1594
+ });
1595
+ ```
1596
+
1597
+ To validate your configuration and check for boundary or collision warning/errors:
1598
+ ```javascript
1599
+ const report = await ppt.useSlide(1).validateSeriesNameLabels('RevenueChart', {
1600
+ enabled: true,
1601
+ position: 'left',
1602
+ autoFit: true
1603
+ });
1604
+
1605
+ if (!report.valid) {
1606
+ console.error('Validation errors:', report.errors);
1607
+ console.warn('Validation warnings:', report.warnings);
1608
+ }
1609
+ ```
1610
+
1419
1611
  ---
1420
1612
 
1421
1613
  ## 📋 Native Lists (Bullet & Numbered Lists)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-pptx-templater",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
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",
@@ -181,4 +181,4 @@
181
181
  "LICENSE",
182
182
  "CHANGELOG.md"
183
183
  ]
184
- }
184
+ }
@@ -1371,6 +1371,34 @@ class PPTXTemplater {
1371
1371
  return ValidationEngine.validateDataLabels(this, idx, chartId, options)
1372
1372
  }
1373
1373
 
1374
+ async validateChartLabels(chartId, options = {}) {
1375
+ this.#assertLoaded()
1376
+ const targetIndices = this.#getTargetSlideIndices()
1377
+ if (targetIndices.length === 0) return { valid: true, errors: [], warnings: [] }
1378
+ const idx = targetIndices[0]
1379
+ return this.#chartManager.validateChartLabels(
1380
+ idx,
1381
+ chartId,
1382
+ options,
1383
+ this.#slideManager,
1384
+ this.#relationshipManager
1385
+ )
1386
+ }
1387
+
1388
+ async validateSeriesNameLabels(chartId, options = {}) {
1389
+ this.#assertLoaded()
1390
+ const targetIndices = this.#getTargetSlideIndices()
1391
+ if (targetIndices.length === 0) return { valid: true, errors: [], warnings: [] }
1392
+ const idx = targetIndices[0]
1393
+ return this.#chartManager.validateSeriesNameLabels(
1394
+ idx,
1395
+ chartId,
1396
+ options,
1397
+ this.#slideManager,
1398
+ this.#relationshipManager
1399
+ )
1400
+ }
1401
+
1374
1402
  getCharts() {
1375
1403
  this.#assertLoaded()
1376
1404
  const targetIndices = this.#getTargetSlideIndices()
@@ -1383,6 +1411,95 @@ class PPTXTemplater {
1383
1411
  return charts
1384
1412
  }
1385
1413
 
1414
+ /**
1415
+ * Retrieves the exact coordinate positions of all data labels for a chart on the active slide.
1416
+ * Calculates absolute layout limits in EMUs (English Metric Units).
1417
+ *
1418
+ * @param {string} chartId The unique chart name/id in the template slide.
1419
+ * @returns {Promise<Array<{series: string, category: string, seriesIndex: number, categoryIndex: number, value: number, x: number, y: number, width: number, height: number}>>} An array of data label geometry objects.
1420
+ */
1421
+ async getChartLabelPositions(chartId) {
1422
+ this.#assertLoaded()
1423
+ const targetIndices = this.#getTargetSlideIndices()
1424
+ if (targetIndices.length === 0) return []
1425
+ const idx = targetIndices[0]
1426
+ return this.#chartManager.getChartLabelPositions(
1427
+ idx,
1428
+ chartId,
1429
+ this.#slideManager,
1430
+ this.#relationshipManager
1431
+ )
1432
+ }
1433
+
1434
+ /**
1435
+ * Retrieves the exact coordinate positions of all bars/columns for a chart on the active slide.
1436
+ * Calculates absolute layout limits in EMUs (English Metric Units).
1437
+ *
1438
+ * @param {string} chartId The unique chart name/id in the template slide.
1439
+ * @returns {Promise<Array<{series: string, category: string, seriesIndex: number, categoryIndex: number, value: number, x: number, y: number, width: number, height: number}>>} An array of bar geometry objects.
1440
+ */
1441
+ async getChartBarPositions(chartId) {
1442
+ this.#assertLoaded()
1443
+ const targetIndices = this.#getTargetSlideIndices()
1444
+ if (targetIndices.length === 0) return []
1445
+ const idx = targetIndices[0]
1446
+ return this.#chartManager.getChartBarPositions(
1447
+ idx,
1448
+ chartId,
1449
+ this.#slideManager,
1450
+ this.#relationshipManager
1451
+ )
1452
+ }
1453
+
1454
+ /**
1455
+ * Adds a textbox shape at a specific EMU coordinate position on targeted slides.
1456
+ * Supports custom font styling and alignment configuration.
1457
+ *
1458
+ * @param {Object} options Textbox positioning and style configuration.
1459
+ * @param {string} options.text Text content to insert in the textbox.
1460
+ * @param {number} options.x Bounding box X offset coordinate (in EMUs).
1461
+ * @param {number} options.y Bounding box Y offset coordinate (in EMUs).
1462
+ * @param {number} [options.width=1200000] Bounding box width (in EMUs).
1463
+ * @param {number} [options.height=300000] Bounding box height (in EMUs).
1464
+ * @param {Object} [options.style] Font formatting properties (fontSize, fontFamily, color, bold, italic, align).
1465
+ * @returns {this} The chainable presentation engine instance.
1466
+ */
1467
+ addTextAtPosition(options) {
1468
+ this.#assertLoaded()
1469
+ const targetIndices = this.#getTargetSlideIndices()
1470
+ for (const idx of targetIndices) {
1471
+ const p = this.#chartManager.addTextAtPosition(idx, options, this.#slideManager)
1472
+ this.#zipManager.addPendingPromise(p)
1473
+ }
1474
+ return this
1475
+ }
1476
+
1477
+ /**
1478
+ * Dynamically places textboxes next to a chart's data labels with vertical collision avoidance.
1479
+ * Textboxes are positioned either on the left or right of the chart area, vertically aligned with their corresponding label.
1480
+ *
1481
+ * @param {Object} options Text alignment, naming, and position configuration.
1482
+ * @param {string} options.chart The target chart name/id.
1483
+ * @param {string|Function} options.text Static label text or a callback function receiving `({ series, category, value })`.
1484
+ * @param {'left'|'right'} [options.position='left'] Alignment position relative to the chart boundaries.
1485
+ * @param {Object} [options.style] Text styling attributes (fontSize, fontFamily, color, bold, italic, align, autoFit).
1486
+ * @returns {this} The chainable presentation engine instance.
1487
+ */
1488
+ addTextNearChartLabel(options) {
1489
+ this.#assertLoaded()
1490
+ const targetIndices = this.#getTargetSlideIndices()
1491
+ for (const idx of targetIndices) {
1492
+ const p = this.#chartManager.addTextNearChartLabel(
1493
+ idx,
1494
+ options,
1495
+ this.#slideManager,
1496
+ this.#relationshipManager
1497
+ )
1498
+ this.#zipManager.addPendingPromise(p)
1499
+ }
1500
+ return this
1501
+ }
1502
+
1386
1503
  /**
1387
1504
  * Updates shape text or list content by placeholder tag or shape name/ID.
1388
1505
  * Supports bullet lists, numbered lists, nested lists, and custom styling.
@@ -1485,6 +1602,46 @@ class PPTXTemplater {
1485
1602
  return this
1486
1603
  }
1487
1604
 
1605
+ /**
1606
+ * Updates the position and/or dimensions of an existing shape on targeted slides.
1607
+ *
1608
+ * @param {string} shapeId The unique shape name/id in the template slide.
1609
+ * @param {Object} options Positioning and styling dimensions config.
1610
+ * @param {number} [options.x] Absolute X offset coordinate (in EMUs).
1611
+ * @param {number} [options.y] Absolute Y offset coordinate (in EMUs).
1612
+ * @param {number} [options.width] Bounding box width (in EMUs).
1613
+ * @param {number} [options.height] Bounding box height (in EMUs).
1614
+ * @returns {this} The chainable presentation engine instance.
1615
+ */
1616
+ updateShapePosition(shapeId, options = {}) {
1617
+ this.#assertLoaded()
1618
+ const targetIndices = this.#getTargetSlideIndices()
1619
+ for (const idx of targetIndices) {
1620
+ this.#shapeManager.updateShapePosition(idx, shapeId, options, this.#slideManager)
1621
+ }
1622
+ return this
1623
+ }
1624
+
1625
+ /**
1626
+ * Updates the position and/or dimensions of an existing textbox on targeted slides.
1627
+ *
1628
+ * @param {string} textBoxId The unique textbox shape name/id in the template slide.
1629
+ * @param {Object} options Positioning and styling dimensions config.
1630
+ * @param {number} [options.x] Absolute X offset coordinate (in EMUs).
1631
+ * @param {number} [options.y] Absolute Y offset coordinate (in EMUs).
1632
+ * @param {number} [options.width] Bounding box width (in EMUs).
1633
+ * @param {number} [options.height] Bounding box height (in EMUs).
1634
+ * @returns {this} The chainable presentation engine instance.
1635
+ */
1636
+ updateTextBoxPosition(textBoxId, options = {}) {
1637
+ this.#assertLoaded()
1638
+ const targetIndices = this.#getTargetSlideIndices()
1639
+ for (const idx of targetIndices) {
1640
+ this.#shapeManager.updateTextBoxPosition(idx, textBoxId, options, this.#slideManager)
1641
+ }
1642
+ return this
1643
+ }
1644
+
1488
1645
  cloneShape(shapeId, newShapeId, options = {}) {
1489
1646
  this.#assertLoaded()
1490
1647
  const targetIndices = this.#getTargetSlideIndices()
@@ -497,6 +497,176 @@ class ValidationEngine {
497
497
  warnings,
498
498
  }
499
499
  }
500
+
501
+ /**
502
+ * Validates chart labels for stacked bar charts.
503
+ *
504
+ * @param {string} xml - Chart XML string.
505
+ * @param {Object} options - Validation options.
506
+ * @returns {Object} report
507
+ */
508
+ static validateChartLabels(xml, options = {}) {
509
+ const errors = []
510
+ const warnings = []
511
+
512
+ if (!xml) {
513
+ errors.push('Chart XML must be provided')
514
+ return { valid: false, errors, warnings }
515
+ }
516
+
517
+ // Check chart type is stacked bar
518
+ const isBarChart = xml.includes('c:barChart')
519
+ const isStacked = xml.includes('val="stacked"') || xml.includes('val="percentStacked"')
520
+ if (!isBarChart || !isStacked) {
521
+ warnings.push(
522
+ 'Chart is not a stacked bar chart (expected <c:barChart> with stacked grouping)'
523
+ )
524
+ }
525
+
526
+ // Verify label count consistency if options.labels is provided
527
+ let ptsCount = 0
528
+ const catMatch = /<c:cat>([\s\S]*?)<\/c:cat>/.exec(xml)
529
+ const valMatch = /<c:val>([\s\S]*?)<\/c:val>/.exec(xml)
530
+ const targetBlock = catMatch ? catMatch[1] : valMatch ? valMatch[1] : ''
531
+ const ptCountMatch = /<c:ptCount val="(\d+)"\/>/.exec(targetBlock)
532
+ if (ptCountMatch) {
533
+ ptsCount = parseInt(ptCountMatch[1], 10)
534
+ }
535
+
536
+ if (options.labels) {
537
+ if (ptsCount > 0 && options.labels.length !== ptsCount) {
538
+ errors.push(
539
+ `Label count (${options.labels.length}) does not match chart data points count (${ptsCount})`
540
+ )
541
+ }
542
+ }
543
+
544
+ // Check series name availability (meaning if showSeriesNameInBar is requested, does the chart have series names?)
545
+ if (options.showSeriesNameInBar) {
546
+ const hasSeriesName = xml.includes('<c:tx>') || xml.includes('<c:f>')
547
+ if (!hasSeriesName) {
548
+ warnings.push('Series name might not be available or defined in the template')
549
+ }
550
+ }
551
+
552
+ // Check template style availability (dLbls, txPr, etc.)
553
+ const hasDLbls = xml.includes('<c:dLbls>')
554
+ const hasTxPr = xml.includes('<c:txPr>')
555
+ if (!hasDLbls) {
556
+ warnings.push('Template does not contain default data labels (<c:dLbls>)')
557
+ } else if (!hasTxPr) {
558
+ warnings.push('Template data labels do not have styling properties (<c:txPr>)')
559
+ }
560
+
561
+ return {
562
+ valid: errors.length === 0,
563
+ errors,
564
+ warnings,
565
+ }
566
+ }
567
+
568
+ /**
569
+ * Validates series name labels configuration.
570
+ *
571
+ * @param {string} xml - Chart XML string.
572
+ * @param {Object} xfrm - Chart graphic frame xfrm.
573
+ * @param {Object} options - Configuration options.
574
+ * @returns {Object} report
575
+ */
576
+ static validateSeriesNameLabels(xml, xfrm, options = {}) {
577
+ const errors = []
578
+ const warnings = []
579
+
580
+ if (!options || !options.enabled) {
581
+ return { valid: true, errors, warnings }
582
+ }
583
+
584
+ const { position } = options
585
+ const allowedPositions = ['left', 'right']
586
+ if (!position || !allowedPositions.includes(position)) {
587
+ errors.push(`Invalid position "${position}". Only "left" and "right" are supported.`)
588
+ }
589
+
590
+ if (options.style) {
591
+ if (typeof options.style !== 'object' || Array.isArray(options.style)) {
592
+ errors.push('style must be a key-value object')
593
+ } else {
594
+ const { fontSize, bold, italic, color, fontFamily, align } = options.style
595
+ if (fontSize !== undefined && (typeof fontSize !== 'number' || fontSize <= 0)) {
596
+ errors.push('style.fontSize must be a positive number')
597
+ }
598
+ if (bold !== undefined && typeof bold !== 'boolean') {
599
+ errors.push('style.bold must be a boolean')
600
+ }
601
+ if (italic !== undefined && typeof italic !== 'boolean') {
602
+ errors.push('style.italic must be a boolean')
603
+ }
604
+ if (color !== undefined && typeof color !== 'string') {
605
+ errors.push('style.color must be a string')
606
+ } else if (color !== undefined) {
607
+ const cleanColor = color.replace('#', '').trim()
608
+ if (!/^[0-9A-Fa-f]{6}$/.test(cleanColor)) {
609
+ errors.push(`style.color "${color}" is not a valid hex color (e.g. "#FF0000")`)
610
+ }
611
+ }
612
+ if (fontFamily !== undefined && typeof fontFamily !== 'string') {
613
+ errors.push('style.fontFamily must be a string')
614
+ }
615
+ if (align !== undefined && !['left', 'right', 'center'].includes(align)) {
616
+ errors.push(
617
+ `Invalid style.align "${align}". Supported alignments are "left", "right", "center"`
618
+ )
619
+ }
620
+ }
621
+ }
622
+
623
+ if (!xml) {
624
+ errors.push('Chart XML must be provided')
625
+ return { valid: false, errors, warnings }
626
+ }
627
+
628
+ if (!xfrm) {
629
+ errors.push('Chart coordinates (xfrm) must be resolved')
630
+ return { valid: false, errors, warnings }
631
+ }
632
+
633
+ // Check plot area layout
634
+ const plotAreaMatch = /<c:plotArea>([\s\S]*?)<\/c:plotArea>/.exec(xml)
635
+ if (!plotAreaMatch) {
636
+ errors.push('Plot area not detected in chart XML')
637
+ }
638
+
639
+ // Verify slide boundary collision
640
+ if (position === 'left') {
641
+ if (xfrm.left <= 0) {
642
+ errors.push(
643
+ `Chart left boundary (${xfrm.left}) is at or outside slide bounds, labels on the left cannot fit`
644
+ )
645
+ } else if (xfrm.left < 500000) {
646
+ warnings.push(
647
+ `Chart left boundary (${xfrm.left}) is very close to slide edge, labels on the left might clip`
648
+ )
649
+ }
650
+ } else if (position === 'right') {
651
+ const chartRight = xfrm.left + xfrm.width
652
+ const slideWidth = 12192000
653
+ if (chartRight >= slideWidth) {
654
+ errors.push(
655
+ `Chart right boundary (${chartRight}) is at or outside slide bounds, labels on the right cannot fit`
656
+ )
657
+ } else if (slideWidth - chartRight < 500000) {
658
+ warnings.push(
659
+ `Chart right boundary (${chartRight}) is very close to slide edge, labels on the right might clip`
660
+ )
661
+ }
662
+ }
663
+
664
+ return {
665
+ valid: errors.length === 0,
666
+ errors,
667
+ warnings,
668
+ }
669
+ }
500
670
  }
501
671
 
502
672
  module.exports = { ValidationEngine }