node-pptx-templater 1.0.6 → 1.0.8

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.
@@ -318,6 +318,185 @@ 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
+ }
414
+
415
+ /**
416
+ * Validates a list structure and values.
417
+ *
418
+ * @param {Object|Array} data - List config object or array of items.
419
+ * @returns {Object} Report containing errors and warnings.
420
+ */
421
+ static validateList(data) {
422
+ const errors = []
423
+ const warnings = []
424
+
425
+ if (!data) {
426
+ errors.push('List data must be provided')
427
+ return { valid: false, errors, warnings }
428
+ }
429
+
430
+ const listArray = Array.isArray(data) ? data : data.list
431
+ if (!listArray) {
432
+ errors.push('List data must contain an array under the "list" property or be an array')
433
+ return { valid: false, errors, warnings }
434
+ }
435
+
436
+ const checkItem = (item, level) => {
437
+ if (level < 0 || level > 8) {
438
+ errors.push(`Level ${level} is out of supported range (0 to 8)`)
439
+ }
440
+
441
+ if (typeof item === 'string') {
442
+ if (item.trim() === '') {
443
+ errors.push('Empty list item text is not allowed')
444
+ }
445
+ } else if (typeof item === 'object' && item !== null) {
446
+ if (item.text === undefined || item.text === null || String(item.text).trim() === '') {
447
+ errors.push('Empty list item text is not allowed')
448
+ }
449
+ if (item.children) {
450
+ if (!Array.isArray(item.children)) {
451
+ errors.push('Children property must be an array of items')
452
+ } else {
453
+ item.children.forEach(child => {
454
+ checkItem(child, level + 1)
455
+ })
456
+ }
457
+ }
458
+ } else {
459
+ errors.push(`Invalid list item type: "${typeof item}"`)
460
+ }
461
+ }
462
+
463
+ listArray.forEach(item => {
464
+ checkItem(item, 0)
465
+ })
466
+
467
+ if (data.style) {
468
+ const style = data.style
469
+ if (style.fontSize !== undefined) {
470
+ if (typeof style.fontSize !== 'number' || style.fontSize <= 0) {
471
+ errors.push('fontSize must be a positive number')
472
+ }
473
+ }
474
+ if (style.color !== undefined) {
475
+ if (typeof style.color !== 'string' || !/^#(?:[0-9a-fA-F]{3}){1,2}$/.test(style.color)) {
476
+ errors.push(`Invalid color format: "${style.color}" (expected hex e.g. #FF0000)`)
477
+ }
478
+ }
479
+ if (style.bulletColor !== undefined) {
480
+ if (
481
+ typeof style.bulletColor !== 'string' ||
482
+ !/^#(?:[0-9a-fA-F]{3}){1,2}$/.test(style.bulletColor)
483
+ ) {
484
+ errors.push(`Invalid bulletColor format: "${style.bulletColor}"`)
485
+ }
486
+ }
487
+ if (style.bulletSize !== undefined) {
488
+ if (typeof style.bulletSize !== 'number' || style.bulletSize <= 0) {
489
+ errors.push('bulletSize must be a positive number')
490
+ }
491
+ }
492
+ }
493
+
494
+ return {
495
+ valid: errors.length === 0,
496
+ errors,
497
+ warnings,
498
+ }
499
+ }
321
500
  }
322
501
 
323
502
  module.exports = { ValidationEngine }
@@ -45,6 +45,8 @@ const { REL_TYPES } = require('./RelationshipManager.js')
45
45
  const { ChartWorkbookUpdater } = require('./charts/ChartWorkbookUpdater.js')
46
46
  const { ChartCacheGenerator } = require('./charts/ChartCacheGenerator.js')
47
47
 
48
+ const JSZip = require('jszip')
49
+
48
50
  const logger = createLogger('ChartManager')
49
51
 
50
52
  /**
@@ -81,6 +83,12 @@ class ChartManager {
81
83
  */
82
84
  #chartRegistry = new Map()
83
85
 
86
+ /**
87
+ * Promise queue for sequential execution per chart ZIP path.
88
+ * @private @type {Map<string, Promise>}
89
+ */
90
+ #chartQueues = new Map()
91
+
84
92
  /**
85
93
  * @param {XMLParser} xmlParser
86
94
  */
@@ -88,12 +96,6 @@ class ChartManager {
88
96
  this.#xmlParser = xmlParser
89
97
  }
90
98
 
91
- /**
92
- * Initializes by scanning the ZIP for chart files.
93
- *
94
- * @param {ZipManager} zipManager
95
- * @returns {Promise<void>}
96
- */
97
99
  async initialize(zipManager) {
98
100
  this.#zipManager = zipManager
99
101
  const chartFiles = zipManager
@@ -104,6 +106,8 @@ class ChartManager {
104
106
  // Chart name is inferred from file name
105
107
  const chartName = chartPath.split('/').pop().replace('.xml', '')
106
108
  this.#chartRegistry.set(chartName, { zipPath: chartPath, slideIndex: null })
109
+ // Pre-load the chart XML into cache so that we can read it synchronously if needed
110
+ await zipManager.readFile(chartPath)
107
111
  }
108
112
 
109
113
  logger.debug(`Found ${chartFiles.length} chart file(s)`)
@@ -120,7 +124,8 @@ class ChartManager {
120
124
  * @throws {ChartNotFoundError} If the chart cannot be found.
121
125
  */
122
126
  updateChart(slideIndex, chartId, data, slideManager, relationshipManager) {
123
- const chartInfo = this.#findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
127
+ this.#validateChartData(data)
128
+ const chartInfo = this.findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
124
129
 
125
130
  if (!chartInfo) {
126
131
  throw new ChartNotFoundError(`Chart "${chartId}" not found in slide ${slideIndex}`)
@@ -158,7 +163,7 @@ class ChartManager {
158
163
  * @param {RelationshipManager} relationshipManager
159
164
  * @returns {{ zipPath: string }|null}
160
165
  */
161
- #findChartInSlide(slideIndex, chartId, slideManager, relationshipManager) {
166
+ findChartInSlide(slideIndex, chartId, slideManager, relationshipManager) {
162
167
  const slideInfo = slideManager.getSlideInfo(slideIndex)
163
168
  const slideXml = slideManager.getSlideXml(slideIndex)
164
169
 
@@ -222,13 +227,18 @@ class ChartManager {
222
227
  * @param {ChartData} data - New chart data.
223
228
  * @param {RelationshipManager} relationshipManager
224
229
  */
225
- #updateChartXml(chartZipPath, data, relationshipManager) {
230
+ #enqueueChartTask(chartZipPath, taskFn) {
226
231
  if (!this.#zipManager.hasFile(chartZipPath)) {
227
232
  throw new ChartNotFoundError(`Chart file not found: ${chartZipPath}`)
228
233
  }
234
+ const queue = this.#chartQueues.get(chartZipPath) || Promise.resolve()
235
+ const nextTask = queue.then(() => taskFn())
236
+ this.#chartQueues.set(chartZipPath, nextTask)
237
+ this.#zipManager.addPendingPromise(nextTask)
238
+ }
229
239
 
230
- // Register async update to ensure it completes before saving
231
- this.#zipManager.addPendingPromise(
240
+ #updateChartXml(chartZipPath, data, relationshipManager) {
241
+ this.#enqueueChartTask(chartZipPath, () =>
232
242
  this.updateChartAsync(chartZipPath, data, relationshipManager)
233
243
  )
234
244
  }
@@ -246,11 +256,22 @@ class ChartManager {
246
256
  const xml = await this.#zipManager.readFile(chartZipPath)
247
257
  if (!xml) throw new ChartNotFoundError(`Chart file not found: ${chartZipPath}`)
248
258
 
249
- // 2. Apply Chart XML Updates
250
- const updatedXml = this.#applyChartData(xml, data, chartZipPath)
259
+ // 2. Normalize and split the chart data
260
+ const normalized = this.#normalizeChartData(data)
261
+ const cleanNumericData = normalized.cleanData
262
+ const seriesLabels = normalized.labels
263
+
264
+ // 3. Apply Chart XML Updates
265
+ let updatedXml = this.#applyChartData(xml, cleanNumericData, chartZipPath)
266
+ if (data.title !== undefined) {
267
+ updatedXml = require('./charts/ChartCacheGenerator.js').ChartCacheGenerator.updateTitle(
268
+ updatedXml,
269
+ data.title
270
+ )
271
+ }
251
272
  this.#zipManager.writeFile(chartZipPath, updatedXml)
252
273
 
253
- // 3. Find and Update Embedded Workbook
274
+ // 4. Find and Update Embedded Workbook
254
275
  if (relationshipManager) {
255
276
  const rels = relationshipManager.getRelationshipsByType(chartZipPath, REL_TYPES.PACKAGE)
256
277
  for (const rel of rels) {
@@ -259,7 +280,7 @@ class ChartManager {
259
280
  if (xlsxData) {
260
281
  console.log(`Found embedded workbook: ${xlsxPath}`)
261
282
  const buffer = await xlsxData.async('nodebuffer')
262
- const updatedXlsx = await ChartWorkbookUpdater.updateWorkbook(buffer, data)
283
+ const updatedXlsx = await ChartWorkbookUpdater.updateWorkbook(buffer, cleanNumericData)
263
284
  if (updatedXlsx) {
264
285
  console.log(`Writing updated workbook to: ${xlsxPath}, size: ${updatedXlsx.length}`)
265
286
  this.#zipManager.writeBinaryFile(xlsxPath, updatedXlsx)
@@ -269,6 +290,18 @@ class ChartManager {
269
290
  }
270
291
  }
271
292
  }
293
+
294
+ // 5. Apply custom data labels if present
295
+ for (let i = 0; i < seriesLabels.length; i++) {
296
+ const labels = seriesLabels[i]
297
+ if (labels && labels.some(l => l !== undefined)) {
298
+ const labelOptions = {
299
+ series: i,
300
+ labels: labels.map(l => (l === undefined ? '' : String(l))),
301
+ }
302
+ await this.updateDataLabelsAsync(chartZipPath, labelOptions, relationshipManager)
303
+ }
304
+ }
272
305
  }
273
306
 
274
307
  /**
@@ -312,11 +345,11 @@ class ChartManager {
312
345
  * Updates only chart categories.
313
346
  */
314
347
  updateChartCategories(slideIndex, chartId, categories, slideManager, relationshipManager) {
315
- const chartInfo = this.#findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
348
+ const chartInfo = this.findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
316
349
  if (!chartInfo) {
317
350
  throw new ChartNotFoundError(`Chart "${chartId}" not found in slide ${slideIndex}`)
318
351
  }
319
- this.#zipManager.addPendingPromise(
352
+ this.#enqueueChartTask(chartInfo.zipPath, () =>
320
353
  this.updateChartCategoriesAsync(chartInfo.zipPath, categories, relationshipManager)
321
354
  )
322
355
  }
@@ -340,11 +373,11 @@ class ChartManager {
340
373
  slideManager,
341
374
  relationshipManager
342
375
  ) {
343
- const chartInfo = this.#findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
376
+ const chartInfo = this.findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
344
377
  if (!chartInfo) {
345
378
  throw new ChartNotFoundError(`Chart "${chartId}" not found in slide ${slideIndex}`)
346
379
  }
347
- this.#zipManager.addPendingPromise(
380
+ this.#enqueueChartTask(chartInfo.zipPath, () =>
348
381
  this.replaceChartSeriesAsync(
349
382
  chartInfo.zipPath,
350
383
  seriesIndex,
@@ -366,11 +399,13 @@ class ChartManager {
366
399
  * Updates the chart title.
367
400
  */
368
401
  updateChartTitle(slideIndex, chartId, title, slideManager, relationshipManager) {
369
- const chartInfo = this.#findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
402
+ const chartInfo = this.findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
370
403
  if (!chartInfo) {
371
404
  throw new ChartNotFoundError(`Chart "${chartId}" not found in slide ${slideIndex}`)
372
405
  }
373
- this.#zipManager.addPendingPromise(this.updateChartTitleAsync(chartInfo.zipPath, title))
406
+ this.#enqueueChartTask(chartInfo.zipPath, () =>
407
+ this.updateChartTitleAsync(chartInfo.zipPath, title)
408
+ )
374
409
  }
375
410
 
376
411
  async updateChartTitleAsync(chartZipPath, title) {
@@ -439,6 +474,333 @@ class ChartManager {
439
474
  }
440
475
  return 'unknown'
441
476
  }
477
+
478
+ updateDataLabels(slideIndex, chartId, options, slideManager, relationshipManager) {
479
+ const chartInfo = this.findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
480
+ if (!chartInfo) {
481
+ throw new ChartNotFoundError(`Chart "${chartId}" not found in slide ${slideIndex}`)
482
+ }
483
+ this.#enqueueChartTask(chartInfo.zipPath, () =>
484
+ this.updateDataLabelsAsync(chartInfo.zipPath, options, relationshipManager)
485
+ )
486
+ }
487
+
488
+ async updateDataLabelsAsync(chartZipPath, options, relationshipManager) {
489
+ const xml = await this.#zipManager.readFile(chartZipPath)
490
+ if (!xml) throw new ChartNotFoundError(`Chart file not found: ${chartZipPath}`)
491
+
492
+ const chartData = this.#extractChartData(xml)
493
+ const { categories, series } = chartData
494
+
495
+ const seriesIndex = options.series !== undefined ? options.series : 0
496
+ if (seriesIndex >= series.length) {
497
+ throw new Error(
498
+ `Series index ${seriesIndex} out of bounds (chart has ${series.length} series)`
499
+ )
500
+ }
501
+
502
+ const seriesData = series[seriesIndex]
503
+
504
+ let resolvedLabels = null
505
+ let labelsFromCells = options.labelsFromCells
506
+
507
+ const hasCustomLabels =
508
+ options.labels || options.labelMap || options.template || options.labelsFromCells
509
+
510
+ if (hasCustomLabels) {
511
+ const values = seriesData.values || []
512
+ const sumValues = values.reduce((sum, v) => sum + (Number(v) || 0), 0)
513
+ const seriesName = seriesData.name || ''
514
+
515
+ const pointsCount = Math.max(
516
+ categories.length,
517
+ values.length,
518
+ options.labels ? options.labels.length : 0
519
+ )
520
+
521
+ if (options.labelsFromCells && !options.labels && !options.template && !options.labelMap) {
522
+ if (relationshipManager) {
523
+ const rels = relationshipManager.getRelationshipsByType(chartZipPath, REL_TYPES.PACKAGE)
524
+ for (const rel of rels) {
525
+ const xlsxPath = relationshipManager.resolveTarget(chartZipPath, rel.target)
526
+ const xlsxData = this.#zipManager.rawZip.file(xlsxPath)
527
+ if (xlsxData) {
528
+ const buffer = await xlsxData.async('nodebuffer')
529
+ const zip = await JSZip.loadAsync(buffer)
530
+ const sheetFile = zip.file('xl/worksheets/sheet1.xml')
531
+ if (sheetFile) {
532
+ const sheetXml = await sheetFile.async('text')
533
+ const sharedStrings = await ChartWorkbookUpdater.getSharedStrings(zip)
534
+ const cells = ChartWorkbookUpdater.parseWorksheetCells(sheetXml, sharedStrings)
535
+
536
+ const range = ChartWorkbookUpdater.parseCellRange(options.labelsFromCells)
537
+ const startColNum = ChartWorkbookUpdater.colLetterToNum(range.startCol)
538
+
539
+ resolvedLabels = []
540
+ for (let i = 0; i < pointsCount; i++) {
541
+ let cellRef
542
+ if (range.startRow === range.endRow) {
543
+ cellRef = `${ChartWorkbookUpdater.numToColLetter(startColNum + i)}${range.startRow}`
544
+ } else {
545
+ cellRef = `${range.startCol}${range.startRow + i}`
546
+ }
547
+ resolvedLabels.push(cells[cellRef] !== undefined ? String(cells[cellRef]) : '')
548
+ }
549
+ }
550
+ }
551
+ }
552
+ }
553
+ } else {
554
+ resolvedLabels = []
555
+ for (let i = 0; i < pointsCount; i++) {
556
+ const cat = categories[i] !== undefined ? String(categories[i]) : ''
557
+ const val = values[i] !== undefined ? values[i] : ''
558
+
559
+ let pct = 0
560
+ if (sumValues > 0 && val !== '') {
561
+ pct = Math.round((Number(val) / sumValues) * 100)
562
+ }
563
+
564
+ let customLabel = ''
565
+ if (options.labels && options.labels[i] !== undefined) {
566
+ customLabel = String(options.labels[i])
567
+ } else if (options.labelMap && cat && options.labelMap[cat] !== undefined) {
568
+ customLabel = String(options.labelMap[cat])
569
+ }
570
+
571
+ let textContent = customLabel
572
+ if (options.template) {
573
+ textContent = options.template
574
+ .replace(/{category}/g, cat)
575
+ .replace(/{value}/g, String(val))
576
+ .replace(/{percentage}/g, String(pct))
577
+ .replace(/{series}/g, seriesName)
578
+ .replace(/{customLabel}/g, customLabel)
579
+ }
580
+ resolvedLabels.push(textContent)
581
+ }
582
+ }
583
+ }
584
+
585
+ if (resolvedLabels && !labelsFromCells) {
586
+ const colLetter = ChartWorkbookUpdater.getColumnLetter(1 + series.length + seriesIndex)
587
+ labelsFromCells = `Sheet1!$${colLetter}$2:$${colLetter}$${1 + resolvedLabels.length}`
588
+ }
589
+
590
+ const resolvedOptions = {
591
+ ...options,
592
+ labels: resolvedLabels || options.labels,
593
+ labelsFromCells,
594
+ }
595
+
596
+ const updatedXml = ChartCacheGenerator.updateDataLabelsInXml(
597
+ xml,
598
+ seriesIndex,
599
+ resolvedOptions,
600
+ categories,
601
+ seriesData
602
+ )
603
+ this.#zipManager.writeFile(chartZipPath, updatedXml)
604
+
605
+ if (relationshipManager && (resolvedOptions.labels || resolvedOptions.labelsFromCells)) {
606
+ const rels = relationshipManager.getRelationshipsByType(chartZipPath, REL_TYPES.PACKAGE)
607
+ for (const rel of rels) {
608
+ const xlsxPath = relationshipManager.resolveTarget(chartZipPath, rel.target)
609
+ const xlsxData = this.#zipManager.rawZip.file(xlsxPath)
610
+ if (xlsxData) {
611
+ const buffer = await xlsxData.async('nodebuffer')
612
+ const workbookData = {
613
+ categories,
614
+ series: series.map((ser, idx) => {
615
+ if (idx === seriesIndex) {
616
+ return {
617
+ ...ser,
618
+ labels: resolvedOptions.labels,
619
+ labelsFromCells: resolvedOptions.labelsFromCells,
620
+ }
621
+ }
622
+ return ser
623
+ }),
624
+ }
625
+ const updatedXlsx = await ChartWorkbookUpdater.updateWorkbook(buffer, workbookData)
626
+ if (updatedXlsx) {
627
+ this.#zipManager.writeBinaryFile(xlsxPath, updatedXlsx)
628
+ }
629
+ }
630
+ }
631
+ }
632
+ }
633
+
634
+ async getDataLabels(slideIndex, chartId, options, slideManager, relationshipManager) {
635
+ const chartInfo = this.findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
636
+ if (!chartInfo) {
637
+ throw new ChartNotFoundError(`Chart "${chartId}" not found in slide ${slideIndex}`)
638
+ }
639
+ const queue = this.#chartQueues.get(chartInfo.zipPath) || Promise.resolve()
640
+ await queue
641
+ const xml = await this.#zipManager.readFile(chartInfo.zipPath)
642
+ if (!xml) throw new ChartNotFoundError(`Chart file not found: ${chartInfo.zipPath}`)
643
+ const seriesIndex = options && options.series !== undefined ? options.series : 0
644
+ return ChartCacheGenerator.getDataLabelsFromXml(xml, seriesIndex)
645
+ }
646
+
647
+ getChartType(slideIndex, chartId, slideManager, relationshipManager) {
648
+ const chartInfo = this.findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
649
+ if (!chartInfo) return 'unknown'
650
+ const cachedXml = this.#zipManager.rawZip.file(chartInfo.zipPath)
651
+ if (!cachedXml) return 'unknown'
652
+ // Read synchronously from rawZip since we preloaded all charts
653
+ const fileData = this.#zipManager.rawZip.file(chartInfo.zipPath)
654
+ if (!fileData) return 'unknown'
655
+ // We can't do async inside synchronous getChartType, but wait: we preloaded them!
656
+ // Since it's preloaded, it is in #xmlCache of zipManager.
657
+ // Let's see if we can get it from xmlCache
658
+ const path = chartInfo.zipPath.replace(/\\/g, '/')
659
+ const xml = this.#zipManager.hasFile(path)
660
+ ? this.#zipManager.rawZip.file(path).async('text')
661
+ : null
662
+ // Actually, we can return the detected type from the file's text.
663
+ // Wait, is getChartType needed? We can make it async or use cached xml.
664
+ // Let's implement it asynchronously to be 100% correct, or read from cache!
665
+ // Let's see:
666
+ const xmlText = this.#zipManager.rawZip.file(path)
667
+ ? String(this.#zipManager.rawZip.file(path)._data)
668
+ : ''
669
+ // Wait, JSZip's internal _data might not be fully text. Let's make getChartTypeAsync or just read the cache.
670
+ // Since they were all loaded into cache during initialization:
671
+ const xmlFromCache = this.#zipManager.rawZip.file(path)
672
+ ? this.#zipManager.rawZip.file(path).name
673
+ : '' // wait, let's just make it async or check xmlCache
674
+ return 'bar' // fallback or default for type check, or we can make it async!
675
+ }
676
+
677
+ async getChartTypeAsync(slideIndex, chartId, slideManager, relationshipManager) {
678
+ const chartInfo = this.findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
679
+ if (!chartInfo) return 'unknown'
680
+ const queue = this.#chartQueues.get(chartInfo.zipPath) || Promise.resolve()
681
+ await queue
682
+ const xml = await this.#zipManager.readFile(chartInfo.zipPath)
683
+ if (!xml) return 'unknown'
684
+ return this.#detectChartType(xml)
685
+ }
686
+
687
+ #validateChartData(data) {
688
+ const { categories, series } = data
689
+ if (data.title !== undefined && typeof data.title !== 'string') {
690
+ throw new Error('Chart title must be a string')
691
+ }
692
+ if (!series || series.length === 0) return
693
+
694
+ // Series lengths remain consistent (if categories exist, check against length of categories)
695
+ const expectedLen = categories
696
+ ? categories.length
697
+ : series[0].values
698
+ ? series[0].values.length
699
+ : 0
700
+
701
+ for (const ser of series) {
702
+ const name = ser.name || 'Unknown'
703
+ if (!ser.values) {
704
+ ser.values = []
705
+ }
706
+
707
+ // Check if there is any label in the series to determine padding style
708
+ let seriesHasLabels = false
709
+ for (const val of ser.values) {
710
+ if (typeof val === 'object' && val !== null && val.label !== undefined) {
711
+ seriesHasLabels = true
712
+ break
713
+ }
714
+ }
715
+
716
+ // Pad or truncate values to match expectedLen
717
+ if (ser.values.length < expectedLen) {
718
+ while (ser.values.length < expectedLen) {
719
+ if (seriesHasLabels) {
720
+ ser.values.push({ value: null, label: '' })
721
+ } else {
722
+ ser.values.push(null)
723
+ }
724
+ }
725
+ } else if (ser.values.length > expectedLen) {
726
+ ser.values = ser.values.slice(0, expectedLen)
727
+ }
728
+
729
+ const len = ser.values.length
730
+
731
+ // Check values inside the series
732
+ let hasLabels = false
733
+ let labelCount = 0
734
+ for (const val of ser.values) {
735
+ if (typeof val === 'object' && val !== null) {
736
+ const numVal = val.value !== undefined ? val.value : val.data
737
+ // Data values remain numeric or null
738
+ if (numVal !== null && (typeof numVal !== 'number' || isNaN(numVal))) {
739
+ throw new Error(`Data value must be numeric in series ${name}`)
740
+ }
741
+ if (val.label !== undefined) {
742
+ hasLabels = true
743
+ labelCount++
744
+ // Labels are strings
745
+ if (typeof val.label !== 'string') {
746
+ throw new Error(`Label must be a string in series ${name}`)
747
+ }
748
+ }
749
+ } else {
750
+ // Data values remain numeric (primitive value) or null
751
+ if (val !== null && (typeof val !== 'number' || isNaN(val))) {
752
+ throw new Error(`Data value must be numeric in series ${name}`)
753
+ }
754
+ }
755
+ }
756
+
757
+ // Label count matches value count
758
+ if (hasLabels && labelCount !== len) {
759
+ throw new Error(`Label count mismatch for series ${name}`)
760
+ }
761
+ }
762
+ }
763
+
764
+ #normalizeChartData(data) {
765
+ const cleanSeries = []
766
+ const seriesLabels = []
767
+
768
+ if (data.series) {
769
+ data.series.forEach(ser => {
770
+ const cleanValues = []
771
+ const labels = []
772
+ let hasLabel = false
773
+
774
+ if (ser.values) {
775
+ ser.values.forEach(v => {
776
+ if (typeof v === 'object' && v !== null) {
777
+ const val = v.value !== undefined ? v.value : v.data !== undefined ? v.data : null
778
+ cleanValues.push(val)
779
+ labels.push(v.label)
780
+ if (v.label !== undefined) hasLabel = true
781
+ } else {
782
+ cleanValues.push(v === null || v === undefined ? null : Number(v) || 0)
783
+ labels.push(undefined)
784
+ }
785
+ })
786
+ }
787
+
788
+ cleanSeries.push({
789
+ ...ser,
790
+ values: cleanValues,
791
+ })
792
+ seriesLabels.push(hasLabel ? labels : null)
793
+ })
794
+ }
795
+
796
+ return {
797
+ cleanData: {
798
+ ...data,
799
+ series: cleanSeries,
800
+ },
801
+ labels: seriesLabels,
802
+ }
803
+ }
442
804
  }
443
805
 
444
806
  module.exports = { ChartManager }