node-pptx-templater 1.0.9 → 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.
@@ -89,6 +89,12 @@ class ChartManager {
89
89
  */
90
90
  #chartQueues = new Map()
91
91
 
92
+ /**
93
+ * Promise queue for sequential execution per slide ZIP path.
94
+ * @private @type {Map<string, Promise>}
95
+ */
96
+ #slideQueues = new Map()
97
+
92
98
  /**
93
99
  * @param {XMLParser} xmlParser
94
100
  */
@@ -132,7 +138,14 @@ class ChartManager {
132
138
  }
133
139
 
134
140
  logger.debug(`Updating chart "${chartId}" at ${chartInfo.zipPath}`)
135
- this.#updateChartXml(chartInfo.zipPath, data, relationshipManager)
141
+ this.#updateChartXml(
142
+ chartInfo.zipPath,
143
+ data,
144
+ relationshipManager,
145
+ slideIndex,
146
+ chartId,
147
+ slideManager
148
+ )
136
149
  }
137
150
 
138
151
  /**
@@ -237,9 +250,16 @@ class ChartManager {
237
250
  this.#zipManager.addPendingPromise(nextTask)
238
251
  }
239
252
 
240
- #updateChartXml(chartZipPath, data, relationshipManager) {
253
+ #updateChartXml(chartZipPath, data, relationshipManager, slideIndex, chartId, slideManager) {
241
254
  this.#enqueueChartTask(chartZipPath, () =>
242
- this.updateChartAsync(chartZipPath, data, relationshipManager)
255
+ this.updateChartAsync(
256
+ chartZipPath,
257
+ data,
258
+ relationshipManager,
259
+ slideIndex,
260
+ chartId,
261
+ slideManager
262
+ )
243
263
  )
244
264
  }
245
265
 
@@ -251,7 +271,14 @@ class ChartManager {
251
271
  * @param {RelationshipManager} relationshipManager
252
272
  * @returns {Promise<void>}
253
273
  */
254
- async updateChartAsync(chartZipPath, data, relationshipManager) {
274
+ async updateChartAsync(
275
+ chartZipPath,
276
+ data,
277
+ relationshipManager,
278
+ slideIndex,
279
+ chartId,
280
+ slideManager
281
+ ) {
255
282
  // 1. Read Chart XML
256
283
  const xml = await this.#zipManager.readFile(chartZipPath)
257
284
  if (!xml) throw new ChartNotFoundError(`Chart file not found: ${chartZipPath}`)
@@ -291,17 +318,40 @@ class ChartManager {
291
318
  }
292
319
  }
293
320
 
294
- // 5. Apply custom data labels if present
321
+ // 5. Apply custom data labels if present or if showSeriesNameInBar is enabled
295
322
  for (let i = 0; i < seriesLabels.length; i++) {
296
323
  const labels = seriesLabels[i]
297
- if (labels && labels.some(l => l !== undefined)) {
324
+ const ser = cleanNumericData.series[i]
325
+ const showSerName =
326
+ ser.showSeriesNameInBar !== undefined ? ser.showSeriesNameInBar : data.showSeriesNameInBar
327
+
328
+ if ((labels && labels.some(l => l !== undefined)) || showSerName) {
298
329
  const labelOptions = {
299
330
  series: i,
300
- labels: labels.map(l => (l === undefined ? '' : String(l))),
331
+ ...(labels ? { labels: labels.map(l => (l === undefined ? '' : String(l))) } : {}),
332
+ showSeriesNameInBar: !!showSerName,
301
333
  }
302
334
  await this.updateDataLabelsAsync(chartZipPath, labelOptions, relationshipManager)
303
335
  }
304
336
  }
337
+
338
+ // 6. Apply series name labels if enabled
339
+ if (
340
+ data.seriesNameLabels &&
341
+ slideIndex !== undefined &&
342
+ chartId !== undefined &&
343
+ slideManager !== undefined
344
+ ) {
345
+ const finalXml = await this.#zipManager.readFile(chartZipPath)
346
+ await this.#applySeriesNameLabels(
347
+ slideIndex,
348
+ chartId,
349
+ data,
350
+ finalXml,
351
+ slideManager,
352
+ relationshipManager
353
+ )
354
+ }
305
355
  }
306
356
 
307
357
  /**
@@ -801,6 +851,1380 @@ class ChartManager {
801
851
  labels: seriesLabels,
802
852
  }
803
853
  }
854
+
855
+ async validateChartLabels(slideIndex, chartId, options, slideManager, relationshipManager) {
856
+ const chartInfo = this.findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
857
+ if (!chartInfo) {
858
+ throw new ChartNotFoundError(`Chart "${chartId}" not found in slide ${slideIndex}`)
859
+ }
860
+ const queue = this.#chartQueues.get(chartInfo.zipPath) || Promise.resolve()
861
+ await queue
862
+ const xml = await this.#zipManager.readFile(chartInfo.zipPath)
863
+ if (!xml) throw new ChartNotFoundError(`Chart file not found: ${chartInfo.zipPath}`)
864
+
865
+ const { ValidationEngine } = require('../core/ValidationEngine')
866
+ return ValidationEngine.validateChartLabels(xml, options)
867
+ }
868
+
869
+ async validateSeriesNameLabels(slideIndex, chartId, options, slideManager, relationshipManager) {
870
+ const chartInfo = this.findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
871
+ if (!chartInfo) {
872
+ throw new ChartNotFoundError(`Chart "${chartId}" not found in slide ${slideIndex}`)
873
+ }
874
+ const queue = this.#chartQueues.get(chartInfo.zipPath) || Promise.resolve()
875
+ await queue
876
+ const xml = await this.#zipManager.readFile(chartInfo.zipPath)
877
+ if (!xml) throw new ChartNotFoundError(`Chart file not found: ${chartInfo.zipPath}`)
878
+
879
+ const slideInfo = slideManager.getSlideInfo(slideIndex)
880
+ const slideXml = await this.#zipManager.readFile(slideInfo.zipPath)
881
+ const xfrm = this.#findChartCoordinates(
882
+ slideXml,
883
+ chartId,
884
+ relationshipManager,
885
+ slideInfo.zipPath
886
+ )
887
+
888
+ const { ValidationEngine } = require('../core/ValidationEngine')
889
+ return ValidationEngine.validateSeriesNameLabels(xml, xfrm, options)
890
+ }
891
+
892
+ #findChartCoordinates(slideXml, chartId, relationshipManager, slideZipPath) {
893
+ const gfPattern = /<p:graphicFrame>([\s\S]*?)<\/p:graphicFrame>/g
894
+ let match
895
+ const escapedChartId = chartId.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
896
+ while ((match = gfPattern.exec(slideXml)) !== null) {
897
+ const gfContent = match[0]
898
+ const nameMatch = /<p:cNvPr[^>]*name="([^"]+)"/.exec(gfContent)
899
+ const idMatch = /<p:cNvPr[^>]*id="([^"]+)"/.exec(gfContent)
900
+
901
+ let isMatch = false
902
+ if (nameMatch && nameMatch[1] === chartId) {
903
+ isMatch = true
904
+ } else if (idMatch && idMatch[1] === chartId) {
905
+ isMatch = true
906
+ } else {
907
+ const chartRIdMatch = /<c:chart[^>]*r:id="([^"]+)"/.exec(gfContent)
908
+ if (chartRIdMatch) {
909
+ const rId = chartRIdMatch[1]
910
+ const rel = relationshipManager.getRelationshipById(slideZipPath, rId)
911
+ if (rel) {
912
+ const chartPath = relationshipManager.resolveTarget(slideZipPath, rel.target)
913
+ if (chartPath.includes(chartId) || rel.id === chartId) {
914
+ isMatch = true
915
+ }
916
+ }
917
+ }
918
+ }
919
+
920
+ if (isMatch) {
921
+ const xfrmMatch = /<p:xfrm[^>]*>([\s\S]*?)<\/p:xfrm>/.exec(gfContent)
922
+ if (xfrmMatch) {
923
+ const offMatch = /<a:off\s+x="(\d+)"\s+y="(\d+)"\/>/.exec(xfrmMatch[1])
924
+ const extMatch = /<a:ext\s+cx="(\d+)"\s+cy="(\d+)"\/>/.exec(xfrmMatch[1])
925
+ if (offMatch && extMatch) {
926
+ return {
927
+ left: parseInt(offMatch[1], 10),
928
+ top: parseInt(offMatch[2], 10),
929
+ width: parseInt(extMatch[1], 10),
930
+ height: parseInt(extMatch[2], 10),
931
+ }
932
+ }
933
+ }
934
+ }
935
+ }
936
+ return null
937
+ }
938
+
939
+ #parsePlotAreaLayout(chartXml) {
940
+ const plotAreaMatch = /<c:plotArea>([\s\S]*?)<\/c:plotArea>/.exec(chartXml)
941
+ if (plotAreaMatch) {
942
+ const layoutMatch = /<c:layout>([\s\S]*?)<\/c:layout>/.exec(plotAreaMatch[1])
943
+ if (layoutMatch) {
944
+ const xMatch = /<c:x\s+val="([^"]+)"\/>/.exec(layoutMatch[1])
945
+ const yMatch = /<c:y\s+val="([^"]+)"\/>/.exec(layoutMatch[1])
946
+ const wMatch = /<c:w\s+val="([^"]+)"\/>/.exec(layoutMatch[1])
947
+ const hMatch = /<c:h\s+val="([^"]+)"\/>/.exec(layoutMatch[1])
948
+
949
+ return {
950
+ x: xMatch ? parseFloat(xMatch[1]) : 0.1,
951
+ y: yMatch ? parseFloat(yMatch[1]) : 0.1,
952
+ w: wMatch ? parseFloat(wMatch[1]) : 0.8,
953
+ h: hMatch ? parseFloat(hMatch[1]) : 0.8,
954
+ }
955
+ }
956
+ }
957
+ return { x: 0.1, y: 0.1, w: 0.8, h: 0.8 }
958
+ }
959
+
960
+ #parseChartTypeAndGrouping(chartXml) {
961
+ let dir = 'col'
962
+ let grouping = 'clustered'
963
+ let chartType = 'unknown'
964
+
965
+ if (chartXml.includes('<c:barChart>') || chartXml.includes('c:barChart')) {
966
+ chartType = 'bar'
967
+ const barDirMatch = /<c:barDir\s+val="([^"]+)"\/>/.exec(chartXml)
968
+ if (barDirMatch) dir = barDirMatch[1]
969
+
970
+ const groupingMatch = /<c:grouping\s+val="([^"]+)"\/>/.exec(chartXml)
971
+ if (groupingMatch) grouping = groupingMatch[1]
972
+ }
973
+
974
+ return { chartType, dir, grouping }
975
+ }
976
+
977
+ #resolveAxisMax(grouping, seriesValues, categoriesCount, seriesCount, chartXml) {
978
+ const maxMatch = /<c:max\s+val="([^"]+)"\/>/.exec(chartXml)
979
+ if (maxMatch) {
980
+ return parseFloat(maxMatch[1])
981
+ }
982
+
983
+ if (grouping === 'stacked' || grouping === 'percentStacked') {
984
+ const sums = []
985
+ for (let c = 0; c < categoriesCount; c++) {
986
+ let sum = 0
987
+ for (let s = 0; s < seriesCount; s++) {
988
+ sum += Math.abs(Number(seriesValues[s]?.[c]) || 0)
989
+ }
990
+ sums.push(sum)
991
+ }
992
+ const maxSum = Math.max(...sums, 1)
993
+ return this.#getAxisMax(maxSum)
994
+ } else {
995
+ let maxVal = 1
996
+ for (let s = 0; s < seriesCount; s++) {
997
+ for (let c = 0; c < categoriesCount; c++) {
998
+ maxVal = Math.max(maxVal, Math.abs(Number(seriesValues[s]?.[c]) || 0))
999
+ }
1000
+ }
1001
+ return this.#getAxisMax(maxVal)
1002
+ }
1003
+ }
1004
+
1005
+ #resolveSegmentsGeometry(geom) {
1006
+ const segments = []
1007
+ const fontSize = 10
1008
+ const emuPerPt = 12700
1009
+ const {
1010
+ plotLeft,
1011
+ plotTop,
1012
+ plotWidth,
1013
+ plotHeight,
1014
+ dir,
1015
+ grouping,
1016
+ categories,
1017
+ seriesCount,
1018
+ categoriesCount,
1019
+ seriesValues,
1020
+ seriesNames,
1021
+ axisMax,
1022
+ } = geom
1023
+
1024
+ for (let c = 0; c < categoriesCount; c++) {
1025
+ const categoryName = categories[c] !== undefined ? String(categories[c]) : `Category ${c + 1}`
1026
+
1027
+ if (dir === 'col') {
1028
+ const colWidth = plotWidth / Math.max(1, categoriesCount)
1029
+ const colLeftX = plotLeft + c * colWidth
1030
+ const colCenterX = colLeftX + colWidth / 2
1031
+
1032
+ if (grouping === 'stacked' || grouping === 'percentStacked') {
1033
+ let categorySum = 0
1034
+ for (let s = 0; s < seriesCount; s++) {
1035
+ categorySum += Math.abs(Number(seriesValues[s]?.[c]) || 0)
1036
+ }
1037
+ if (categorySum === 0) categorySum = 1
1038
+
1039
+ const barWidth = colWidth * 0.6
1040
+ const barLeft = colCenterX - barWidth / 2
1041
+
1042
+ let cumulative = 0
1043
+ for (let s = 0; s < seriesCount; s++) {
1044
+ const val = Number(seriesValues[s]?.[c]) || 0
1045
+ const absVal = Math.abs(val)
1046
+ const nextCumulative = cumulative + absVal
1047
+
1048
+ let segBottomY, segTopY
1049
+ if (grouping === 'percentStacked') {
1050
+ segBottomY = plotTop + plotHeight - plotHeight * (cumulative / categorySum)
1051
+ segTopY = plotTop + plotHeight - plotHeight * (nextCumulative / categorySum)
1052
+ } else {
1053
+ segBottomY = plotTop + plotHeight - plotHeight * (cumulative / axisMax)
1054
+ segTopY = plotTop + plotHeight - plotHeight * (nextCumulative / axisMax)
1055
+ }
1056
+
1057
+ const segHeight = Math.max(0, segBottomY - segTopY)
1058
+ const segCenterY = (segBottomY + segTopY) / 2
1059
+ const seriesName = seriesNames[s] || `Series ${s + 1}`
1060
+
1061
+ const labelText = String(val)
1062
+ const lblWidth = labelText.length * fontSize * 0.55 * emuPerPt
1063
+ const lblHeight = fontSize * 1.2 * emuPerPt
1064
+
1065
+ segments.push({
1066
+ series: seriesName,
1067
+ category: categoryName,
1068
+ seriesIndex: s,
1069
+ categoryIndex: c,
1070
+ value: val,
1071
+ bar: {
1072
+ x: Math.round(barLeft),
1073
+ y: Math.round(segTopY),
1074
+ width: Math.round(barWidth),
1075
+ height: Math.round(segHeight),
1076
+ },
1077
+ label: {
1078
+ x: Math.round(colCenterX - lblWidth / 2),
1079
+ y: Math.round(segCenterY - lblHeight / 2),
1080
+ width: Math.round(lblWidth),
1081
+ height: Math.round(lblHeight),
1082
+ },
1083
+ })
1084
+
1085
+ cumulative = nextCumulative
1086
+ }
1087
+ } else {
1088
+ const slotWidth = colWidth / Math.max(1, seriesCount)
1089
+ const barWidth = slotWidth * 0.8
1090
+
1091
+ for (let s = 0; s < seriesCount; s++) {
1092
+ const val = Number(seriesValues[s]?.[c]) || 0
1093
+ const slotLeftX = colLeftX + s * slotWidth
1094
+ const slotCenterX = slotLeftX + slotWidth / 2
1095
+ const barLeft = slotCenterX - barWidth / 2
1096
+
1097
+ const barHeight = plotHeight * (Math.abs(val) / axisMax)
1098
+ let barTopY, barBottomY
1099
+ if (val >= 0) {
1100
+ barTopY = plotTop + plotHeight - barHeight
1101
+ barBottomY = plotTop + plotHeight
1102
+ } else {
1103
+ barTopY = plotTop + plotHeight
1104
+ barBottomY = plotTop + plotHeight + barHeight
1105
+ }
1106
+
1107
+ const seriesName = seriesNames[s] || `Series ${s + 1}`
1108
+ const labelText = String(val)
1109
+ const lblWidth = labelText.length * fontSize * 0.55 * emuPerPt
1110
+ const lblHeight = fontSize * 1.2 * emuPerPt
1111
+ const segCenterY = (barTopY + barBottomY) / 2
1112
+
1113
+ segments.push({
1114
+ series: seriesName,
1115
+ category: categoryName,
1116
+ seriesIndex: s,
1117
+ categoryIndex: c,
1118
+ value: val,
1119
+ bar: {
1120
+ x: Math.round(barLeft),
1121
+ y: Math.round(barTopY),
1122
+ width: Math.round(barWidth),
1123
+ height: Math.round(barBottomY - barTopY),
1124
+ },
1125
+ label: {
1126
+ x: Math.round(slotCenterX - lblWidth / 2),
1127
+ y: Math.round(segCenterY - lblHeight / 2),
1128
+ width: Math.round(lblWidth),
1129
+ height: Math.round(lblHeight),
1130
+ },
1131
+ })
1132
+ }
1133
+ }
1134
+ } else {
1135
+ const rowHeight = plotHeight / Math.max(1, categoriesCount)
1136
+ const catTopY = plotTop + c * rowHeight
1137
+ const catCenterY = catTopY + rowHeight / 2
1138
+
1139
+ if (grouping === 'stacked' || grouping === 'percentStacked') {
1140
+ let categorySum = 0
1141
+ for (let s = 0; s < seriesCount; s++) {
1142
+ categorySum += Math.abs(Number(seriesValues[s]?.[c]) || 0)
1143
+ }
1144
+ if (categorySum === 0) categorySum = 1
1145
+
1146
+ const barHeight = rowHeight * 0.6
1147
+ const barTop = catCenterY - barHeight / 2
1148
+
1149
+ let cumulative = 0
1150
+ for (let s = 0; s < seriesCount; s++) {
1151
+ const val = Number(seriesValues[s]?.[c]) || 0
1152
+ const absVal = Math.abs(val)
1153
+ const nextCumulative = cumulative + absVal
1154
+
1155
+ let segLeftX, segRightX
1156
+ if (grouping === 'percentStacked') {
1157
+ segLeftX = plotLeft + plotWidth * (cumulative / categorySum)
1158
+ segRightX = plotLeft + plotWidth * (nextCumulative / categorySum)
1159
+ } else {
1160
+ segLeftX = plotLeft + plotWidth * (cumulative / axisMax)
1161
+ segRightX = plotLeft + plotWidth * (nextCumulative / axisMax)
1162
+ }
1163
+
1164
+ const segWidth = Math.max(0, segRightX - segLeftX)
1165
+ const segCenterX = (segLeftX + segRightX) / 2
1166
+ const seriesName = seriesNames[s] || `Series ${s + 1}`
1167
+
1168
+ const labelText = String(val)
1169
+ const lblWidth = labelText.length * fontSize * 0.55 * emuPerPt
1170
+ const lblHeight = fontSize * 1.2 * emuPerPt
1171
+
1172
+ segments.push({
1173
+ series: seriesName,
1174
+ category: categoryName,
1175
+ seriesIndex: s,
1176
+ categoryIndex: c,
1177
+ value: val,
1178
+ bar: {
1179
+ x: Math.round(segLeftX),
1180
+ y: Math.round(barTop),
1181
+ width: Math.round(segWidth),
1182
+ height: Math.round(barHeight),
1183
+ },
1184
+ label: {
1185
+ x: Math.round(segCenterX - lblWidth / 2),
1186
+ y: Math.round(catCenterY - lblHeight / 2),
1187
+ width: Math.round(lblWidth),
1188
+ height: Math.round(lblHeight),
1189
+ },
1190
+ })
1191
+
1192
+ cumulative = nextCumulative
1193
+ }
1194
+ } else {
1195
+ const slotHeight = rowHeight / Math.max(1, seriesCount)
1196
+ const barHeight = slotHeight * 0.8
1197
+
1198
+ for (let s = 0; s < seriesCount; s++) {
1199
+ const val = Number(seriesValues[s]?.[c]) || 0
1200
+ const slotTopY = catTopY + s * slotHeight
1201
+ const slotCenterY = slotTopY + slotHeight / 2
1202
+ const barTop = slotCenterY - barHeight / 2
1203
+
1204
+ const barWidth = plotWidth * (Math.abs(val) / axisMax)
1205
+ let barLeftX, barRightX
1206
+ if (val >= 0) {
1207
+ barLeftX = plotLeft
1208
+ barRightX = plotLeft + barWidth
1209
+ } else {
1210
+ barLeftX = plotLeft - barWidth
1211
+ barRightX = plotLeft
1212
+ }
1213
+
1214
+ const seriesName = seriesNames[s] || `Series ${s + 1}`
1215
+ const labelText = String(val)
1216
+ const lblWidth = labelText.length * fontSize * 0.55 * emuPerPt
1217
+ const lblHeight = fontSize * 1.2 * emuPerPt
1218
+ const segCenterX = (barLeftX + barRightX) / 2
1219
+
1220
+ segments.push({
1221
+ series: seriesName,
1222
+ category: categoryName,
1223
+ seriesIndex: s,
1224
+ categoryIndex: c,
1225
+ value: val,
1226
+ bar: {
1227
+ x: Math.round(barLeftX),
1228
+ y: Math.round(barTop),
1229
+ width: Math.round(barRightX - barLeftX),
1230
+ height: Math.round(barHeight),
1231
+ },
1232
+ label: {
1233
+ x: Math.round(segCenterX - lblWidth / 2),
1234
+ y: Math.round(slotCenterY - lblHeight / 2),
1235
+ width: Math.round(lblWidth),
1236
+ height: Math.round(lblHeight),
1237
+ },
1238
+ })
1239
+ }
1240
+ }
1241
+ }
1242
+ }
1243
+
1244
+ return segments
1245
+ }
1246
+
1247
+ #resolveBarSegmentCoordinates(
1248
+ plotLeft,
1249
+ plotTop,
1250
+ plotWidth,
1251
+ plotHeight,
1252
+ dir,
1253
+ grouping,
1254
+ categoriesCount,
1255
+ seriesCount,
1256
+ seriesValues,
1257
+ axisMax
1258
+ ) {
1259
+ const categories = Array.from({ length: categoriesCount }, (_, i) => `Category ${i + 1}`)
1260
+ const seriesNames = Array.from({ length: seriesCount }, (_, i) => `Series ${i + 1}`)
1261
+ const geom = {
1262
+ plotLeft,
1263
+ plotTop,
1264
+ plotWidth,
1265
+ plotHeight,
1266
+ dir,
1267
+ grouping,
1268
+ categories,
1269
+ seriesCount,
1270
+ categoriesCount,
1271
+ seriesValues,
1272
+ seriesNames,
1273
+ axisMax,
1274
+ }
1275
+ const segments = this.#resolveSegmentsGeometry(geom)
1276
+ return segments.map(seg => ({
1277
+ seriesIndex: seg.seriesIndex,
1278
+ categoryIndex: seg.categoryIndex,
1279
+ x: seg.label.x + seg.label.width / 2,
1280
+ y: seg.label.y + seg.label.height / 2,
1281
+ }))
1282
+ }
1283
+
1284
+ #cleanRPrAttributes(attrs) {
1285
+ if (!attrs) return 'lang="en-US"'
1286
+
1287
+ const attrMap = {}
1288
+ const pattern = /([a-zA-Z0-9:]+)="([^"]*)"/g
1289
+ let match
1290
+ while ((match = pattern.exec(attrs)) !== null) {
1291
+ attrMap[match[1]] = match[2]
1292
+ }
1293
+
1294
+ delete attrMap['u']
1295
+ delete attrMap['strike']
1296
+ delete attrMap['kern']
1297
+ delete attrMap['baseline']
1298
+ delete attrMap['spc']
1299
+ delete attrMap['dirty']
1300
+ delete attrMap['smtClean']
1301
+
1302
+ if (attrMap['b'] === '0' || attrMap['b'] === 'false') delete attrMap['b']
1303
+ if (attrMap['i'] === '0' || attrMap['i'] === 'false') delete attrMap['i']
1304
+
1305
+ if (!attrMap['lang']) {
1306
+ attrMap['lang'] = 'en-US'
1307
+ }
1308
+
1309
+ return Object.entries(attrMap)
1310
+ .map(([k, v]) => `${k}="${v}"`)
1311
+ .join(' ')
1312
+ }
1313
+
1314
+ #resolveVerticalCollisions(labels, plotTop, plotBottom) {
1315
+ labels.sort((a, b) => a.targetY - b.targetY)
1316
+ const N = labels.length
1317
+ for (let iter = 0; iter < 100; iter++) {
1318
+ let moved = false
1319
+ for (let i = 0; i < N - 1; i++) {
1320
+ const curr = labels[i]
1321
+ const next = labels[i + 1]
1322
+
1323
+ const currBottom = curr.y + curr.height / 2
1324
+ const nextTop = next.y - next.height / 2
1325
+
1326
+ if (currBottom > nextTop) {
1327
+ const overlap = currBottom - nextTop
1328
+ curr.y -= overlap / 2
1329
+ next.y += overlap / 2
1330
+ moved = true
1331
+ }
1332
+ }
1333
+
1334
+ for (let i = 0; i < N; i++) {
1335
+ const lbl = labels[i]
1336
+ const halfH = lbl.height / 2
1337
+ if (lbl.y - halfH < plotTop) {
1338
+ lbl.y = plotTop + halfH
1339
+ }
1340
+ if (lbl.y + halfH > plotBottom) {
1341
+ lbl.y = plotBottom - halfH
1342
+ }
1343
+ }
1344
+
1345
+ if (!moved) break
1346
+ }
1347
+ }
1348
+
1349
+ #escapeXml(str) {
1350
+ return String(str)
1351
+ .replace(/&/g, '&amp;')
1352
+ .replace(/</g, '&lt;')
1353
+ .replace(/>/g, '&gt;')
1354
+ .replace(/"/g, '&quot;')
1355
+ .replace(/'/g, '&apos;')
1356
+ }
1357
+
1358
+ async #applySeriesNameLabels(
1359
+ slideIndex,
1360
+ chartId,
1361
+ data,
1362
+ chartXml,
1363
+ slideManager,
1364
+ relationshipManager
1365
+ ) {
1366
+ const options = data.seriesNameLabels
1367
+ const slideInfo = slideManager.getSlideInfo(slideIndex)
1368
+ const slideZipPath = slideInfo.zipPath
1369
+
1370
+ const queue = this.#slideQueues.get(slideZipPath) || Promise.resolve()
1371
+ const nextTask = queue.then(async () => {
1372
+ let slideXml = await this.#zipManager.readFile(slideZipPath)
1373
+
1374
+ const escapedChartId = chartId.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
1375
+ const shapePattern = new RegExp(
1376
+ `<p:sp>(?:(?!<p:sp>)[\\s\\S])*?name="SeriesNameLabel-${escapedChartId}-\\d+(?:-\\d+)?"(?:(?!<\\/p:sp>)[\\s\\S])*?<\\/p:sp>`,
1377
+ 'g'
1378
+ )
1379
+ slideXml = slideXml.replace(shapePattern, '')
1380
+
1381
+ if (!options.enabled) {
1382
+ this.#zipManager.writeFile(slideZipPath, slideXml)
1383
+ return
1384
+ }
1385
+
1386
+ const chartXfrm = this.#findChartCoordinates(
1387
+ slideXml,
1388
+ chartId,
1389
+ relationshipManager,
1390
+ slideZipPath
1391
+ )
1392
+ if (!chartXfrm) {
1393
+ logger.warn(`Could not find coordinates for chart "${chartId}" on slide ${slideIndex}`)
1394
+ this.#zipManager.writeFile(slideZipPath, slideXml)
1395
+ return
1396
+ }
1397
+
1398
+ const plotLayout = this.#parsePlotAreaLayout(chartXml)
1399
+ const { dir, grouping } = this.#parseChartTypeAndGrouping(chartXml)
1400
+
1401
+ const normalized = this.#normalizeChartData(data)
1402
+ const cleanNumericData = normalized.cleanData
1403
+
1404
+ const categoriesCount = cleanNumericData.categories ? cleanNumericData.categories.length : 1
1405
+ const seriesCount = cleanNumericData.series ? cleanNumericData.series.length : 0
1406
+ const seriesValues = cleanNumericData.series.map(s => s.values || [])
1407
+ const seriesNames = cleanNumericData.series.map(s => s.name || '')
1408
+
1409
+ const axisMax = this.#resolveAxisMax(
1410
+ grouping,
1411
+ seriesValues,
1412
+ categoriesCount,
1413
+ seriesCount,
1414
+ chartXml
1415
+ )
1416
+
1417
+ const txPrMatch = /<c:txPr>([\s\S]*?)<\/c:txPr>/.exec(chartXml)
1418
+ const existingTxPr = txPrMatch ? txPrMatch[1] : ''
1419
+ const { ChartCacheGenerator } = require('./charts/ChartCacheGenerator.js')
1420
+ const { bodyPr, pPrXml, rPrXml } = ChartCacheGenerator.extractTxPrParts(existingTxPr)
1421
+
1422
+ const szMatch = /sz="(\d+)"/.exec(rPrXml)
1423
+ let fontSize = szMatch ? parseInt(szMatch[1], 10) / 100 : 10
1424
+ if (options.style && options.style.fontSize !== undefined) {
1425
+ fontSize = Number(options.style.fontSize)
1426
+ }
1427
+
1428
+ const position = options.position || 'left'
1429
+ const autoFit = options.autoFit !== false
1430
+ const emuPerPt = 12700
1431
+ const charAspect = 0.55
1432
+ const spacing = 200000
1433
+
1434
+ const labelItems = []
1435
+ let maxLabelWidth = 0
1436
+
1437
+ for (let s = 0; s < seriesCount; s++) {
1438
+ const seriesName = seriesNames[s] || `Series ${s + 1}`
1439
+ const textWidth = seriesName.length * fontSize * charAspect * emuPerPt
1440
+ maxLabelWidth = Math.max(maxLabelWidth, textWidth)
1441
+ labelItems.push({
1442
+ seriesIndex: s,
1443
+ seriesName,
1444
+ textWidth,
1445
+ })
1446
+ }
1447
+
1448
+ const slideWidth = 12192000
1449
+ const fontEmuHeight = fontSize * 1.2 * emuPerPt
1450
+
1451
+ const labelsForCollision = labelItems.map(item => {
1452
+ let maxWidth = maxLabelWidth
1453
+ if (position === 'left') {
1454
+ maxWidth = chartXfrm.left - spacing
1455
+ } else {
1456
+ maxWidth = slideWidth - (chartXfrm.left + chartXfrm.width) - spacing
1457
+ }
1458
+ maxWidth = Math.max(100000, maxWidth)
1459
+
1460
+ let boxWidth = item.textWidth
1461
+ let boxHeight = fontEmuHeight
1462
+ if (autoFit) {
1463
+ if (boxWidth > maxWidth) {
1464
+ boxWidth = maxWidth
1465
+ const lines = Math.ceil(item.textWidth / maxWidth)
1466
+ boxHeight = lines * fontEmuHeight
1467
+ }
1468
+ } else {
1469
+ const standardWidth = 1371600
1470
+ boxWidth = Math.min(standardWidth, maxWidth)
1471
+ if (item.textWidth > boxWidth) {
1472
+ const lines = Math.ceil(item.textWidth / boxWidth)
1473
+ boxHeight = lines * fontEmuHeight
1474
+ }
1475
+ }
1476
+
1477
+ const segments = this.#resolveSegmentsGeometry({
1478
+ plotLeft: chartXfrm.left + chartXfrm.width * plotLayout.x,
1479
+ plotTop: chartXfrm.top + chartXfrm.height * plotLayout.y,
1480
+ plotWidth: chartXfrm.width * plotLayout.w,
1481
+ plotHeight: chartXfrm.height * plotLayout.h,
1482
+ dir,
1483
+ grouping,
1484
+ categories: cleanNumericData.categories,
1485
+ seriesCount,
1486
+ categoriesCount,
1487
+ seriesValues,
1488
+ seriesNames,
1489
+ axisMax,
1490
+ })
1491
+
1492
+ const matchedSegs = segments.filter(seg => seg.seriesIndex === item.seriesIndex)
1493
+ let sumY = 0
1494
+ matchedSegs.forEach(seg => {
1495
+ sumY += seg.bar.y + seg.bar.height / 2
1496
+ })
1497
+ const targetY =
1498
+ matchedSegs.length > 0 ? sumY / matchedSegs.length : chartXfrm.top + chartXfrm.height / 2
1499
+
1500
+ return {
1501
+ seriesIndex: item.seriesIndex,
1502
+ name: item.seriesName,
1503
+ targetY,
1504
+ y: targetY,
1505
+ width: boxWidth,
1506
+ height: boxHeight,
1507
+ }
1508
+ })
1509
+
1510
+ this.#resolveVerticalCollisions(
1511
+ labelsForCollision,
1512
+ chartXfrm.top,
1513
+ chartXfrm.top + chartXfrm.height
1514
+ )
1515
+
1516
+ const finalBodyPr =
1517
+ bodyPr ||
1518
+ '<a:bodyPr lIns="0" tIns="0" rIns="0" bIns="0" anchor="ctr"><a:spAutoFit/></a:bodyPr>'
1519
+ let finalPPrXml = pPrXml || '<a:pPr/>'
1520
+ let templateRPr =
1521
+ rPrXml ||
1522
+ `<a:rPr lang="en-US" sz="${Math.round(fontSize * 100)}"><a:solidFill><a:srgbClr val="000000"/></a:solidFill><a:latin typeface="Arial"/></a:rPr>`
1523
+
1524
+ if (!templateRPr || !templateRPr.trim().startsWith('<a:rPr')) {
1525
+ templateRPr = `<a:rPr lang="en-US" sz="${Math.round(fontSize * 100)}"><a:solidFill><a:srgbClr val="000000"/></a:solidFill><a:latin typeface="Arial"/></a:rPr>`
1526
+ }
1527
+ if (!finalPPrXml || !finalPPrXml.trim().startsWith('<a:pPr')) {
1528
+ finalPPrXml = '<a:pPr/>'
1529
+ }
1530
+
1531
+ let existingSolidFill = ''
1532
+ let existingLatin = ''
1533
+ let existingEa = ''
1534
+ let existingCs = ''
1535
+
1536
+ const solidFillMatch = /(<a:solidFill>[\s\S]*?<\/a:solidFill>|<a:solidFill[^>]*\/>)/.exec(
1537
+ templateRPr
1538
+ )
1539
+ if (solidFillMatch) {
1540
+ existingSolidFill = solidFillMatch[1]
1541
+ }
1542
+
1543
+ const latinMatch = /(<a:latin[^>]*\/>)/.exec(templateRPr)
1544
+ if (latinMatch) {
1545
+ existingLatin = latinMatch[1]
1546
+ }
1547
+
1548
+ const eaMatch = /(<a:ea[^>]*\/>)/.exec(templateRPr)
1549
+ if (eaMatch) {
1550
+ existingEa = eaMatch[1]
1551
+ }
1552
+
1553
+ const csMatch = /(<a:cs[^>]*\/>)/.exec(templateRPr)
1554
+ if (csMatch) {
1555
+ existingCs = csMatch[1]
1556
+ }
1557
+
1558
+ const rPrStartMatch = /^<a:rPr([^>]*)>/.exec(templateRPr)
1559
+ let rPrAttrs = rPrStartMatch ? rPrStartMatch[1] : ''
1560
+ if (rPrAttrs.endsWith('/')) {
1561
+ rPrAttrs = rPrAttrs.slice(0, -1)
1562
+ }
1563
+ rPrAttrs = rPrAttrs.trim()
1564
+
1565
+ const cleanedAttrs = this.#cleanRPrAttributes(rPrAttrs)
1566
+
1567
+ const attrMap = {}
1568
+ const pattern = /([a-zA-Z0-9:]+)="([^"]*)"/g
1569
+ let match
1570
+ while ((match = pattern.exec(cleanedAttrs)) !== null) {
1571
+ attrMap[match[1]] = match[2]
1572
+ }
1573
+
1574
+ attrMap['sz'] = String(Math.round(fontSize * 100))
1575
+ if (options.style) {
1576
+ if (options.style.bold !== undefined) {
1577
+ if (options.style.bold) attrMap['b'] = '1'
1578
+ else delete attrMap['b']
1579
+ }
1580
+ if (options.style.italic !== undefined) {
1581
+ if (options.style.italic) attrMap['i'] = '1'
1582
+ else delete attrMap['i']
1583
+ }
1584
+ if (options.style.color !== undefined) {
1585
+ const hexColor = String(options.style.color).replace('#', '').trim()
1586
+ existingSolidFill = `<a:solidFill><a:srgbClr val="${hexColor}"/></a:solidFill>`
1587
+ }
1588
+ if (options.style.fontFamily !== undefined) {
1589
+ const typeface = options.style.fontFamily
1590
+ existingLatin = `<a:latin typeface="${typeface}"/>`
1591
+ existingEa = `<a:ea typeface="${typeface}"/>`
1592
+ existingCs = `<a:cs typeface="${typeface}"/>`
1593
+ }
1594
+ }
1595
+
1596
+ if (
1597
+ existingSolidFill &&
1598
+ (existingSolidFill.includes('val="bg1"') || existingSolidFill.includes('val="bg2"'))
1599
+ ) {
1600
+ existingSolidFill = '<a:solidFill><a:schemeClr val="tx1"/></a:solidFill>'
1601
+ }
1602
+
1603
+ const finalAttrsStr = Object.entries(attrMap)
1604
+ .map(([k, v]) => `${k}="${v}"`)
1605
+ .join(' ')
1606
+
1607
+ let finalRPrXml = `<a:rPr ${finalAttrsStr}>`
1608
+ if (existingSolidFill) finalRPrXml += existingSolidFill
1609
+ if (existingLatin) finalRPrXml += existingLatin
1610
+ if (existingEa) finalRPrXml += existingEa
1611
+ if (existingCs) finalRPrXml += existingCs
1612
+ finalRPrXml += '</a:rPr>'
1613
+
1614
+ let alignVal = options.style ? options.style.align : undefined
1615
+ if (!alignVal) {
1616
+ alignVal = position === 'left' ? 'r' : 'l'
1617
+ } else {
1618
+ if (alignVal === 'left') alignVal = 'l'
1619
+ else if (alignVal === 'right') alignVal = 'r'
1620
+ else if (alignVal === 'center') alignVal = 'ctr'
1621
+ }
1622
+
1623
+ if (finalPPrXml.includes('algn=')) {
1624
+ finalPPrXml = finalPPrXml.replace(/algn="[^"]+"/, `algn="${alignVal}"`)
1625
+ } else if (finalPPrXml.includes('<a:pPr')) {
1626
+ finalPPrXml = finalPPrXml.replace('<a:pPr', `<a:pPr algn="${alignVal}"`)
1627
+ } else {
1628
+ finalPPrXml = `<a:pPr algn="${alignVal}"/>`
1629
+ }
1630
+
1631
+ const idMatchPattern = /id="(\d+)"/g
1632
+ let idMatch
1633
+ const existingIds = []
1634
+ while ((idMatch = idMatchPattern.exec(slideXml)) !== null) {
1635
+ existingIds.push(parseInt(idMatch[1], 10))
1636
+ }
1637
+
1638
+ let shapesXml = ''
1639
+ const { generateUniqueId } = require('../utils/idUtils.js')
1640
+ for (let i = 0; i < labelsForCollision.length; i++) {
1641
+ const lbl = labelsForCollision[i]
1642
+ const newId = generateUniqueId(existingIds)
1643
+ existingIds.push(newId)
1644
+
1645
+ let boxLeft = 0
1646
+ if (position === 'left') {
1647
+ boxLeft = chartXfrm.left - lbl.width - spacing
1648
+ } else {
1649
+ boxLeft = chartXfrm.left + chartXfrm.width + spacing
1650
+ }
1651
+ boxLeft = Math.max(0, boxLeft)
1652
+ const boxTop = lbl.y - lbl.height / 2
1653
+
1654
+ shapesXml += `<p:sp>
1655
+ <p:nvSpPr>
1656
+ <p:cNvPr id="${newId}" name="SeriesNameLabel-${chartId}-${lbl.seriesIndex}"/>
1657
+ <p:cNvSpPr txBox="1"/>
1658
+ <p:nvPr/>
1659
+ </p:nvSpPr>
1660
+ <p:spPr>
1661
+ <a:xfrm>
1662
+ <a:off x="${Math.round(boxLeft)}" y="${Math.round(boxTop)}"/>
1663
+ <a:ext cx="${Math.round(lbl.width)}" cy="${Math.round(lbl.height)}"/>
1664
+ </a:xfrm>
1665
+ <a:prstGeom prst="rect">
1666
+ <a:avLst/>
1667
+ </a:prstGeom>
1668
+ <a:noFill/>
1669
+ </p:spPr>
1670
+ <p:txBody>
1671
+ ${finalBodyPr}
1672
+ <a:lstStyle/>
1673
+ <a:p>
1674
+ ${finalPPrXml}
1675
+ <a:r>
1676
+ ${finalRPrXml}
1677
+ <a:t>${this.#escapeXml(lbl.name)}</a:t>
1678
+ </a:r>
1679
+ <a:endParaRPr lang="en-US"/>
1680
+ </a:p>
1681
+ </p:txBody>
1682
+ </p:sp>`
1683
+ }
1684
+
1685
+ const spTreeEnd = '</p:spTree>'
1686
+ if (slideXml.includes(spTreeEnd)) {
1687
+ slideXml = slideXml.replace(spTreeEnd, `${shapesXml}${spTreeEnd}`)
1688
+ }
1689
+
1690
+ this.#zipManager.writeFile(slideZipPath, slideXml)
1691
+ })
1692
+
1693
+ this.#slideQueues.set(slideZipPath, nextTask)
1694
+ return nextTask
1695
+ }
1696
+
1697
+ #getAxisMax(maxVal) {
1698
+ if (maxVal <= 0) return 1
1699
+ const padded = maxVal * 1.05
1700
+ const power = Math.floor(Math.log10(padded))
1701
+ const temp = padded / Math.pow(10, power)
1702
+ let niceMax
1703
+ if (temp <= 1.0) niceMax = 1.0
1704
+ else if (temp <= 1.2) niceMax = 1.2
1705
+ else if (temp <= 1.5) niceMax = 1.5
1706
+ else if (temp <= 2.0) niceMax = 2.0
1707
+ else if (temp <= 2.5) niceMax = 2.5
1708
+ else if (temp <= 3.0) niceMax = 3.0
1709
+ else if (temp <= 4.0) niceMax = 4.0
1710
+ else if (temp <= 5.0) niceMax = 5.0
1711
+ else if (temp <= 6.0) niceMax = 6.0
1712
+ else if (temp <= 8.0) niceMax = 8.0
1713
+ else niceMax = 10.0
1714
+ return niceMax * Math.pow(10, power)
1715
+ }
1716
+
1717
+ async #resolveChartGeometry(slideIndex, chartId, slideManager, relationshipManager) {
1718
+ const chartInfo = this.findChartInSlide(slideIndex, chartId, slideManager, relationshipManager)
1719
+ if (!chartInfo) {
1720
+ throw new Error(`Chart "${chartId}" not found in slide ${slideIndex}`)
1721
+ }
1722
+ const queue = this.#chartQueues.get(chartInfo.zipPath) || Promise.resolve()
1723
+ await queue
1724
+ const chartXml = await this.#zipManager.readFile(chartInfo.zipPath)
1725
+ if (!chartXml) {
1726
+ throw new Error(`Chart file not found: ${chartInfo.zipPath}`)
1727
+ }
1728
+
1729
+ const slideInfo = slideManager.getSlideInfo(slideIndex)
1730
+ const slideXml = await this.#zipManager.readFile(slideInfo.zipPath)
1731
+ const chartXfrm = this.#findChartCoordinates(
1732
+ slideXml,
1733
+ chartId,
1734
+ relationshipManager,
1735
+ slideInfo.zipPath
1736
+ )
1737
+ if (!chartXfrm) {
1738
+ throw new Error(`Coordinates not found for chart "${chartId}" on slide ${slideIndex}`)
1739
+ }
1740
+
1741
+ const plotLayout = this.#parsePlotAreaLayout(chartXml)
1742
+ const { dir, grouping } = this.#parseChartTypeAndGrouping(chartXml)
1743
+
1744
+ const chartData = this.#extractChartData(chartXml)
1745
+ const categories = chartData.categories
1746
+ const series = chartData.series
1747
+
1748
+ const categoriesCount = categories.length || 1
1749
+ const seriesCount = series.length
1750
+ const seriesValues = series.map(s => s.values || [])
1751
+ const seriesNames = series.map(s => s.name || '')
1752
+
1753
+ const axisMax = this.#resolveAxisMax(
1754
+ grouping,
1755
+ seriesValues,
1756
+ categoriesCount,
1757
+ seriesCount,
1758
+ chartXml
1759
+ )
1760
+
1761
+ const chartLeft = chartXfrm.left
1762
+ const chartTop = chartXfrm.top
1763
+ const chartWidth = chartXfrm.width
1764
+ const chartHeight = chartXfrm.height
1765
+
1766
+ const px = plotLayout.x
1767
+ const py = plotLayout.y
1768
+ const pw = plotLayout.w
1769
+ const ph = plotLayout.h
1770
+ const plotLeft = chartLeft + chartWidth * px
1771
+ const plotTop = chartTop + chartHeight * py
1772
+ const plotWidth = chartWidth * pw
1773
+ const plotHeight = chartHeight * ph
1774
+
1775
+ return {
1776
+ chartLeft,
1777
+ chartTop,
1778
+ chartWidth,
1779
+ chartHeight,
1780
+ plotLeft,
1781
+ plotTop,
1782
+ plotWidth,
1783
+ plotHeight,
1784
+ dir,
1785
+ grouping,
1786
+ categories,
1787
+ series,
1788
+ seriesCount,
1789
+ categoriesCount,
1790
+ seriesValues,
1791
+ seriesNames,
1792
+ axisMax,
1793
+ }
1794
+ }
1795
+
1796
+ async getChartLabelPositions(slideIndex, chartId, slideManager, relationshipManager) {
1797
+ const geom = await this.#resolveChartGeometry(
1798
+ slideIndex,
1799
+ chartId,
1800
+ slideManager,
1801
+ relationshipManager
1802
+ )
1803
+ const segments = this.#resolveSegmentsGeometry(geom)
1804
+ return segments.map(seg => ({
1805
+ series: seg.series,
1806
+ category: seg.category,
1807
+ seriesIndex: seg.seriesIndex,
1808
+ categoryIndex: seg.categoryIndex,
1809
+ value: seg.value,
1810
+ x: seg.label.x,
1811
+ y: seg.label.y,
1812
+ width: seg.label.width,
1813
+ height: seg.label.height,
1814
+ }))
1815
+ }
1816
+
1817
+ async getChartBarPositions(slideIndex, chartId, slideManager, relationshipManager) {
1818
+ const geom = await this.#resolveChartGeometry(
1819
+ slideIndex,
1820
+ chartId,
1821
+ slideManager,
1822
+ relationshipManager
1823
+ )
1824
+ const segments = this.#resolveSegmentsGeometry(geom)
1825
+ return segments.map(seg => ({
1826
+ series: seg.series,
1827
+ category: seg.category,
1828
+ seriesIndex: seg.seriesIndex,
1829
+ categoryIndex: seg.categoryIndex,
1830
+ value: seg.value,
1831
+ x: seg.bar.x,
1832
+ y: seg.bar.y,
1833
+ width: seg.bar.width,
1834
+ height: seg.bar.height,
1835
+ }))
1836
+ }
1837
+
1838
+ async addTextAtPosition(slideIndex, options, slideManager) {
1839
+ const { text, x, y, width = 1200000, height = 300000, style = {} } = options
1840
+ const slideInfo = slideManager.getSlideInfo(slideIndex)
1841
+ const slideZipPath = slideInfo.zipPath
1842
+
1843
+ const queue = this.#slideQueues.get(slideZipPath) || Promise.resolve()
1844
+ const nextTask = queue.then(async () => {
1845
+ let slideXml = await this.#zipManager.readFile(slideZipPath)
1846
+
1847
+ const fontSize = style.fontSize || 10
1848
+ const fontFamily = style.fontFamily || 'Arial'
1849
+ const alignVal = style.align === 'center' ? 'ctr' : style.align === 'right' ? 'r' : 'l'
1850
+
1851
+ let colorXml = '<a:solidFill><a:srgbClr val="000000"/></a:solidFill>'
1852
+ if (style.color) {
1853
+ const hexColor = String(style.color).replace('#', '').trim()
1854
+ colorXml = `<a:solidFill><a:srgbClr val="${hexColor}"/></a:solidFill>`
1855
+ }
1856
+
1857
+ const finalBodyPr =
1858
+ '<a:bodyPr lIns="0" tIns="0" rIns="0" bIns="0" anchor="ctr"><a:spAutoFit/></a:bodyPr>'
1859
+ const finalPPrXml = `<a:pPr algn="${alignVal}"/>`
1860
+
1861
+ let rPrAttrs = `lang="en-US" sz="${Math.round(fontSize * 100)}"`
1862
+ if (style.bold) rPrAttrs += ' b="1"'
1863
+ if (style.italic) rPrAttrs += ' i="1"'
1864
+
1865
+ const finalRPrXml = `<a:rPr ${rPrAttrs}>${colorXml}<a:latin typeface="${fontFamily}"/><a:ea typeface="${fontFamily}"/><a:cs typeface="${fontFamily}"/></a:rPr>`
1866
+
1867
+ const idMatchPattern = /id="(\d+)"/g
1868
+ let idMatch
1869
+ const existingIds = []
1870
+ while ((idMatch = idMatchPattern.exec(slideXml)) !== null) {
1871
+ existingIds.push(parseInt(idMatch[1], 10))
1872
+ }
1873
+ const { generateUniqueId } = require('../utils/idUtils.js')
1874
+ const newId = generateUniqueId(existingIds)
1875
+
1876
+ const shapeXml = `<p:sp>
1877
+ <p:nvSpPr>
1878
+ <p:cNvPr id="${newId}" name="TextBoxAtPosition-${newId}"/>
1879
+ <p:cNvSpPr txBox="1"/>
1880
+ <p:nvPr/>
1881
+ </p:nvSpPr>
1882
+ <p:spPr>
1883
+ <a:xfrm>
1884
+ <a:off x="${Math.round(x)}" y="${Math.round(y)}"/>
1885
+ <a:ext cx="${Math.round(width)}" cy="${Math.round(height)}"/>
1886
+ </a:xfrm>
1887
+ <a:prstGeom prst="rect">
1888
+ <a:avLst/>
1889
+ </a:prstGeom>
1890
+ <a:noFill/>
1891
+ </p:spPr>
1892
+ <p:txBody>
1893
+ ${finalBodyPr}
1894
+ <a:lstStyle/>
1895
+ <a:p>
1896
+ ${finalPPrXml}
1897
+ <a:r>
1898
+ ${finalRPrXml}
1899
+ <a:t>${this.#escapeXml(text)}</a:t>
1900
+ </a:r>
1901
+ <a:endParaRPr lang="en-US"/>
1902
+ </a:p>
1903
+ </p:txBody>
1904
+ </p:sp>`
1905
+
1906
+ const spTreeEnd = '</p:spTree>'
1907
+ if (slideXml.includes(spTreeEnd)) {
1908
+ slideXml = slideXml.replace(spTreeEnd, `${shapeXml}${spTreeEnd}`)
1909
+ }
1910
+
1911
+ this.#zipManager.writeFile(slideZipPath, slideXml)
1912
+ })
1913
+
1914
+ this.#slideQueues.set(slideZipPath, nextTask)
1915
+ return nextTask
1916
+ }
1917
+
1918
+ async addTextNearChartLabel(slideIndex, options, slideManager, relationshipManager) {
1919
+ const { chart, text, position = 'left', style = {} } = options
1920
+
1921
+ const allowedPositions = ['left', 'right']
1922
+ if (!allowedPositions.includes(position)) {
1923
+ throw new Error(`Invalid position "${position}". Only "left" and "right" are supported.`)
1924
+ }
1925
+
1926
+ const slideInfo = slideManager.getSlideInfo(slideIndex)
1927
+ const slideZipPath = slideInfo.zipPath
1928
+
1929
+ const queue = this.#slideQueues.get(slideZipPath) || Promise.resolve()
1930
+ const nextTask = queue.then(async () => {
1931
+ const labels = await this.getChartLabelPositions(
1932
+ slideIndex,
1933
+ chart,
1934
+ slideManager,
1935
+ relationshipManager
1936
+ )
1937
+
1938
+ let slideXml = await this.#zipManager.readFile(slideZipPath)
1939
+ const escapedChartId = chart.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
1940
+ const shapePattern = new RegExp(
1941
+ `<p:sp>(?:(?!<p:sp>)[\\s\\S])*?name="SeriesNameLabel-${escapedChartId}-\\d+"(?:(?!<\\/p:sp>)[\\s\\S])*?<\\/p:sp>`,
1942
+ 'g'
1943
+ )
1944
+ slideXml = slideXml.replace(shapePattern, '')
1945
+ this.#zipManager.writeFile(slideZipPath, slideXml)
1946
+
1947
+ const fontSize = style.fontSize || 10
1948
+ const emuPerPt = 12700
1949
+ const charAspect = 0.55
1950
+ const spacing = 200000
1951
+
1952
+ const labelItems = []
1953
+ let maxLabelWidth = 0
1954
+
1955
+ for (let i = 0; i < labels.length; i++) {
1956
+ const lbl = labels[i]
1957
+ let labelText = ''
1958
+ if (typeof text === 'function') {
1959
+ labelText = text({
1960
+ series: lbl.series,
1961
+ category: lbl.category,
1962
+ value: lbl.value,
1963
+ })
1964
+ } else {
1965
+ labelText = String(text)
1966
+ }
1967
+
1968
+ const textWidth = labelText.length * fontSize * charAspect * emuPerPt
1969
+ maxLabelWidth = Math.max(maxLabelWidth, textWidth)
1970
+ labelItems.push({
1971
+ seriesIndex: i,
1972
+ seriesName: lbl.series,
1973
+ categoryName: lbl.category,
1974
+ labelText,
1975
+ textWidth,
1976
+ lbl,
1977
+ })
1978
+ }
1979
+
1980
+ const chartXfrm = this.#findChartCoordinates(
1981
+ slideXml,
1982
+ chart,
1983
+ relationshipManager,
1984
+ slideZipPath
1985
+ )
1986
+
1987
+ const autoFit = style.autoFit !== false
1988
+ const slideWidth = 12192000
1989
+ const fontEmuHeight = fontSize * 1.2 * emuPerPt
1990
+
1991
+ const labelsForCollision = labelItems.map(item => {
1992
+ let maxWidth = maxLabelWidth
1993
+ if (chartXfrm) {
1994
+ if (position === 'left') {
1995
+ maxWidth = chartXfrm.left - spacing
1996
+ } else {
1997
+ maxWidth = slideWidth - (chartXfrm.left + chartXfrm.width) - spacing
1998
+ }
1999
+ }
2000
+ maxWidth = Math.max(100000, maxWidth)
2001
+
2002
+ let boxWidth = item.textWidth
2003
+ let boxHeight = fontEmuHeight
2004
+ if (autoFit) {
2005
+ if (boxWidth > maxWidth) {
2006
+ boxWidth = maxWidth
2007
+ const lines = Math.ceil(item.textWidth / maxWidth)
2008
+ boxHeight = lines * fontEmuHeight
2009
+ }
2010
+ } else {
2011
+ const standardWidth = 1371600
2012
+ boxWidth = Math.min(standardWidth, maxWidth)
2013
+ if (item.textWidth > boxWidth) {
2014
+ const lines = Math.ceil(item.textWidth / boxWidth)
2015
+ boxHeight = lines * fontEmuHeight
2016
+ }
2017
+ }
2018
+
2019
+ return {
2020
+ seriesIndex: item.seriesIndex,
2021
+ name: item.labelText,
2022
+ targetY: item.lbl.y + item.lbl.height / 2,
2023
+ y: item.lbl.y + item.lbl.height / 2,
2024
+ width: boxWidth,
2025
+ height: boxHeight,
2026
+ lbl: item.lbl,
2027
+ }
2028
+ })
2029
+
2030
+ if (chartXfrm) {
2031
+ this.#resolveVerticalCollisions(
2032
+ labelsForCollision,
2033
+ chartXfrm.top,
2034
+ chartXfrm.top + chartXfrm.height
2035
+ )
2036
+ }
2037
+
2038
+ const chartInfo = this.findChartInSlide(slideIndex, chart, slideManager, relationshipManager)
2039
+ const chartXml = await this.#zipManager.readFile(chartInfo.zipPath)
2040
+ const txPrMatch = /<c:txPr>([\s\S]*?)<\/c:txPr>/.exec(chartXml)
2041
+ const existingTxPr = txPrMatch ? txPrMatch[1] : ''
2042
+ const { ChartCacheGenerator } = require('./charts/ChartCacheGenerator.js')
2043
+ const { pPrXml, rPrXml } = ChartCacheGenerator.extractTxPrParts(existingTxPr)
2044
+
2045
+ const finalBodyPr =
2046
+ '<a:bodyPr lIns="0" tIns="0" rIns="0" bIns="0" anchor="ctr"><a:spAutoFit/></a:bodyPr>'
2047
+ let finalPPrXml = pPrXml || '<a:pPr/>'
2048
+ let templateRPr =
2049
+ rPrXml ||
2050
+ `<a:rPr lang="en-US" sz="${Math.round(fontSize * 100)}"><a:solidFill><a:srgbClr val="000000"/></a:solidFill><a:latin typeface="Arial"/></a:rPr>`
2051
+
2052
+ if (!templateRPr || !templateRPr.trim().startsWith('<a:rPr')) {
2053
+ templateRPr = `<a:rPr lang="en-US" sz="${Math.round(fontSize * 100)}"><a:solidFill><a:srgbClr val="000000"/></a:solidFill><a:latin typeface="Arial"/></a:rPr>`
2054
+ }
2055
+ if (!finalPPrXml || !finalPPrXml.trim().startsWith('<a:pPr')) {
2056
+ finalPPrXml = '<a:pPr/>'
2057
+ }
2058
+
2059
+ let existingSolidFill = ''
2060
+ let existingLatin = ''
2061
+ let existingEa = ''
2062
+ let existingCs = ''
2063
+
2064
+ const solidFillMatch = /(<a:solidFill>[\s\S]*?<\/a:solidFill>|<a:solidFill[^>]*\/>)/.exec(
2065
+ templateRPr
2066
+ )
2067
+ if (solidFillMatch) {
2068
+ existingSolidFill = solidFillMatch[1]
2069
+ }
2070
+
2071
+ const latinMatch = /(<a:latin[^>]*\/>)/.exec(templateRPr)
2072
+ if (latinMatch) {
2073
+ existingLatin = latinMatch[1]
2074
+ }
2075
+
2076
+ const eaMatch = /(<a:ea[^>]*\/>)/.exec(templateRPr)
2077
+ if (eaMatch) {
2078
+ existingEa = eaMatch[1]
2079
+ }
2080
+
2081
+ const csMatch = /(<a:cs[^>]*\/>)/.exec(templateRPr)
2082
+ if (csMatch) {
2083
+ existingCs = csMatch[1]
2084
+ }
2085
+
2086
+ const rPrStartMatch = /^<a:rPr([^>]*)>/.exec(templateRPr)
2087
+ let rPrAttrs = rPrStartMatch ? rPrStartMatch[1] : ''
2088
+ if (rPrAttrs.endsWith('/')) {
2089
+ rPrAttrs = rPrAttrs.slice(0, -1)
2090
+ }
2091
+ rPrAttrs = rPrAttrs.trim()
2092
+
2093
+ const cleanedAttrs = this.#cleanRPrAttributes(rPrAttrs)
2094
+
2095
+ const attrMap = {}
2096
+ const pattern = /([a-zA-Z0-9:]+)="([^"]*)"/g
2097
+ let match
2098
+ while ((match = pattern.exec(cleanedAttrs)) !== null) {
2099
+ attrMap[match[1]] = match[2]
2100
+ }
2101
+
2102
+ attrMap['sz'] = String(Math.round(fontSize * 100))
2103
+ if (style.bold !== undefined) {
2104
+ if (style.bold) attrMap['b'] = '1'
2105
+ else delete attrMap['b']
2106
+ }
2107
+ if (style.italic !== undefined) {
2108
+ if (style.italic) attrMap['i'] = '1'
2109
+ else delete attrMap['i']
2110
+ }
2111
+
2112
+ if (style.color !== undefined) {
2113
+ const hexColor = String(style.color).replace('#', '').trim()
2114
+ existingSolidFill = `<a:solidFill><a:srgbClr val="${hexColor}"/></a:solidFill>`
2115
+ } else {
2116
+ if (
2117
+ existingSolidFill &&
2118
+ (existingSolidFill.includes('val="bg1"') || existingSolidFill.includes('val="bg2"'))
2119
+ ) {
2120
+ existingSolidFill = '<a:solidFill><a:schemeClr val="tx1"/></a:solidFill>'
2121
+ }
2122
+ }
2123
+
2124
+ if (style.fontFamily !== undefined) {
2125
+ const typeface = style.fontFamily
2126
+ existingLatin = `<a:latin typeface="${typeface}"/>`
2127
+ existingEa = `<a:ea typeface="${typeface}"/>`
2128
+ existingCs = `<a:cs typeface="${typeface}"/>`
2129
+ }
2130
+
2131
+ const finalAttrsStr = Object.entries(attrMap)
2132
+ .map(([k, v]) => `${k}="${v}"`)
2133
+ .join(' ')
2134
+
2135
+ let finalRPrXml = `<a:rPr ${finalAttrsStr}>`
2136
+ if (existingSolidFill) finalRPrXml += existingSolidFill
2137
+ if (existingLatin) finalRPrXml += existingLatin
2138
+ if (existingEa) finalRPrXml += existingEa
2139
+ if (existingCs) finalRPrXml += existingCs
2140
+ finalRPrXml += '</a:rPr>'
2141
+
2142
+ let alignVal = style.align
2143
+ if (!alignVal) {
2144
+ alignVal = position === 'left' ? 'r' : 'l'
2145
+ } else {
2146
+ if (alignVal === 'left') alignVal = 'l'
2147
+ else if (alignVal === 'right') alignVal = 'r'
2148
+ else if (alignVal === 'center') alignVal = 'ctr'
2149
+ }
2150
+
2151
+ if (finalPPrXml.includes('algn=')) {
2152
+ finalPPrXml = finalPPrXml.replace(/algn="[^"]+"/, `algn="${alignVal}"`)
2153
+ } else if (finalPPrXml.includes('<a:pPr')) {
2154
+ finalPPrXml = finalPPrXml.replace('<a:pPr', `<a:pPr algn="${alignVal}"`)
2155
+ } else {
2156
+ finalPPrXml = `<a:pPr algn="${alignVal}"/>`
2157
+ }
2158
+
2159
+ slideXml = await this.#zipManager.readFile(slideZipPath)
2160
+ const idMatchPattern = /id="(\d+)"/g
2161
+ let idMatch
2162
+ const existingIds = []
2163
+ while ((idMatch = idMatchPattern.exec(slideXml)) !== null) {
2164
+ existingIds.push(parseInt(idMatch[1], 10))
2165
+ }
2166
+
2167
+ let shapesXml = ''
2168
+ const { generateUniqueId } = require('../utils/idUtils.js')
2169
+ for (let i = 0; i < labelsForCollision.length; i++) {
2170
+ const lbl = labelsForCollision[i]
2171
+ const newId = generateUniqueId(existingIds)
2172
+ existingIds.push(newId)
2173
+
2174
+ let boxLeft = 0
2175
+ if (chartXfrm) {
2176
+ if (position === 'left') {
2177
+ boxLeft = chartXfrm.left - lbl.width - spacing
2178
+ } else {
2179
+ boxLeft = chartXfrm.left + chartXfrm.width + spacing
2180
+ }
2181
+ } else {
2182
+ boxLeft = lbl.lbl.x - lbl.width - spacing
2183
+ }
2184
+ boxLeft = Math.max(0, boxLeft)
2185
+ const boxTop = lbl.y - lbl.height / 2
2186
+
2187
+ shapesXml += `<p:sp>
2188
+ <p:nvSpPr>
2189
+ <p:cNvPr id="${newId}" name="SeriesNameLabel-${chart}-${lbl.lbl.seriesIndex}"/>
2190
+ <p:cNvSpPr txBox="1"/>
2191
+ <p:nvPr/>
2192
+ </p:nvSpPr>
2193
+ <p:spPr>
2194
+ <a:xfrm>
2195
+ <a:off x="${Math.round(boxLeft)}" y="${Math.round(boxTop)}"/>
2196
+ <a:ext cx="${Math.round(lbl.width)}" cy="${Math.round(lbl.height)}"/>
2197
+ </a:xfrm>
2198
+ <a:prstGeom prst="rect">
2199
+ <a:avLst/>
2200
+ </a:prstGeom>
2201
+ <a:noFill/>
2202
+ </p:spPr>
2203
+ <p:txBody>
2204
+ ${finalBodyPr}
2205
+ <a:lstStyle/>
2206
+ <a:p>
2207
+ ${finalPPrXml}
2208
+ <a:r>
2209
+ ${finalRPrXml}
2210
+ <a:t>${this.#escapeXml(lbl.name)}</a:t>
2211
+ </a:r>
2212
+ <a:endParaRPr lang="en-US"/>
2213
+ </a:p>
2214
+ </p:txBody>
2215
+ </p:sp>`
2216
+ }
2217
+
2218
+ const spTreeEnd = '</p:spTree>'
2219
+ if (slideXml.includes(spTreeEnd)) {
2220
+ slideXml = slideXml.replace(spTreeEnd, `${shapesXml}${spTreeEnd}`)
2221
+ }
2222
+ this.#zipManager.writeFile(slideZipPath, slideXml)
2223
+ })
2224
+
2225
+ this.#slideQueues.set(slideZipPath, nextTask)
2226
+ return nextTask
2227
+ }
804
2228
  }
805
2229
 
806
2230
  module.exports = { ChartManager }