node-pptx-templater 1.0.10 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +192 -0
- package/package.json +2 -2
- package/src/core/PPTXTemplater.js +157 -0
- package/src/core/ValidationEngine.js +170 -0
- package/src/managers/ChartManager.js +1431 -7
- package/src/managers/ShapeManager.js +70 -0
- package/src/managers/charts/ChartCacheGenerator.js +92 -52
|
@@ -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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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, '&')
|
|
1352
|
+
.replace(/</g, '<')
|
|
1353
|
+
.replace(/>/g, '>')
|
|
1354
|
+
.replace(/"/g, '"')
|
|
1355
|
+
.replace(/'/g, ''')
|
|
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 }
|