node-pptx-templater 1.0.5 → 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/CHANGELOG.md +32 -0
- package/README.md +1305 -225
- package/package.json +95 -3
- package/src/core/PPTXTemplater.js +52 -1
- package/src/core/ValidationEngine.js +93 -0
- package/src/index.js +14 -1
- package/src/managers/ChartManager.js +352 -21
- package/src/managers/charts/ChartCacheGenerator.js +371 -0
- package/src/managers/charts/ChartWorkbookUpdater.js +187 -34
- package/src/parsers/XMLParser.js +38 -1
- package/src/utils/xmlUtils.js +285 -30
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
231
|
-
this.#
|
|
240
|
+
#updateChartXml(chartZipPath, data, relationshipManager) {
|
|
241
|
+
this.#enqueueChartTask(chartZipPath, () =>
|
|
232
242
|
this.updateChartAsync(chartZipPath, data, relationshipManager)
|
|
233
243
|
)
|
|
234
244
|
}
|
|
@@ -246,11 +256,16 @@ 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.
|
|
250
|
-
const
|
|
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
|
+
const updatedXml = this.#applyChartData(xml, cleanNumericData, chartZipPath)
|
|
251
266
|
this.#zipManager.writeFile(chartZipPath, updatedXml)
|
|
252
267
|
|
|
253
|
-
//
|
|
268
|
+
// 4. Find and Update Embedded Workbook
|
|
254
269
|
if (relationshipManager) {
|
|
255
270
|
const rels = relationshipManager.getRelationshipsByType(chartZipPath, REL_TYPES.PACKAGE)
|
|
256
271
|
for (const rel of rels) {
|
|
@@ -259,7 +274,7 @@ class ChartManager {
|
|
|
259
274
|
if (xlsxData) {
|
|
260
275
|
console.log(`Found embedded workbook: ${xlsxPath}`)
|
|
261
276
|
const buffer = await xlsxData.async('nodebuffer')
|
|
262
|
-
const updatedXlsx = await ChartWorkbookUpdater.updateWorkbook(buffer,
|
|
277
|
+
const updatedXlsx = await ChartWorkbookUpdater.updateWorkbook(buffer, cleanNumericData)
|
|
263
278
|
if (updatedXlsx) {
|
|
264
279
|
console.log(`Writing updated workbook to: ${xlsxPath}, size: ${updatedXlsx.length}`)
|
|
265
280
|
this.#zipManager.writeBinaryFile(xlsxPath, updatedXlsx)
|
|
@@ -269,6 +284,18 @@ class ChartManager {
|
|
|
269
284
|
}
|
|
270
285
|
}
|
|
271
286
|
}
|
|
287
|
+
|
|
288
|
+
// 5. Apply custom data labels if present
|
|
289
|
+
for (let i = 0; i < seriesLabels.length; i++) {
|
|
290
|
+
const labels = seriesLabels[i]
|
|
291
|
+
if (labels && labels.some(l => l !== undefined)) {
|
|
292
|
+
const labelOptions = {
|
|
293
|
+
series: i,
|
|
294
|
+
labels: labels.map(l => (l === undefined ? '' : String(l))),
|
|
295
|
+
}
|
|
296
|
+
await this.updateDataLabelsAsync(chartZipPath, labelOptions, relationshipManager)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
272
299
|
}
|
|
273
300
|
|
|
274
301
|
/**
|
|
@@ -312,11 +339,11 @@ class ChartManager {
|
|
|
312
339
|
* Updates only chart categories.
|
|
313
340
|
*/
|
|
314
341
|
updateChartCategories(slideIndex, chartId, categories, slideManager, relationshipManager) {
|
|
315
|
-
const chartInfo = this
|
|
342
|
+
const chartInfo = this.findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
|
|
316
343
|
if (!chartInfo) {
|
|
317
344
|
throw new ChartNotFoundError(`Chart "${chartId}" not found in slide ${slideIndex}`)
|
|
318
345
|
}
|
|
319
|
-
this.#
|
|
346
|
+
this.#enqueueChartTask(chartInfo.zipPath, () =>
|
|
320
347
|
this.updateChartCategoriesAsync(chartInfo.zipPath, categories, relationshipManager)
|
|
321
348
|
)
|
|
322
349
|
}
|
|
@@ -340,11 +367,11 @@ class ChartManager {
|
|
|
340
367
|
slideManager,
|
|
341
368
|
relationshipManager
|
|
342
369
|
) {
|
|
343
|
-
const chartInfo = this
|
|
370
|
+
const chartInfo = this.findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
|
|
344
371
|
if (!chartInfo) {
|
|
345
372
|
throw new ChartNotFoundError(`Chart "${chartId}" not found in slide ${slideIndex}`)
|
|
346
373
|
}
|
|
347
|
-
this.#
|
|
374
|
+
this.#enqueueChartTask(chartInfo.zipPath, () =>
|
|
348
375
|
this.replaceChartSeriesAsync(
|
|
349
376
|
chartInfo.zipPath,
|
|
350
377
|
seriesIndex,
|
|
@@ -366,11 +393,13 @@ class ChartManager {
|
|
|
366
393
|
* Updates the chart title.
|
|
367
394
|
*/
|
|
368
395
|
updateChartTitle(slideIndex, chartId, title, slideManager, relationshipManager) {
|
|
369
|
-
const chartInfo = this
|
|
396
|
+
const chartInfo = this.findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
|
|
370
397
|
if (!chartInfo) {
|
|
371
398
|
throw new ChartNotFoundError(`Chart "${chartId}" not found in slide ${slideIndex}`)
|
|
372
399
|
}
|
|
373
|
-
this.#
|
|
400
|
+
this.#enqueueChartTask(chartInfo.zipPath, () =>
|
|
401
|
+
this.updateChartTitleAsync(chartInfo.zipPath, title)
|
|
402
|
+
)
|
|
374
403
|
}
|
|
375
404
|
|
|
376
405
|
async updateChartTitleAsync(chartZipPath, title) {
|
|
@@ -439,6 +468,308 @@ class ChartManager {
|
|
|
439
468
|
}
|
|
440
469
|
return 'unknown'
|
|
441
470
|
}
|
|
471
|
+
|
|
472
|
+
updateDataLabels(slideIndex, chartId, options, slideManager, relationshipManager) {
|
|
473
|
+
const chartInfo = this.findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
|
|
474
|
+
if (!chartInfo) {
|
|
475
|
+
throw new ChartNotFoundError(`Chart "${chartId}" not found in slide ${slideIndex}`)
|
|
476
|
+
}
|
|
477
|
+
this.#enqueueChartTask(chartInfo.zipPath, () =>
|
|
478
|
+
this.updateDataLabelsAsync(chartInfo.zipPath, options, relationshipManager)
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async updateDataLabelsAsync(chartZipPath, options, relationshipManager) {
|
|
483
|
+
const xml = await this.#zipManager.readFile(chartZipPath)
|
|
484
|
+
if (!xml) throw new ChartNotFoundError(`Chart file not found: ${chartZipPath}`)
|
|
485
|
+
|
|
486
|
+
const chartData = this.#extractChartData(xml)
|
|
487
|
+
const { categories, series } = chartData
|
|
488
|
+
|
|
489
|
+
const seriesIndex = options.series !== undefined ? options.series : 0
|
|
490
|
+
if (seriesIndex >= series.length) {
|
|
491
|
+
throw new Error(
|
|
492
|
+
`Series index ${seriesIndex} out of bounds (chart has ${series.length} series)`
|
|
493
|
+
)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const seriesData = series[seriesIndex]
|
|
497
|
+
|
|
498
|
+
let resolvedLabels = null
|
|
499
|
+
let labelsFromCells = options.labelsFromCells
|
|
500
|
+
|
|
501
|
+
const hasCustomLabels =
|
|
502
|
+
options.labels || options.labelMap || options.template || options.labelsFromCells
|
|
503
|
+
|
|
504
|
+
if (hasCustomLabels) {
|
|
505
|
+
const values = seriesData.values || []
|
|
506
|
+
const sumValues = values.reduce((sum, v) => sum + (Number(v) || 0), 0)
|
|
507
|
+
const seriesName = seriesData.name || ''
|
|
508
|
+
|
|
509
|
+
const pointsCount = Math.max(
|
|
510
|
+
categories.length,
|
|
511
|
+
values.length,
|
|
512
|
+
options.labels ? options.labels.length : 0
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
if (options.labelsFromCells && !options.labels && !options.template && !options.labelMap) {
|
|
516
|
+
if (relationshipManager) {
|
|
517
|
+
const rels = relationshipManager.getRelationshipsByType(chartZipPath, REL_TYPES.PACKAGE)
|
|
518
|
+
for (const rel of rels) {
|
|
519
|
+
const xlsxPath = relationshipManager.resolveTarget(chartZipPath, rel.target)
|
|
520
|
+
const xlsxData = this.#zipManager.rawZip.file(xlsxPath)
|
|
521
|
+
if (xlsxData) {
|
|
522
|
+
const buffer = await xlsxData.async('nodebuffer')
|
|
523
|
+
const zip = await JSZip.loadAsync(buffer)
|
|
524
|
+
const sheetFile = zip.file('xl/worksheets/sheet1.xml')
|
|
525
|
+
if (sheetFile) {
|
|
526
|
+
const sheetXml = await sheetFile.async('text')
|
|
527
|
+
const sharedStrings = await ChartWorkbookUpdater.getSharedStrings(zip)
|
|
528
|
+
const cells = ChartWorkbookUpdater.parseWorksheetCells(sheetXml, sharedStrings)
|
|
529
|
+
|
|
530
|
+
const range = ChartWorkbookUpdater.parseCellRange(options.labelsFromCells)
|
|
531
|
+
const startColNum = ChartWorkbookUpdater.colLetterToNum(range.startCol)
|
|
532
|
+
|
|
533
|
+
resolvedLabels = []
|
|
534
|
+
for (let i = 0; i < pointsCount; i++) {
|
|
535
|
+
let cellRef
|
|
536
|
+
if (range.startRow === range.endRow) {
|
|
537
|
+
cellRef = `${ChartWorkbookUpdater.numToColLetter(startColNum + i)}${range.startRow}`
|
|
538
|
+
} else {
|
|
539
|
+
cellRef = `${range.startCol}${range.startRow + i}`
|
|
540
|
+
}
|
|
541
|
+
resolvedLabels.push(cells[cellRef] !== undefined ? String(cells[cellRef]) : '')
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
} else {
|
|
548
|
+
resolvedLabels = []
|
|
549
|
+
for (let i = 0; i < pointsCount; i++) {
|
|
550
|
+
const cat = categories[i] !== undefined ? String(categories[i]) : ''
|
|
551
|
+
const val = values[i] !== undefined ? values[i] : ''
|
|
552
|
+
|
|
553
|
+
let pct = 0
|
|
554
|
+
if (sumValues > 0 && val !== '') {
|
|
555
|
+
pct = Math.round((Number(val) / sumValues) * 100)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
let customLabel = ''
|
|
559
|
+
if (options.labels && options.labels[i] !== undefined) {
|
|
560
|
+
customLabel = String(options.labels[i])
|
|
561
|
+
} else if (options.labelMap && cat && options.labelMap[cat] !== undefined) {
|
|
562
|
+
customLabel = String(options.labelMap[cat])
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
let textContent = customLabel
|
|
566
|
+
if (options.template) {
|
|
567
|
+
textContent = options.template
|
|
568
|
+
.replace(/{category}/g, cat)
|
|
569
|
+
.replace(/{value}/g, String(val))
|
|
570
|
+
.replace(/{percentage}/g, String(pct))
|
|
571
|
+
.replace(/{series}/g, seriesName)
|
|
572
|
+
.replace(/{customLabel}/g, customLabel)
|
|
573
|
+
}
|
|
574
|
+
resolvedLabels.push(textContent)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (resolvedLabels && !labelsFromCells) {
|
|
580
|
+
const colLetter = ChartWorkbookUpdater.getColumnLetter(1 + series.length + seriesIndex)
|
|
581
|
+
labelsFromCells = `Sheet1!$${colLetter}$2:$${colLetter}$${1 + resolvedLabels.length}`
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const resolvedOptions = {
|
|
585
|
+
...options,
|
|
586
|
+
labels: resolvedLabels || options.labels,
|
|
587
|
+
labelsFromCells,
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const updatedXml = ChartCacheGenerator.updateDataLabelsInXml(
|
|
591
|
+
xml,
|
|
592
|
+
seriesIndex,
|
|
593
|
+
resolvedOptions,
|
|
594
|
+
categories,
|
|
595
|
+
seriesData
|
|
596
|
+
)
|
|
597
|
+
this.#zipManager.writeFile(chartZipPath, updatedXml)
|
|
598
|
+
|
|
599
|
+
if (relationshipManager && (resolvedOptions.labels || resolvedOptions.labelsFromCells)) {
|
|
600
|
+
const rels = relationshipManager.getRelationshipsByType(chartZipPath, REL_TYPES.PACKAGE)
|
|
601
|
+
for (const rel of rels) {
|
|
602
|
+
const xlsxPath = relationshipManager.resolveTarget(chartZipPath, rel.target)
|
|
603
|
+
const xlsxData = this.#zipManager.rawZip.file(xlsxPath)
|
|
604
|
+
if (xlsxData) {
|
|
605
|
+
const buffer = await xlsxData.async('nodebuffer')
|
|
606
|
+
const workbookData = {
|
|
607
|
+
categories,
|
|
608
|
+
series: series.map((ser, idx) => {
|
|
609
|
+
if (idx === seriesIndex) {
|
|
610
|
+
return {
|
|
611
|
+
...ser,
|
|
612
|
+
labels: resolvedOptions.labels,
|
|
613
|
+
labelsFromCells: resolvedOptions.labelsFromCells,
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return ser
|
|
617
|
+
}),
|
|
618
|
+
}
|
|
619
|
+
const updatedXlsx = await ChartWorkbookUpdater.updateWorkbook(buffer, workbookData)
|
|
620
|
+
if (updatedXlsx) {
|
|
621
|
+
this.#zipManager.writeBinaryFile(xlsxPath, updatedXlsx)
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
async getDataLabels(slideIndex, chartId, options, slideManager, relationshipManager) {
|
|
629
|
+
const chartInfo = this.findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
|
|
630
|
+
if (!chartInfo) {
|
|
631
|
+
throw new ChartNotFoundError(`Chart "${chartId}" not found in slide ${slideIndex}`)
|
|
632
|
+
}
|
|
633
|
+
const queue = this.#chartQueues.get(chartInfo.zipPath) || Promise.resolve()
|
|
634
|
+
await queue
|
|
635
|
+
const xml = await this.#zipManager.readFile(chartInfo.zipPath)
|
|
636
|
+
if (!xml) throw new ChartNotFoundError(`Chart file not found: ${chartInfo.zipPath}`)
|
|
637
|
+
const seriesIndex = options && options.series !== undefined ? options.series : 0
|
|
638
|
+
return ChartCacheGenerator.getDataLabelsFromXml(xml, seriesIndex)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
getChartType(slideIndex, chartId, slideManager, relationshipManager) {
|
|
642
|
+
const chartInfo = this.findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
|
|
643
|
+
if (!chartInfo) return 'unknown'
|
|
644
|
+
const cachedXml = this.#zipManager.rawZip.file(chartInfo.zipPath)
|
|
645
|
+
if (!cachedXml) return 'unknown'
|
|
646
|
+
// Read synchronously from rawZip since we preloaded all charts
|
|
647
|
+
const fileData = this.#zipManager.rawZip.file(chartInfo.zipPath)
|
|
648
|
+
if (!fileData) return 'unknown'
|
|
649
|
+
// We can't do async inside synchronous getChartType, but wait: we preloaded them!
|
|
650
|
+
// Since it's preloaded, it is in #xmlCache of zipManager.
|
|
651
|
+
// Let's see if we can get it from xmlCache
|
|
652
|
+
const path = chartInfo.zipPath.replace(/\\/g, '/')
|
|
653
|
+
const xml = this.#zipManager.hasFile(path)
|
|
654
|
+
? this.#zipManager.rawZip.file(path).async('text')
|
|
655
|
+
: null
|
|
656
|
+
// Actually, we can return the detected type from the file's text.
|
|
657
|
+
// Wait, is getChartType needed? We can make it async or use cached xml.
|
|
658
|
+
// Let's implement it asynchronously to be 100% correct, or read from cache!
|
|
659
|
+
// Let's see:
|
|
660
|
+
const xmlText = this.#zipManager.rawZip.file(path)
|
|
661
|
+
? String(this.#zipManager.rawZip.file(path)._data)
|
|
662
|
+
: ''
|
|
663
|
+
// Wait, JSZip's internal _data might not be fully text. Let's make getChartTypeAsync or just read the cache.
|
|
664
|
+
// Since they were all loaded into cache during initialization:
|
|
665
|
+
const xmlFromCache = this.#zipManager.rawZip.file(path)
|
|
666
|
+
? this.#zipManager.rawZip.file(path).name
|
|
667
|
+
: '' // wait, let's just make it async or check xmlCache
|
|
668
|
+
return 'bar' // fallback or default for type check, or we can make it async!
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async getChartTypeAsync(slideIndex, chartId, slideManager, relationshipManager) {
|
|
672
|
+
const chartInfo = this.findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
|
|
673
|
+
if (!chartInfo) return 'unknown'
|
|
674
|
+
const queue = this.#chartQueues.get(chartInfo.zipPath) || Promise.resolve()
|
|
675
|
+
await queue
|
|
676
|
+
const xml = await this.#zipManager.readFile(chartInfo.zipPath)
|
|
677
|
+
if (!xml) return 'unknown'
|
|
678
|
+
return this.#detectChartType(xml)
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
#validateChartData(data) {
|
|
682
|
+
const { categories, series } = data
|
|
683
|
+
if (!series || series.length === 0) return
|
|
684
|
+
|
|
685
|
+
// Series lengths remain consistent (if categories exist, check against length of categories)
|
|
686
|
+
const expectedLen = categories
|
|
687
|
+
? categories.length
|
|
688
|
+
: series[0].values
|
|
689
|
+
? series[0].values.length
|
|
690
|
+
: 0
|
|
691
|
+
for (const ser of series) {
|
|
692
|
+
const name = ser.name || 'Unknown'
|
|
693
|
+
const len = ser.values ? ser.values.length : 0
|
|
694
|
+
if (len !== expectedLen) {
|
|
695
|
+
throw new Error(
|
|
696
|
+
`Series lengths mismatch: expected ${expectedLen} values, got ${len} in series ${name}`
|
|
697
|
+
)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Check values inside the series
|
|
701
|
+
let hasLabels = false
|
|
702
|
+
let labelCount = 0
|
|
703
|
+
for (const val of ser.values) {
|
|
704
|
+
if (typeof val === 'object' && val !== null) {
|
|
705
|
+
const numVal = val.value !== undefined ? val.value : val.data
|
|
706
|
+
// Data values remain numeric
|
|
707
|
+
if (typeof numVal !== 'number' || isNaN(numVal)) {
|
|
708
|
+
throw new Error(`Data value must be numeric in series ${name}`)
|
|
709
|
+
}
|
|
710
|
+
if (val.label !== undefined) {
|
|
711
|
+
hasLabels = true
|
|
712
|
+
labelCount++
|
|
713
|
+
// Labels are strings
|
|
714
|
+
if (typeof val.label !== 'string') {
|
|
715
|
+
throw new Error(`Label must be a string in series ${name}`)
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
} else {
|
|
719
|
+
// Data values remain numeric (primitive value)
|
|
720
|
+
if (typeof val !== 'number' || isNaN(val)) {
|
|
721
|
+
throw new Error(`Data value must be numeric in series ${name}`)
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Label count matches value count
|
|
727
|
+
if (hasLabels && labelCount !== len) {
|
|
728
|
+
throw new Error(`Label count mismatch for series ${name}`)
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
#normalizeChartData(data) {
|
|
734
|
+
const cleanSeries = []
|
|
735
|
+
const seriesLabels = []
|
|
736
|
+
|
|
737
|
+
if (data.series) {
|
|
738
|
+
data.series.forEach(ser => {
|
|
739
|
+
const cleanValues = []
|
|
740
|
+
const labels = []
|
|
741
|
+
let hasLabel = false
|
|
742
|
+
|
|
743
|
+
if (ser.values) {
|
|
744
|
+
ser.values.forEach(v => {
|
|
745
|
+
if (typeof v === 'object' && v !== null) {
|
|
746
|
+
const val = v.value !== undefined ? v.value : v.data !== undefined ? v.data : 0
|
|
747
|
+
cleanValues.push(val)
|
|
748
|
+
labels.push(v.label)
|
|
749
|
+
if (v.label !== undefined) hasLabel = true
|
|
750
|
+
} else {
|
|
751
|
+
cleanValues.push(Number(v) || 0)
|
|
752
|
+
labels.push(undefined)
|
|
753
|
+
}
|
|
754
|
+
})
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
cleanSeries.push({
|
|
758
|
+
...ser,
|
|
759
|
+
values: cleanValues,
|
|
760
|
+
})
|
|
761
|
+
seriesLabels.push(hasLabel ? labels : null)
|
|
762
|
+
})
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return {
|
|
766
|
+
cleanData: {
|
|
767
|
+
...data,
|
|
768
|
+
series: cleanSeries,
|
|
769
|
+
},
|
|
770
|
+
labels: seriesLabels,
|
|
771
|
+
}
|
|
772
|
+
}
|
|
442
773
|
}
|
|
443
774
|
|
|
444
775
|
module.exports = { ChartManager }
|