@rowsncolumns/toolkit 24.4.5 → 24.5.0
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/dist/cjs/autofilter-condition.d.ts +45 -1
- package/dist/cjs/autofilter-condition.d.ts.map +1 -1
- package/dist/cjs/autofilter-condition.js +135 -12
- package/dist/cjs/excel-parser/helpers.d.ts +15 -1
- package/dist/cjs/excel-parser/helpers.d.ts.map +1 -1
- package/dist/cjs/excel-parser/helpers.js +119 -5
- package/dist/cjs/excel-parser/interface.d.ts +3 -1
- package/dist/cjs/excel-parser/interface.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/ods/index.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/ods/index.js +186 -97
- package/dist/cjs/excel-parser/plugins/ods/styles.d.ts +15 -0
- package/dist/cjs/excel-parser/plugins/ods/styles.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/ods/styles.js +80 -6
- package/dist/cjs/excel-parser/plugins/xlsb/index.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsb/index.js +107 -103
- package/dist/cjs/excel-parser/plugins/xlsx/charts.d.ts +77 -1
- package/dist/cjs/excel-parser/plugins/xlsx/charts.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/charts.js +920 -122
- package/dist/cjs/excel-parser/plugins/xlsx/drawing.d.ts +7 -1
- package/dist/cjs/excel-parser/plugins/xlsx/drawing.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/drawing.js +24 -9
- package/dist/cjs/excel-parser/plugins/xlsx/form-controls.d.ts +24 -3
- package/dist/cjs/excel-parser/plugins/xlsx/form-controls.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/form-controls.js +25 -3
- package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts +24 -1
- package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/index.js +561 -394
- package/dist/cjs/excel-parser/plugins/xlsx/pivots.d.ts +8 -2
- package/dist/cjs/excel-parser/plugins/xlsx/pivots.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/pivots.js +312 -21
- package/dist/cjs/excel-parser/plugins/xlsx/relationships.d.ts +6 -0
- package/dist/cjs/excel-parser/plugins/xlsx/relationships.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/relationships.js +9 -0
- package/dist/cjs/excel-parser/plugins/xlsx/theme.d.ts +1 -1
- package/dist/cjs/excel-parser/types.d.ts +9 -1
- package/dist/cjs/excel-parser/types.d.ts.map +1 -1
- package/dist/cjs/excel-parser/workbook.d.ts +1 -0
- package/dist/cjs/excel-parser/workbook.d.ts.map +1 -1
- package/dist/cjs/excel-parser/workbook.js +5 -0
- package/dist/cjs/excel-parser/worker.js +2 -0
- package/dist/cjs/exporter/ods/ods-export.d.ts.map +1 -1
- package/dist/cjs/exporter/ods/ods-export.js +255 -80
- package/dist/cjs/exporter/xlsx/chart-builder.d.ts +7 -1
- package/dist/cjs/exporter/xlsx/chart-builder.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/chart-builder.js +902 -119
- package/dist/cjs/exporter/xlsx/form-controls-builder.d.ts +16 -3
- package/dist/cjs/exporter/xlsx/form-controls-builder.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/form-controls-builder.js +30 -8
- package/dist/cjs/exporter/xlsx/pivot-builder.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/pivot-builder.js +206 -162
- package/dist/cjs/exporter/xlsx/style-builder.d.ts +8 -2
- package/dist/cjs/exporter/xlsx/style-builder.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/style-builder.js +25 -0
- package/dist/cjs/exporter/xlsx/xlsx-export.d.ts +2 -2
- package/dist/cjs/exporter/xlsx/xlsx-export.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/xlsx-export.js +728 -452
- package/dist/esm/autofilter-condition.d.ts +45 -1
- package/dist/esm/autofilter-condition.d.ts.map +1 -1
- package/dist/esm/autofilter-condition.js +131 -11
- package/dist/esm/excel-parser/helpers.d.ts +15 -1
- package/dist/esm/excel-parser/helpers.d.ts.map +1 -1
- package/dist/esm/excel-parser/helpers.js +94 -4
- package/dist/esm/excel-parser/interface.d.ts +3 -1
- package/dist/esm/excel-parser/interface.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/ods/index.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/ods/index.js +105 -22
- package/dist/esm/excel-parser/plugins/ods/styles.d.ts +15 -0
- package/dist/esm/excel-parser/plugins/ods/styles.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/ods/styles.js +79 -5
- package/dist/esm/excel-parser/plugins/xlsb/index.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsb/index.js +15 -7
- package/dist/esm/excel-parser/plugins/xlsx/charts.d.ts +77 -1
- package/dist/esm/excel-parser/plugins/xlsx/charts.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/charts.js +708 -16
- package/dist/esm/excel-parser/plugins/xlsx/drawing.d.ts +7 -1
- package/dist/esm/excel-parser/plugins/xlsx/drawing.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/drawing.js +23 -0
- package/dist/esm/excel-parser/plugins/xlsx/form-controls.d.ts +24 -3
- package/dist/esm/excel-parser/plugins/xlsx/form-controls.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/form-controls.js +21 -3
- package/dist/esm/excel-parser/plugins/xlsx/index.d.ts +24 -1
- package/dist/esm/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/index.js +192 -54
- package/dist/esm/excel-parser/plugins/xlsx/pivots.d.ts +8 -2
- package/dist/esm/excel-parser/plugins/xlsx/pivots.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/pivots.js +220 -4
- package/dist/esm/excel-parser/plugins/xlsx/relationships.d.ts +6 -0
- package/dist/esm/excel-parser/plugins/xlsx/relationships.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/relationships.js +9 -0
- package/dist/esm/excel-parser/plugins/xlsx/theme.d.ts +1 -1
- package/dist/esm/excel-parser/types.d.ts +9 -1
- package/dist/esm/excel-parser/types.d.ts.map +1 -1
- package/dist/esm/excel-parser/workbook.d.ts +1 -0
- package/dist/esm/excel-parser/workbook.d.ts.map +1 -1
- package/dist/esm/excel-parser/workbook.js +4 -0
- package/dist/esm/excel-parser/worker.js +2 -0
- package/dist/esm/exporter/ods/ods-export.d.ts.map +1 -1
- package/dist/esm/exporter/ods/ods-export.js +238 -72
- package/dist/esm/exporter/xlsx/chart-builder.d.ts +7 -1
- package/dist/esm/exporter/xlsx/chart-builder.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/chart-builder.js +674 -58
- package/dist/esm/exporter/xlsx/form-controls-builder.d.ts +16 -3
- package/dist/esm/exporter/xlsx/form-controls-builder.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/form-controls-builder.js +29 -8
- package/dist/esm/exporter/xlsx/pivot-builder.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/pivot-builder.js +187 -139
- package/dist/esm/exporter/xlsx/style-builder.d.ts +8 -2
- package/dist/esm/exporter/xlsx/style-builder.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/style-builder.js +28 -0
- package/dist/esm/exporter/xlsx/xlsx-export.d.ts +2 -2
- package/dist/esm/exporter/xlsx/xlsx-export.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/xlsx-export.js +451 -197
- package/package.json +10 -5
|
@@ -11,6 +11,37 @@ const chartBuilder = new XMLBuilder({
|
|
|
11
11
|
});
|
|
12
12
|
const sanitizeXmlText = (value) => value ? value.replace(INVALID_XML_CHAR_REGEX, "") : value;
|
|
13
13
|
const cloneTemplate = (template) => JSON.parse(JSON.stringify(template));
|
|
14
|
+
/**
|
|
15
|
+
* Put `groups` where the template's `slotKey` chart group sat, in place, so
|
|
16
|
+
* every chart group keeps preceding `c:catAx` / `c:valAx` (CT_PlotArea:
|
|
17
|
+
* layout, chart groups, axes, dTable, spPr). Assigning a new key onto the
|
|
18
|
+
* plotArea object would append it after the axes.
|
|
19
|
+
*/
|
|
20
|
+
const replacePlotAreaChartGroups = (plotArea, groups, slotKey = "c:barChart") => {
|
|
21
|
+
const rebuilt = {};
|
|
22
|
+
let placed = false;
|
|
23
|
+
for (const [key, value] of Object.entries(plotArea)) {
|
|
24
|
+
if (key === slotKey) {
|
|
25
|
+
for (const [groupKey, group] of groups)
|
|
26
|
+
rebuilt[groupKey] = group;
|
|
27
|
+
placed = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (!placed && (key === "c:catAx" || key === "c:valAx")) {
|
|
31
|
+
for (const [groupKey, group] of groups)
|
|
32
|
+
rebuilt[groupKey] = group;
|
|
33
|
+
placed = true;
|
|
34
|
+
}
|
|
35
|
+
rebuilt[key] = value;
|
|
36
|
+
}
|
|
37
|
+
if (!placed) {
|
|
38
|
+
for (const [groupKey, group] of groups)
|
|
39
|
+
rebuilt[groupKey] = group;
|
|
40
|
+
}
|
|
41
|
+
for (const key of Object.keys(plotArea))
|
|
42
|
+
delete plotArea[key];
|
|
43
|
+
Object.assign(plotArea, rebuilt);
|
|
44
|
+
};
|
|
14
45
|
const removeProp = (obj, key) => {
|
|
15
46
|
delete obj[key];
|
|
16
47
|
};
|
|
@@ -764,10 +795,20 @@ const buildBarSeriesNode = (index, ref, seriesData, domainRef, sheetName, isFirs
|
|
|
764
795
|
/**
|
|
765
796
|
* Build a line series node.
|
|
766
797
|
*/
|
|
767
|
-
const buildLineSeriesNode = (index, ref, seriesData, domainRef, sheetName, smooth) => {
|
|
798
|
+
const buildLineSeriesNode = (index, ref, seriesData, domainRef, sheetName, smooth, showMarkers = false) => {
|
|
768
799
|
const seriesNode = cloneTemplate(line_series);
|
|
769
800
|
seriesNode["c:idx"] = { a$: { val: index.toString() } };
|
|
770
801
|
seriesNode["c:order"] = { a$: { val: index.toString() } };
|
|
802
|
+
// A series' own marker wins; otherwise "Line with Markers" leaves the
|
|
803
|
+
// symbol to Excel's automatic marker and a plain Line keeps the
|
|
804
|
+
// template's <c:symbol val="none"/> — same rule as the pure line branch.
|
|
805
|
+
const markerNode = buildMarkerNode(seriesData?.marker);
|
|
806
|
+
if (markerNode) {
|
|
807
|
+
seriesNode["c:marker"] = markerNode;
|
|
808
|
+
}
|
|
809
|
+
else if (showMarkers) {
|
|
810
|
+
removeProp(seriesNode, "c:marker");
|
|
811
|
+
}
|
|
771
812
|
const seriesColor = seriesData?.color;
|
|
772
813
|
// Line series carry the stroke fill on `c:spPr/a:ln`. A gradient on
|
|
773
814
|
// a line stroke is unusual (Excel renders a flat-color stroke even
|
|
@@ -891,8 +932,7 @@ const buildPieChart = (domainRef, valueRef, title, subtitle) => {
|
|
|
891
932
|
const plotArea = pieChart["c:plotArea"];
|
|
892
933
|
const donut = plotArea["c:doughnutChart"];
|
|
893
934
|
removeProp(donut, "c:holeSize");
|
|
894
|
-
plotArea["c:pieChart"]
|
|
895
|
-
removeProp(plotArea, "c:doughnutChart");
|
|
935
|
+
replacePlotAreaChartGroups(plotArea, [["c:pieChart", donut]], "c:doughnutChart");
|
|
896
936
|
const seriesNode = plotArea["c:pieChart"]["c:ser"];
|
|
897
937
|
if (domainRef) {
|
|
898
938
|
seriesNode["c:cat"]["c:strRef"]["c:f"] = domainRef;
|
|
@@ -944,8 +984,7 @@ const buildOfPieChart = (domainRef, valueRef, title, spec, subtitle) => {
|
|
|
944
984
|
: {}),
|
|
945
985
|
"c:serLines": {},
|
|
946
986
|
};
|
|
947
|
-
plotArea["c:ofPieChart"]
|
|
948
|
-
removeProp(plotArea, "c:doughnutChart");
|
|
987
|
+
replacePlotAreaChartGroups(plotArea, [["c:ofPieChart", ofPieNode]], "c:doughnutChart");
|
|
949
988
|
const seriesNode = ofPieNode["c:ser"];
|
|
950
989
|
if (domainRef) {
|
|
951
990
|
seriesNode["c:cat"]["c:strRef"]["c:f"] = domainRef;
|
|
@@ -977,8 +1016,7 @@ const buildRadarLikeChart = (chartElementKey, domainRef, seriesRefs, title, rada
|
|
|
977
1016
|
a$: { val: radarStyle ?? "standard" },
|
|
978
1017
|
};
|
|
979
1018
|
}
|
|
980
|
-
|
|
981
|
-
plotArea[chartElementKey] = containerNode;
|
|
1019
|
+
replacePlotAreaChartGroups(plotArea, [[chartElementKey, containerNode]]);
|
|
982
1020
|
const seriesContainer = containerNode["c:ser"];
|
|
983
1021
|
seriesRefs.forEach((ref, index) => {
|
|
984
1022
|
if (!ref)
|
|
@@ -997,6 +1035,68 @@ const buildRadarLikeChart = (chartElementKey, domainRef, seriesRefs, title, rada
|
|
|
997
1035
|
});
|
|
998
1036
|
return chartObj;
|
|
999
1037
|
};
|
|
1038
|
+
/**
|
|
1039
|
+
* Stock chart. The price series (high/low/close, or open/high/low/close)
|
|
1040
|
+
* go in `<c:stockChart>`; Excel's VHLC / VOHLC layouts carry the volume
|
|
1041
|
+
* as the first series in a sibling `<c:barChart>`, which the parser reads
|
|
1042
|
+
* back as the V-variant. Excel plots that volume on a secondary value
|
|
1043
|
+
* axis; here both groups share the primary axes. CT_StockChart order is
|
|
1044
|
+
* ser, hiLowLines, upDownBars, axId: every layout draws the high-low
|
|
1045
|
+
* lines, and an open (OHLC / VOHLC) adds Excel's up/down bars between
|
|
1046
|
+
* open and close — without them Excel renders bare, disconnected markers.
|
|
1047
|
+
*/
|
|
1048
|
+
const buildStockChart = (domainRef, seriesRefs, title, stockVariant, subtitle) => {
|
|
1049
|
+
const hasVolume = stockVariant === "VHLC" || stockVariant === "VOHLC";
|
|
1050
|
+
const hasOpen = stockVariant === "OHLC" || stockVariant === "VOHLC";
|
|
1051
|
+
const chartObj = cloneTemplate(column_json);
|
|
1052
|
+
applyChartTitle(chartObj, title, undefined, subtitle);
|
|
1053
|
+
const plotArea = chartObj["c:plotArea"];
|
|
1054
|
+
const axisIds = plotArea["c:barChart"]?.["c:axId"] ?? [];
|
|
1055
|
+
const buildSeriesNode = (ref, index) => ({
|
|
1056
|
+
"c:idx": { a$: { val: String(index) } },
|
|
1057
|
+
"c:order": { a$: { val: String(index) } },
|
|
1058
|
+
...(domainRef ? { "c:cat": { "c:strRef": { "c:f": domainRef } } } : {}),
|
|
1059
|
+
"c:val": { "c:numRef": { "c:f": ref } },
|
|
1060
|
+
});
|
|
1061
|
+
const volumeRef = hasVolume ? seriesRefs[0] : undefined;
|
|
1062
|
+
const priceRefs = hasVolume ? seriesRefs.slice(1) : seriesRefs;
|
|
1063
|
+
const priceOffset = hasVolume ? 1 : 0;
|
|
1064
|
+
const stockNode = {
|
|
1065
|
+
"c:ser": priceRefs.flatMap((ref, index) => ref ? [buildSeriesNode(ref, index + priceOffset)] : []),
|
|
1066
|
+
"c:hiLowLines": {},
|
|
1067
|
+
...(hasOpen
|
|
1068
|
+
? {
|
|
1069
|
+
"c:upDownBars": {
|
|
1070
|
+
"c:gapWidth": { a$: { val: "150" } },
|
|
1071
|
+
"c:upBars": {},
|
|
1072
|
+
"c:downBars": {},
|
|
1073
|
+
},
|
|
1074
|
+
}
|
|
1075
|
+
: {}),
|
|
1076
|
+
"c:axId": axisIds,
|
|
1077
|
+
};
|
|
1078
|
+
// Swap the template's bar group in place so the chart groups keep
|
|
1079
|
+
// preceding the axes (CT_PlotArea order).
|
|
1080
|
+
const rebuilt = {};
|
|
1081
|
+
for (const [key, value] of Object.entries(plotArea)) {
|
|
1082
|
+
if (key !== "c:barChart") {
|
|
1083
|
+
rebuilt[key] = value;
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
if (volumeRef) {
|
|
1087
|
+
rebuilt["c:barChart"] = {
|
|
1088
|
+
"c:barDir": { a$: { val: "col" } },
|
|
1089
|
+
"c:grouping": { a$: { val: "clustered" } },
|
|
1090
|
+
"c:varyColors": { a$: { val: "0" } },
|
|
1091
|
+
"c:ser": [buildSeriesNode(volumeRef, 0)],
|
|
1092
|
+
"c:axId": axisIds,
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
rebuilt["c:stockChart"] = stockNode;
|
|
1096
|
+
}
|
|
1097
|
+
chartObj["c:plotArea"] = rebuilt;
|
|
1098
|
+
return chartObj;
|
|
1099
|
+
};
|
|
1000
1100
|
const buildDoughnutChart = (domainRef, valueRef, title, subtitle) => {
|
|
1001
1101
|
// Doughnut uses the same donut_json template as pie but keeps the
|
|
1002
1102
|
// <c:doughnutChart> wrapper (and the holeSize, which we set later
|
|
@@ -1013,13 +1113,48 @@ const buildDoughnutChart = (domainRef, valueRef, title, subtitle) => {
|
|
|
1013
1113
|
}
|
|
1014
1114
|
return chartObj;
|
|
1015
1115
|
};
|
|
1016
|
-
|
|
1116
|
+
/**
|
|
1117
|
+
* Excel's Change Colors palette has no OOXML element of its own — the
|
|
1118
|
+
* colours it implies are written on each `<c:ser><c:spPr>`. Resolve
|
|
1119
|
+
* `spec.palette` onto every series that carries no fill of its own so the
|
|
1120
|
+
* file shows the palette in Excel and the parser reads the colours back.
|
|
1121
|
+
*/
|
|
1122
|
+
const resolvePaletteIntoSeries = (chart) => {
|
|
1123
|
+
const palette = chart.spec.palette;
|
|
1124
|
+
const series = chart.spec?.series;
|
|
1125
|
+
if (!palette?.length || !Array.isArray(series))
|
|
1126
|
+
return chart;
|
|
1127
|
+
// A pie / doughnut colours SLICES from the palette, not the series — a
|
|
1128
|
+
// series fill would paint every slice alike. `applySliceColors` writes
|
|
1129
|
+
// the palette per slice as `<c:dPt>` instead.
|
|
1130
|
+
if (isCircularChartType(chart.spec.chartType))
|
|
1131
|
+
return chart;
|
|
1132
|
+
return {
|
|
1133
|
+
...chart,
|
|
1134
|
+
spec: {
|
|
1135
|
+
...chart.spec,
|
|
1136
|
+
series: series.map((entry, index) => entry?.color || entry?.gradient || entry?.noFill
|
|
1137
|
+
? entry
|
|
1138
|
+
: { ...entry, color: palette[index % palette.length] }),
|
|
1139
|
+
},
|
|
1140
|
+
};
|
|
1141
|
+
};
|
|
1142
|
+
export const buildChartXml = (input, sheetNameById, resolvePivotSourceName) => {
|
|
1143
|
+
const chart = resolvePaletteIntoSeries(input);
|
|
1017
1144
|
const series = chart.spec?.series ?? [];
|
|
1018
1145
|
if (!series.length)
|
|
1019
1146
|
return undefined;
|
|
1020
1147
|
const domainRef = formatSourcesRef(chart.spec.domains?.[0]?.sources, sheetNameById);
|
|
1021
1148
|
const seriesRefs = series.map((entry) => formatSourcesRef(entry?.sources, sheetNameById));
|
|
1022
|
-
|
|
1149
|
+
// Excel keeps one category axis per axis group: a series on the
|
|
1150
|
+
// secondary axis takes the second chart domain (`domains[1]`) as its
|
|
1151
|
+
// `<c:cat>` when it carries no category range of its own.
|
|
1152
|
+
const secondaryDomainRef = formatSourcesRef(chart.spec.domains?.[1]?.sources, sheetNameById);
|
|
1153
|
+
const seriesCatRefs = series.map((entry) => seriesCategoryRef(entry, sheetNameById) ??
|
|
1154
|
+
(entry
|
|
1155
|
+
?.useSecondaryAxis && secondaryDomainRef
|
|
1156
|
+
? secondaryDomainRef
|
|
1157
|
+
: undefined));
|
|
1023
1158
|
if (!seriesRefs.some(Boolean))
|
|
1024
1159
|
return undefined;
|
|
1025
1160
|
const chartType = chart.spec.chartType;
|
|
@@ -1044,9 +1179,11 @@ export const buildChartXml = (chart, sheetNameById) => {
|
|
|
1044
1179
|
const baseBarChart = plotArea["c:barChart"];
|
|
1045
1180
|
const axisIds = baseBarChart?.["c:axId"] ?? [];
|
|
1046
1181
|
const dLbls = baseBarChart?.["c:dLbls"] ?? {};
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
//
|
|
1182
|
+
const comboStackedType = chart.spec.stackedType;
|
|
1183
|
+
const showMarkers = Boolean(chart.spec.showMarkers);
|
|
1184
|
+
// One container per chart type, swapped into the template's bar slot
|
|
1185
|
+
// together so they all precede the axes (CT_PlotArea order).
|
|
1186
|
+
const comboGroups = [];
|
|
1050
1187
|
for (const [groupType, groupSeries] of groups) {
|
|
1051
1188
|
if (groupType === "bar" || groupType === "column") {
|
|
1052
1189
|
const barChartNode = {
|
|
@@ -1062,21 +1199,32 @@ export const buildChartXml = (chart, sheetNameById) => {
|
|
|
1062
1199
|
const seriesNode = buildBarSeriesNode(index, ref, data, seriesCatRefs[index] ?? domainRef, sheetName, i === 0);
|
|
1063
1200
|
barChartNode["c:ser"].push(seriesNode);
|
|
1064
1201
|
}
|
|
1065
|
-
|
|
1202
|
+
comboGroups.push(["c:barChart", barChartNode]);
|
|
1066
1203
|
}
|
|
1067
1204
|
else if (groupType === "line") {
|
|
1205
|
+
// The line group honours the same grouping / marker rules as the
|
|
1206
|
+
// pure line branch (CT_LineChart: ser, dLbls, marker, axId).
|
|
1068
1207
|
const lineChartNode = {
|
|
1069
|
-
"c:grouping": {
|
|
1208
|
+
"c:grouping": {
|
|
1209
|
+
a$: {
|
|
1210
|
+
val: comboStackedType === "STACKED"
|
|
1211
|
+
? "stacked"
|
|
1212
|
+
: comboStackedType === "PERCENT_STACKED"
|
|
1213
|
+
? "percentStacked"
|
|
1214
|
+
: "standard",
|
|
1215
|
+
},
|
|
1216
|
+
},
|
|
1070
1217
|
"c:varyColors": { a$: { val: "0" } },
|
|
1071
1218
|
"c:ser": [],
|
|
1072
1219
|
"c:dLbls": dLbls,
|
|
1220
|
+
"c:marker": { a$: { val: "1" } },
|
|
1073
1221
|
"c:axId": axisIds,
|
|
1074
1222
|
};
|
|
1075
1223
|
for (const { index, ref, data } of groupSeries) {
|
|
1076
|
-
const seriesNode = buildLineSeriesNode(index, ref, data, seriesCatRefs[index] ?? domainRef, sheetName, smooth);
|
|
1224
|
+
const seriesNode = buildLineSeriesNode(index, ref, data, seriesCatRefs[index] ?? domainRef, sheetName, smooth, showMarkers);
|
|
1077
1225
|
lineChartNode["c:ser"].push(seriesNode);
|
|
1078
1226
|
}
|
|
1079
|
-
|
|
1227
|
+
comboGroups.push(["c:lineChart", lineChartNode]);
|
|
1080
1228
|
}
|
|
1081
1229
|
else if (groupType === "area") {
|
|
1082
1230
|
const areaChartNode = {
|
|
@@ -1090,15 +1238,16 @@ export const buildChartXml = (chart, sheetNameById) => {
|
|
|
1090
1238
|
const seriesNode = buildAreaSeriesNode(index, ref, data, seriesCatRefs[index] ?? domainRef, sheetName);
|
|
1091
1239
|
areaChartNode["c:ser"].push(seriesNode);
|
|
1092
1240
|
}
|
|
1093
|
-
|
|
1241
|
+
comboGroups.push(["c:areaChart", areaChartNode]);
|
|
1094
1242
|
}
|
|
1095
1243
|
}
|
|
1244
|
+
replacePlotAreaChartGroups(plotArea, comboGroups);
|
|
1245
|
+
// Legend, data labels, axis options, overlays and the secondary axis
|
|
1246
|
+
// group are applied by the shared formatting pass below, like every
|
|
1247
|
+
// other chart type.
|
|
1096
1248
|
chartSpace["c:chartSpace"]["c:chart"] = comboChart;
|
|
1097
|
-
const xml = chartBuilder.build(chartSpace);
|
|
1098
|
-
return `${XML_DECLARATION}${xml}`;
|
|
1099
1249
|
}
|
|
1100
|
-
|
|
1101
|
-
if (isC15ChartType(chartType)) {
|
|
1250
|
+
else if (isC15ChartType(chartType)) {
|
|
1102
1251
|
// Histogram / boxplot / waterfall / funnel emit as a barChart for
|
|
1103
1252
|
// older Excel compatibility. For histogram (Phase 5.5b), the
|
|
1104
1253
|
// barChart is wrapped in mc:AlternateContent with a c15:histogramChart
|
|
@@ -1169,8 +1318,9 @@ export const buildChartXml = (chart, sheetNameById) => {
|
|
|
1169
1318
|
c15Chart = buildC15FunnelChartNode(chart.spec, c15Args.seriesRefs, c15Args.series, c15Args.domainRef, c15Args.axIds);
|
|
1170
1319
|
}
|
|
1171
1320
|
const altContent = buildC15AlternateContent(chartType, c15Chart, bar);
|
|
1172
|
-
|
|
1173
|
-
|
|
1321
|
+
replacePlotAreaChartGroups(barChartDoc["c:plotArea"], [
|
|
1322
|
+
["mc:AlternateContent", altContent],
|
|
1323
|
+
]);
|
|
1174
1324
|
chartSpace["c:chartSpace"]["c:chart"] = barChartDoc;
|
|
1175
1325
|
}
|
|
1176
1326
|
else if (chartType === "bar" || chartType === "column") {
|
|
@@ -1248,7 +1398,7 @@ export const buildChartXml = (chart, sheetNameById) => {
|
|
|
1248
1398
|
chartSpace["c:chartSpace"]["c:chart"] = radarChart;
|
|
1249
1399
|
}
|
|
1250
1400
|
else if (chartType === "stock") {
|
|
1251
|
-
const stockChart =
|
|
1401
|
+
const stockChart = buildStockChart(domainRef, seriesRefs, chartTitle, chart.spec.stockVariant, chartSubtitle);
|
|
1252
1402
|
chartSpace["c:chartSpace"]["c:chart"] = stockChart;
|
|
1253
1403
|
}
|
|
1254
1404
|
else if (chartType === "line") {
|
|
@@ -1256,10 +1406,16 @@ export const buildChartXml = (chart, sheetNameById) => {
|
|
|
1256
1406
|
applyChartTitle(lineChart, chartTitle, chartTitleFmt, chartSubtitle);
|
|
1257
1407
|
const plotArea = lineChart["c:plotArea"];
|
|
1258
1408
|
const barChart = plotArea["c:barChart"];
|
|
1409
|
+
const lineStackedType = chart.spec.stackedType;
|
|
1410
|
+
const showMarkers = Boolean(chart.spec.showMarkers);
|
|
1259
1411
|
const lineChartNode = {
|
|
1260
1412
|
"c:grouping": {
|
|
1261
1413
|
a$: {
|
|
1262
|
-
val: "
|
|
1414
|
+
val: lineStackedType === "STACKED"
|
|
1415
|
+
? "stacked"
|
|
1416
|
+
: lineStackedType === "PERCENT_STACKED"
|
|
1417
|
+
? "percentStacked"
|
|
1418
|
+
: "standard",
|
|
1263
1419
|
},
|
|
1264
1420
|
},
|
|
1265
1421
|
"c:varyColors": {
|
|
@@ -1269,10 +1425,12 @@ export const buildChartXml = (chart, sheetNameById) => {
|
|
|
1269
1425
|
},
|
|
1270
1426
|
"c:ser": [],
|
|
1271
1427
|
"c:dLbls": barChart?.["c:dLbls"] ?? {},
|
|
1428
|
+
// Excel writes the chart-level flag on every line chart and hides
|
|
1429
|
+
// a plain Line's markers per series (<c:symbol val="none"/>).
|
|
1430
|
+
"c:marker": { a$: { val: "1" } },
|
|
1272
1431
|
"c:axId": barChart?.["c:axId"] ?? [],
|
|
1273
1432
|
};
|
|
1274
|
-
|
|
1275
|
-
plotArea["c:lineChart"] = lineChartNode;
|
|
1433
|
+
replacePlotAreaChartGroups(plotArea, [["c:lineChart", lineChartNode]]);
|
|
1276
1434
|
const seriesContainer = lineChartNode["c:ser"];
|
|
1277
1435
|
seriesRefs.forEach((ref, index) => {
|
|
1278
1436
|
if (!ref)
|
|
@@ -1301,6 +1459,16 @@ export const buildChartXml = (chart, sheetNameById) => {
|
|
|
1301
1459
|
if (smooth) {
|
|
1302
1460
|
seriesNode["c:smooth"].a$.val = "1";
|
|
1303
1461
|
}
|
|
1462
|
+
// A series' own marker wins; otherwise "Line with Markers" leaves
|
|
1463
|
+
// the symbol to Excel's automatic marker and a plain Line keeps the
|
|
1464
|
+
// template's <c:symbol val="none"/>.
|
|
1465
|
+
const markerNode = buildMarkerNode(series[index]?.marker);
|
|
1466
|
+
if (markerNode) {
|
|
1467
|
+
seriesNode["c:marker"] = markerNode;
|
|
1468
|
+
}
|
|
1469
|
+
else if (showMarkers) {
|
|
1470
|
+
removeProp(seriesNode, "c:marker");
|
|
1471
|
+
}
|
|
1304
1472
|
const labelNode = buildSeriesLabel(series[index]?.dataLabel ?? undefined, sheetNameById.get(chart.position.sheetId), series[index]?.dataLabelRef);
|
|
1305
1473
|
if (labelNode) {
|
|
1306
1474
|
seriesNode["c:tx"] = labelNode["c:tx"];
|
|
@@ -1329,8 +1497,7 @@ export const buildChartXml = (chart, sheetNameById) => {
|
|
|
1329
1497
|
"c:dLbls": barChart?.["c:dLbls"] ?? {},
|
|
1330
1498
|
"c:axId": barChart?.["c:axId"] ?? [],
|
|
1331
1499
|
};
|
|
1332
|
-
|
|
1333
|
-
plotArea["c:areaChart"] = areaChartNode;
|
|
1500
|
+
replacePlotAreaChartGroups(plotArea, [["c:areaChart", areaChartNode]]);
|
|
1334
1501
|
const seriesContainer = areaChartNode["c:ser"];
|
|
1335
1502
|
seriesRefs.forEach((ref, index) => {
|
|
1336
1503
|
if (!ref)
|
|
@@ -1426,12 +1593,11 @@ export const buildChartXml = (chart, sheetNameById) => {
|
|
|
1426
1593
|
const plotArea = chartDoc["c:plotArea"];
|
|
1427
1594
|
const scatterChart = plotArea["c:scatterChart"];
|
|
1428
1595
|
if (chartType === "bubble") {
|
|
1429
|
-
removeProp(plotArea, "c:scatterChart");
|
|
1430
1596
|
removeProp(scatterChart, "c:scatterStyle");
|
|
1431
1597
|
scatterChart["c:bubbleScale"] = { a$: { val: "100" } };
|
|
1432
1598
|
scatterChart["c:showNegBubbles"] = { a$: { val: "0" } };
|
|
1433
1599
|
scatterChart["c:sizeRepresents"] = { a$: { val: "area" } };
|
|
1434
|
-
plotArea["c:bubbleChart"]
|
|
1600
|
+
replacePlotAreaChartGroups(plotArea, [["c:bubbleChart", scatterChart]], "c:scatterChart");
|
|
1435
1601
|
}
|
|
1436
1602
|
else {
|
|
1437
1603
|
// <c:scatterStyle> — Excel default is "lineMarker". Round-trip
|
|
@@ -1477,9 +1643,40 @@ export const buildChartXml = (chart, sheetNameById) => {
|
|
|
1477
1643
|
if (chart.spec.roundedCorners) {
|
|
1478
1644
|
chartSpace["c:chartSpace"]["c:roundedCorners"] = { a$: { val: "1" } };
|
|
1479
1645
|
}
|
|
1646
|
+
// Chart Styles gallery pick — `<c:style val>` (1-48) with the Office
|
|
1647
|
+
// 2010+ `<c14:style>` twin at +100 in the mc:Choice the template
|
|
1648
|
+
// already carries. Unstyled charts keep the template's default (2).
|
|
1649
|
+
const chartStyle = Number(chart.spec.chartStyle);
|
|
1650
|
+
if (Number.isInteger(chartStyle) && chartStyle >= 1 && chartStyle <= 48) {
|
|
1651
|
+
const alternate = chartSpace["c:chartSpace"]["mc:AlternateContent"];
|
|
1652
|
+
alternate["mc:Choice"]["c14:style"].a$.val = String(100 + chartStyle);
|
|
1653
|
+
alternate["mc:Fallback"]["c:style"].a$.val = String(chartStyle);
|
|
1654
|
+
}
|
|
1655
|
+
applyPivotChartBinding(chartSpace, chart.spec, resolvePivotSourceName);
|
|
1656
|
+
// Format Chart Area → Fill / Border: the chartSpace `<c:spPr>`. The
|
|
1657
|
+
// template's theme fill (`bg1`) and hairline stay when nothing is
|
|
1658
|
+
// authored so untouched charts export exactly as before.
|
|
1659
|
+
const chartSpacePr = chartSpace["c:chartSpace"]["c:spPr"];
|
|
1660
|
+
const backgroundHex = toHex(chart.spec.backgroundColor);
|
|
1661
|
+
if (backgroundHex) {
|
|
1662
|
+
chartSpacePr["a:solidFill"] = { "a:srgbClr": { a$: { val: backgroundHex } } };
|
|
1663
|
+
}
|
|
1664
|
+
const borderHex = toHex(chart.borderColor);
|
|
1665
|
+
if (borderHex) {
|
|
1666
|
+
chartSpacePr["a:ln"]["a:solidFill"] = {
|
|
1667
|
+
"a:srgbClr": { a$: { val: borderHex } },
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1480
1670
|
const xml = chartBuilder.build(chartSpace);
|
|
1481
1671
|
return `${XML_DECLARATION}${xml}`;
|
|
1482
1672
|
};
|
|
1673
|
+
/** `#RRGGBB` / `RRGGBB` → uppercase `RRGGBB`; anything else is not emitted. */
|
|
1674
|
+
const toHex = (color) => {
|
|
1675
|
+
if (typeof color !== "string")
|
|
1676
|
+
return undefined;
|
|
1677
|
+
const hex = color.trim().replace(/^#/, "");
|
|
1678
|
+
return /^[0-9a-fA-F]{6}$/.test(hex) ? hex.toUpperCase() : undefined;
|
|
1679
|
+
};
|
|
1483
1680
|
// -------------------------- 3-D / surface --------------------------
|
|
1484
1681
|
/** Series-axis id for the 3-D chart groups that take a third axis. */
|
|
1485
1682
|
const SER_AX_ID = "1789284465";
|
|
@@ -1656,6 +1853,7 @@ const applyChartFormatting = (chartSpace, spec, sheetNameById) => {
|
|
|
1656
1853
|
if (!chartNode)
|
|
1657
1854
|
return;
|
|
1658
1855
|
applyLegend(chartNode, spec?.legend);
|
|
1856
|
+
applyEmptyCellsOptions(chartNode, spec?.emptyCells, spec?.showNaAsEmpty);
|
|
1659
1857
|
const plotArea = chartNode["c:plotArea"];
|
|
1660
1858
|
if (!plotArea)
|
|
1661
1859
|
return;
|
|
@@ -1663,8 +1861,10 @@ const applyChartFormatting = (chartSpace, spec, sheetNameById) => {
|
|
|
1663
1861
|
applyAxisOptions(plotArea, spec?.valueAxisOptions, "value");
|
|
1664
1862
|
applyAxisOptions(plotArea, spec?.categoryAxisOptions, "category");
|
|
1665
1863
|
applyDoughnutHole(plotArea, spec?.doughnutHoleSize);
|
|
1864
|
+
applyExplosion(plotArea, spec?.explosion);
|
|
1865
|
+
applySliceColors(plotArea, spec);
|
|
1666
1866
|
applySeriesOverlays(plotArea, spec?.series, sheetNameById);
|
|
1667
|
-
|
|
1867
|
+
applySecondaryAxisGroup(plotArea, spec, spec?.series ?? []);
|
|
1668
1868
|
};
|
|
1669
1869
|
const SERIES_CONTAINER_KEYS = [
|
|
1670
1870
|
"c:barChart",
|
|
@@ -1685,29 +1885,42 @@ const applySeriesOverlays = (plotArea, series, sheetNameById) => {
|
|
|
1685
1885
|
return;
|
|
1686
1886
|
let seriesIndex = 0;
|
|
1687
1887
|
for (const key of SERIES_CONTAINER_KEYS) {
|
|
1688
|
-
const
|
|
1689
|
-
if (!
|
|
1888
|
+
const containers = plotArea[key];
|
|
1889
|
+
if (!containers)
|
|
1690
1890
|
continue;
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
for (const
|
|
1696
|
-
const
|
|
1697
|
-
if (!
|
|
1891
|
+
// A split secondary axis group is an array of containers.
|
|
1892
|
+
const containerList = Array.isArray(containers)
|
|
1893
|
+
? containers
|
|
1894
|
+
: [containers];
|
|
1895
|
+
for (const container of containerList) {
|
|
1896
|
+
const ser = container?.["c:ser"];
|
|
1897
|
+
if (!ser)
|
|
1698
1898
|
continue;
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
//
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
if (
|
|
1710
|
-
|
|
1899
|
+
const list = Array.isArray(ser) ? ser : [ser];
|
|
1900
|
+
for (const node of list) {
|
|
1901
|
+
// A combo chart groups its series by chart type, so the emitted
|
|
1902
|
+
// order differs from spec.series — `<c:idx>` carries the spec
|
|
1903
|
+
// index; fall back to emission order for nodes without one.
|
|
1904
|
+
const idx = Number(node?.["c:idx"]?.a$?.val);
|
|
1905
|
+
const data = Number.isInteger(idx)
|
|
1906
|
+
? series[idx]
|
|
1907
|
+
: series[seriesIndex];
|
|
1908
|
+
seriesIndex++;
|
|
1909
|
+
if (!data)
|
|
1910
|
+
continue;
|
|
1911
|
+
if (Array.isArray(data.trendlines) && data.trendlines.length) {
|
|
1912
|
+
const built = data.trendlines
|
|
1913
|
+
.map(buildTrendlineNode)
|
|
1914
|
+
.filter(Boolean);
|
|
1915
|
+
// OOXML allows multiple <c:trendline> on a series; preserve the
|
|
1916
|
+
// order from the spec.
|
|
1917
|
+
node["c:trendline"] = built.length === 1 ? built[0] : built;
|
|
1918
|
+
}
|
|
1919
|
+
if (data.errorBars) {
|
|
1920
|
+
const built = buildErrorBarsNode(data.errorBars, sheetNameById);
|
|
1921
|
+
if (built)
|
|
1922
|
+
node["c:errBars"] = built;
|
|
1923
|
+
}
|
|
1711
1924
|
}
|
|
1712
1925
|
}
|
|
1713
1926
|
}
|
|
@@ -1798,12 +2011,297 @@ const applySecondaryAxis = (plotArea, opts) => {
|
|
|
1798
2011
|
"c:scaling": { "c:orientation": { a$: { val: "minMax" } } },
|
|
1799
2012
|
"c:delete": { a$: { val: "0" } },
|
|
1800
2013
|
"c:axPos": { a$: { val: "r" } },
|
|
2014
|
+
"c:numFmt": { a$: { formatCode: "General", sourceLinked: "1" } },
|
|
1801
2015
|
"c:crossAx": { a$: { val: String(primaryAxId) } },
|
|
1802
2016
|
};
|
|
1803
2017
|
// Apply the same logarithmic / reverse / min / max / gridlines etc.
|
|
1804
2018
|
applyAxisOptions({ "c:valAx": secondary }, opts, "value");
|
|
1805
2019
|
plotArea["c:valAx"] = [...existingArr, secondary];
|
|
1806
2020
|
};
|
|
2021
|
+
/** Category-axis chart groups a series can be plotted on a secondary axis in. */
|
|
2022
|
+
const CATEGORY_GROUP_KEYS = ["c:barChart", "c:lineChart", "c:areaChart"];
|
|
2023
|
+
/** `<c:axPos>` opposite the primary axis — where Excel draws the secondary one. */
|
|
2024
|
+
const OPPOSITE_AX_POS = { b: "t", t: "b", l: "r", r: "l" };
|
|
2025
|
+
const asNodeArray = (value) => value === undefined || value === null
|
|
2026
|
+
? []
|
|
2027
|
+
: Array.isArray(value)
|
|
2028
|
+
? value
|
|
2029
|
+
: [value];
|
|
2030
|
+
const axIdValueOf = (axis) => axis?.["c:axId"]?.a$?.val;
|
|
2031
|
+
/**
|
|
2032
|
+
* Excel plots "Plot Series On: Secondary Axis" series in their own chart
|
|
2033
|
+
* group bound to a second `<c:catAx>` / `<c:valAx>` pair (one category axis
|
|
2034
|
+
* and one value axis per axis group). Move every `useSecondaryAxis` series
|
|
2035
|
+
* out of its primary group into a clone of that group carrying the
|
|
2036
|
+
* secondary axIds (so a bar-primary + line-secondary combo keeps one bar
|
|
2037
|
+
* group and one line group), then add the secondary axis pair: the
|
|
2038
|
+
* category axis sits opposite the primary and is hidden (`<c:delete
|
|
2039
|
+
* val="1"/>`) when it shares the primary categories, shown when the spec
|
|
2040
|
+
* carries its own `domains[1]`; the value axis sits opposite the primary,
|
|
2041
|
+
* crosses at `max`, and takes `secondaryValueAxisOptions`.
|
|
2042
|
+
*
|
|
2043
|
+
* Charts with no secondary-axis series keep the old single-group layout
|
|
2044
|
+
* (a bare second `<c:valAx>` for `secondaryValueAxisOptions` alone), as do
|
|
2045
|
+
* the 3-D and c15 (histogram / boxplot / waterfall / funnel) bodies.
|
|
2046
|
+
*/
|
|
2047
|
+
const applySecondaryAxisGroup = (plotArea, spec, series) => {
|
|
2048
|
+
const opts = spec?.secondaryValueAxisOptions;
|
|
2049
|
+
const secondaryIdx = new Set();
|
|
2050
|
+
let plottedSeries = 0;
|
|
2051
|
+
(series ?? []).forEach((entry, index) => {
|
|
2052
|
+
if (!entry)
|
|
2053
|
+
return;
|
|
2054
|
+
plottedSeries += 1;
|
|
2055
|
+
if (entry.useSecondaryAxis)
|
|
2056
|
+
secondaryIdx.add(String(index));
|
|
2057
|
+
});
|
|
2058
|
+
const groupKeys = CATEGORY_GROUP_KEYS.filter((key) => plotArea?.[key]);
|
|
2059
|
+
const catAxes = asNodeArray(plotArea?.["c:catAx"]);
|
|
2060
|
+
const valAxes = asNodeArray(plotArea?.["c:valAx"]);
|
|
2061
|
+
if (!secondaryIdx.size ||
|
|
2062
|
+
secondaryIdx.size >= plottedSeries ||
|
|
2063
|
+
!groupKeys.length ||
|
|
2064
|
+
plotArea["mc:AlternateContent"] ||
|
|
2065
|
+
normalizeThreeDToken(spec?.sourceChartType) ||
|
|
2066
|
+
catAxes.length !== 1 ||
|
|
2067
|
+
valAxes.length !== 1) {
|
|
2068
|
+
applySecondaryAxis(plotArea, opts);
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
const primaryCat = catAxes[0];
|
|
2072
|
+
const primaryVal = valAxes[0];
|
|
2073
|
+
const catAxId = parseInt(axIdValueOf(primaryCat) ?? "47003664", 10);
|
|
2074
|
+
const valAxId = parseInt(axIdValueOf(primaryVal) ?? "1789284464", 10);
|
|
2075
|
+
const secondaryCatAxId = String(catAxId + 100);
|
|
2076
|
+
const secondaryValAxId = String(valAxId + 100);
|
|
2077
|
+
const secondaryAxIds = [
|
|
2078
|
+
{ a$: { val: secondaryCatAxId } },
|
|
2079
|
+
{ a$: { val: secondaryValAxId } },
|
|
2080
|
+
];
|
|
2081
|
+
let moved = false;
|
|
2082
|
+
for (const key of groupKeys) {
|
|
2083
|
+
const primaries = [];
|
|
2084
|
+
const secondaries = [];
|
|
2085
|
+
for (const container of asNodeArray(plotArea[key])) {
|
|
2086
|
+
const sers = asNodeArray(container["c:ser"]);
|
|
2087
|
+
const keep = sers.filter((node) => !secondaryIdx.has(String(node?.["c:idx"]?.a$?.val)));
|
|
2088
|
+
const move = sers.filter((node) => secondaryIdx.has(String(node?.["c:idx"]?.a$?.val)));
|
|
2089
|
+
if (!move.length) {
|
|
2090
|
+
primaries.push(container);
|
|
2091
|
+
continue;
|
|
2092
|
+
}
|
|
2093
|
+
moved = true;
|
|
2094
|
+
if (keep.length)
|
|
2095
|
+
primaries.push({ ...container, "c:ser": keep });
|
|
2096
|
+
// Same group shape (grouping, gapWidth, dLbls, …) — only the series
|
|
2097
|
+
// and the axis binding differ.
|
|
2098
|
+
const secondary = cloneTemplate(container);
|
|
2099
|
+
secondary["c:ser"] = move;
|
|
2100
|
+
secondary["c:axId"] = secondaryAxIds;
|
|
2101
|
+
secondaries.push(secondary);
|
|
2102
|
+
}
|
|
2103
|
+
const groups = [...primaries, ...secondaries];
|
|
2104
|
+
plotArea[key] = groups.length === 1 ? groups[0] : groups;
|
|
2105
|
+
}
|
|
2106
|
+
if (!moved) {
|
|
2107
|
+
applySecondaryAxis(plotArea, opts);
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2110
|
+
// Secondary category axis — opposite the primary; hidden unless the
|
|
2111
|
+
// secondary group has its own category list (domains[1]).
|
|
2112
|
+
const hasSecondaryDomain = Boolean(spec?.domains?.[1]?.sources?.length);
|
|
2113
|
+
const catAx2 = cloneTemplate(column_json["c:plotArea"]["c:catAx"]);
|
|
2114
|
+
catAx2["c:axId"] = { a$: { val: secondaryCatAxId } };
|
|
2115
|
+
catAx2["c:delete"] = { a$: { val: hasSecondaryDomain ? "0" : "1" } };
|
|
2116
|
+
catAx2["c:axPos"] = {
|
|
2117
|
+
a$: { val: OPPOSITE_AX_POS[primaryCat?.["c:axPos"]?.a$?.val] ?? "t" },
|
|
2118
|
+
};
|
|
2119
|
+
catAx2["c:crossAx"] = { a$: { val: secondaryValAxId } };
|
|
2120
|
+
catAx2["c:crosses"] = { a$: { val: "autoZero" } };
|
|
2121
|
+
// Secondary value axis — opposite the primary, crossing at max, no
|
|
2122
|
+
// gridlines (Excel's default for a secondary axis).
|
|
2123
|
+
const valAx2 = cloneTemplate(column_json["c:plotArea"]["c:valAx"]);
|
|
2124
|
+
valAx2["c:axId"] = { a$: { val: secondaryValAxId } };
|
|
2125
|
+
valAx2["c:delete"] = { a$: { val: "0" } };
|
|
2126
|
+
valAx2["c:axPos"] = {
|
|
2127
|
+
a$: { val: OPPOSITE_AX_POS[primaryVal?.["c:axPos"]?.a$?.val] ?? "r" },
|
|
2128
|
+
};
|
|
2129
|
+
removeProp(valAx2, "c:majorGridlines");
|
|
2130
|
+
valAx2["c:crossAx"] = { a$: { val: secondaryCatAxId } };
|
|
2131
|
+
valAx2["c:crosses"] = { a$: { val: "max" } };
|
|
2132
|
+
applyAxisOptions({ "c:valAx": valAx2 }, opts, "value");
|
|
2133
|
+
plotArea["c:catAx"] = [primaryCat, catAx2];
|
|
2134
|
+
plotArea["c:valAx"] = [primaryVal, valAx2];
|
|
2135
|
+
};
|
|
2136
|
+
const PIVOT_OPTIONS_EXT_URI = "{781A3756-C4B2-4CAC-9D66-4F8BD8637D16}";
|
|
2137
|
+
const C14_NS = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart";
|
|
2138
|
+
/**
|
|
2139
|
+
* PivotChart binding — `<c:pivotSource>` (name + fmtId) at the chartSpace
|
|
2140
|
+
* root so Excel keeps the chart on the pivot cache, `<c:pivotFmts>` inside
|
|
2141
|
+
* `<c:chart>` and the c14 `<c14:pivotOptions>` field-button choices in the
|
|
2142
|
+
* chartSpace `<c:extLst>`. All three only for a chart bound to an exported
|
|
2143
|
+
* pivot; a chart whose pivot is missing exports as a plain chart, which is
|
|
2144
|
+
* what Excel does when a PivotChart loses its pivot.
|
|
2145
|
+
*/
|
|
2146
|
+
const applyPivotChartBinding = (chartSpace, spec, resolvePivotSourceName) => {
|
|
2147
|
+
const pivotTableId = spec.pivotTableId;
|
|
2148
|
+
if (pivotTableId == null || !resolvePivotSourceName)
|
|
2149
|
+
return;
|
|
2150
|
+
const name = resolvePivotSourceName(pivotTableId);
|
|
2151
|
+
if (!name)
|
|
2152
|
+
return;
|
|
2153
|
+
const root = chartSpace["c:chartSpace"];
|
|
2154
|
+
setChildInOrder(root, "c:pivotSource", {
|
|
2155
|
+
"c:name": name,
|
|
2156
|
+
"c:fmtId": { a$: { val: String(spec.pivotSource?.formatId ?? 0) } },
|
|
2157
|
+
}, ["c:protection", "c:chart"]);
|
|
2158
|
+
const formats = spec.pivotFormats ?? [];
|
|
2159
|
+
if (formats.length) {
|
|
2160
|
+
setChildInOrder(root["c:chart"], "c:pivotFmts", {
|
|
2161
|
+
"c:pivotFmt": formats.map((format) => {
|
|
2162
|
+
const idx = { "c:idx": { a$: { val: String(format.index) } } };
|
|
2163
|
+
// A verbatim body (C-83) carries its own <c:spPr>; the fill-only
|
|
2164
|
+
// form is for formats authored in-app.
|
|
2165
|
+
if (format.body?.length) {
|
|
2166
|
+
return { ...idx, ...chartXmlElementsToBuilderObject(format.body) };
|
|
2167
|
+
}
|
|
2168
|
+
const hex = toHex(format.color);
|
|
2169
|
+
return {
|
|
2170
|
+
...idx,
|
|
2171
|
+
...(hex
|
|
2172
|
+
? { "c:spPr": { "a:solidFill": { "a:srgbClr": { a$: { val: hex } } } } }
|
|
2173
|
+
: {}),
|
|
2174
|
+
};
|
|
2175
|
+
}),
|
|
2176
|
+
}, ["c:view3D", "c:floor", "c:sideWall", "c:backWall", "c:plotArea"]);
|
|
2177
|
+
}
|
|
2178
|
+
const buttons = spec.pivotFieldButtons ?? {};
|
|
2179
|
+
const flag = (shown) => ({
|
|
2180
|
+
a$: { val: shown === false ? "0" : "1" },
|
|
2181
|
+
});
|
|
2182
|
+
const existing = root["c:extLst"]?.["c:ext"];
|
|
2183
|
+
const exts = Array.isArray(existing) ? existing : existing ? [existing] : [];
|
|
2184
|
+
root["c:extLst"] = {
|
|
2185
|
+
"c:ext": [
|
|
2186
|
+
...exts,
|
|
2187
|
+
{
|
|
2188
|
+
a$: { uri: PIVOT_OPTIONS_EXT_URI, "xmlns:c14": C14_NS },
|
|
2189
|
+
"c14:pivotOptions": {
|
|
2190
|
+
"c14:dropZoneFilter": flag(buttons.reportFilter),
|
|
2191
|
+
"c14:dropZoneCategories": flag(buttons.axis),
|
|
2192
|
+
"c14:dropZoneData": flag(buttons.value),
|
|
2193
|
+
"c14:dropZoneSeries": flag(buttons.legend),
|
|
2194
|
+
"c14:dropZonesVisible": flag(buttons.visible),
|
|
2195
|
+
},
|
|
2196
|
+
},
|
|
2197
|
+
],
|
|
2198
|
+
};
|
|
2199
|
+
};
|
|
2200
|
+
/** Prefixes the chartSpace root declares; any other prefix is declared on the element that uses it. */
|
|
2201
|
+
const ROOT_CHART_PREFIXES = new Set(["c", "a", "r"]);
|
|
2202
|
+
const CHART_XML_PREFIX_URIS = {
|
|
2203
|
+
mc: "http://schemas.openxmlformats.org/markup-compatibility/2006",
|
|
2204
|
+
c14: "http://schemas.microsoft.com/office/drawing/2007/8/2/chart",
|
|
2205
|
+
c15: "http://schemas.microsoft.com/office/drawing/2012/chart",
|
|
2206
|
+
c16: "http://schemas.microsoft.com/office/drawing/2014/chart",
|
|
2207
|
+
c16r2: "http://schemas.microsoft.com/office/drawing/2015/06/chart",
|
|
2208
|
+
c16r3: "http://schemas.microsoft.com/office/drawing/2017/03/chart",
|
|
2209
|
+
a14: "http://schemas.microsoft.com/office/drawing/2010/main",
|
|
2210
|
+
a16: "http://schemas.microsoft.com/office/drawing/2014/main",
|
|
2211
|
+
};
|
|
2212
|
+
/**
|
|
2213
|
+
* Verbatim `ChartXmlElement`s → the XMLBuilder object shape (`a$` attributes,
|
|
2214
|
+
* `t$` text), keys in document order; same-named siblings group into an array.
|
|
2215
|
+
* An extension prefix the root does not declare is declared on the first
|
|
2216
|
+
* element that uses it (`declared` tracks the ancestors' declarations).
|
|
2217
|
+
*/
|
|
2218
|
+
const chartXmlElementsToBuilderObject = (elements, declared = ROOT_CHART_PREFIXES) => {
|
|
2219
|
+
const out = {};
|
|
2220
|
+
for (const element of elements) {
|
|
2221
|
+
const prefix = element.tag.includes(":") ? element.tag.slice(0, element.tag.indexOf(":")) : "";
|
|
2222
|
+
const scope = new Set(declared);
|
|
2223
|
+
const attributes = { ...(element.attributes ?? {}) };
|
|
2224
|
+
if (prefix && !scope.has(prefix) && CHART_XML_PREFIX_URIS[prefix]) {
|
|
2225
|
+
attributes[`xmlns:${prefix}`] = CHART_XML_PREFIX_URIS[prefix];
|
|
2226
|
+
scope.add(prefix);
|
|
2227
|
+
}
|
|
2228
|
+
const node = {};
|
|
2229
|
+
if (Object.keys(attributes).length)
|
|
2230
|
+
node.a$ = attributes;
|
|
2231
|
+
if (element.children?.length) {
|
|
2232
|
+
Object.assign(node, chartXmlElementsToBuilderObject(element.children, scope));
|
|
2233
|
+
}
|
|
2234
|
+
else if (element.text != null) {
|
|
2235
|
+
node.t$ = element.text;
|
|
2236
|
+
}
|
|
2237
|
+
const existing = out[element.tag];
|
|
2238
|
+
if (existing === undefined)
|
|
2239
|
+
out[element.tag] = node;
|
|
2240
|
+
else if (Array.isArray(existing))
|
|
2241
|
+
existing.push(node);
|
|
2242
|
+
else
|
|
2243
|
+
out[element.tag] = [existing, node];
|
|
2244
|
+
}
|
|
2245
|
+
return out;
|
|
2246
|
+
};
|
|
2247
|
+
/** `emptyCells` → `<c:dispBlanksAs val>`. */
|
|
2248
|
+
const DISP_BLANKS_AS = {
|
|
2249
|
+
gaps: "gap",
|
|
2250
|
+
zero: "zero",
|
|
2251
|
+
connect: "span",
|
|
2252
|
+
};
|
|
2253
|
+
/**
|
|
2254
|
+
* Set `key` on `node` keeping the schema order: an existing key is
|
|
2255
|
+
* overwritten in place, a new one is inserted ahead of the first of
|
|
2256
|
+
* `beforeKeys` that is present (else appended).
|
|
2257
|
+
*/
|
|
2258
|
+
const setChildInOrder = (node, key, value, beforeKeys) => {
|
|
2259
|
+
if (key in node) {
|
|
2260
|
+
node[key] = value;
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
const entries = Object.entries(node);
|
|
2264
|
+
const at = entries.findIndex(([k]) => beforeKeys.includes(k));
|
|
2265
|
+
if (at === -1) {
|
|
2266
|
+
node[key] = value;
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
entries.splice(at, 0, [key, value]);
|
|
2270
|
+
for (const k of Object.keys(node))
|
|
2271
|
+
delete node[k];
|
|
2272
|
+
for (const [k, v] of entries)
|
|
2273
|
+
node[k] = v;
|
|
2274
|
+
};
|
|
2275
|
+
/**
|
|
2276
|
+
* Excel's "Hidden and Empty Cells" dialog: `<c:dispBlanksAs>` (gap /
|
|
2277
|
+
* zero / span) and the 2017 `<c16r3:dispNaAsBlank>` extension for "Show
|
|
2278
|
+
* #N/A as an empty cell". Both only when authored, so a file imported
|
|
2279
|
+
* without them round-trips unchanged.
|
|
2280
|
+
*/
|
|
2281
|
+
const applyEmptyCellsOptions = (chartNode, emptyCells, showNaAsEmpty) => {
|
|
2282
|
+
const dispBlanksAs = emptyCells ? DISP_BLANKS_AS[emptyCells] : undefined;
|
|
2283
|
+
if (dispBlanksAs) {
|
|
2284
|
+
setChildInOrder(chartNode, "c:dispBlanksAs", { a$: { val: dispBlanksAs } }, ["c:showDLblsOverMax", "c:extLst"]);
|
|
2285
|
+
}
|
|
2286
|
+
if (typeof showNaAsEmpty === "boolean") {
|
|
2287
|
+
const existing = chartNode["c:extLst"]?.["c:ext"];
|
|
2288
|
+
const exts = Array.isArray(existing) ? existing : existing ? [existing] : [];
|
|
2289
|
+
chartNode["c:extLst"] = {
|
|
2290
|
+
"c:ext": [
|
|
2291
|
+
...exts,
|
|
2292
|
+
{
|
|
2293
|
+
a$: {
|
|
2294
|
+
uri: "{56B9EC1D-385E-4148-901F-78D8002777C0}",
|
|
2295
|
+
"xmlns:c16r3": "http://schemas.microsoft.com/office/drawing/2017/03/chart",
|
|
2296
|
+
},
|
|
2297
|
+
"c16r3:dataDisplayOptions16": {
|
|
2298
|
+
"c16r3:dispNaAsBlank": { a$: { val: showNaAsEmpty ? "1" : "0" } },
|
|
2299
|
+
},
|
|
2300
|
+
},
|
|
2301
|
+
],
|
|
2302
|
+
};
|
|
2303
|
+
}
|
|
2304
|
+
};
|
|
1807
2305
|
const applyLegend = (chartNode, legend) => {
|
|
1808
2306
|
if (!legend)
|
|
1809
2307
|
return;
|
|
@@ -1838,16 +2336,19 @@ const applyDataLabels = (plotArea, dataLabels) => {
|
|
|
1838
2336
|
return;
|
|
1839
2337
|
// Find the chart-type container inside <c:plotArea>. We attach <c:dLbls>
|
|
1840
2338
|
// to whichever exists.
|
|
1841
|
-
|
|
2339
|
+
// Chart-level labels apply to every chart group — a combo chart has
|
|
2340
|
+
// one per series type.
|
|
2341
|
+
const containers = [
|
|
1842
2342
|
"c:barChart",
|
|
1843
2343
|
"c:lineChart",
|
|
1844
2344
|
"c:pieChart",
|
|
1845
2345
|
"c:doughnutChart",
|
|
1846
2346
|
"c:areaChart",
|
|
1847
|
-
]
|
|
1848
|
-
|
|
2347
|
+
]
|
|
2348
|
+
.flatMap((k) => (Array.isArray(plotArea[k]) ? plotArea[k] : [plotArea[k]]))
|
|
2349
|
+
.filter(Boolean);
|
|
2350
|
+
if (!containers.length)
|
|
1849
2351
|
return;
|
|
1850
|
-
const container = plotArea[containerKey];
|
|
1851
2352
|
const posCode = dataLabels.position && DLBL_POS[dataLabels.position];
|
|
1852
2353
|
const dLbls = {};
|
|
1853
2354
|
if (posCode)
|
|
@@ -1885,7 +2386,9 @@ const applyDataLabels = (plotArea, dataLabels) => {
|
|
|
1885
2386
|
a$: { val: dataLabels.showPercent ? "1" : "0" },
|
|
1886
2387
|
};
|
|
1887
2388
|
dLbls["c:showBubbleSize"] = { a$: { val: "0" } };
|
|
1888
|
-
container
|
|
2389
|
+
for (const container of containers) {
|
|
2390
|
+
container["c:dLbls"] = cloneTemplate(dLbls);
|
|
2391
|
+
}
|
|
1889
2392
|
};
|
|
1890
2393
|
const applyAxisOptions = (plotArea, options, kind) => {
|
|
1891
2394
|
if (!options)
|
|
@@ -1919,6 +2422,13 @@ const applyAxisOptions = (plotArea, options, kind) => {
|
|
|
1919
2422
|
if (options.showMajorGridlines === false && node["c:majorGridlines"]) {
|
|
1920
2423
|
delete node["c:majorGridlines"];
|
|
1921
2424
|
}
|
|
2425
|
+
// <c:numFmt sourceLinked="0"> — an explicit tick-label format. Without
|
|
2426
|
+
// one the axis stays "Linked to source" (the template's General/1).
|
|
2427
|
+
if (options.numberFormatPattern) {
|
|
2428
|
+
node["c:numFmt"] = {
|
|
2429
|
+
a$: { formatCode: options.numberFormatPattern, sourceLinked: "0" },
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
1922
2432
|
// <c:txPr> — axis tick-label color/font. Without this, exported
|
|
1923
2433
|
// axes would render with the default theme color instead of the
|
|
1924
2434
|
// authored one (BEP.xlsx axis colors #14325A / #F0621C).
|
|
@@ -1926,6 +2436,112 @@ const applyAxisOptions = (plotArea, options, kind) => {
|
|
|
1926
2436
|
if (txPr)
|
|
1927
2437
|
node["c:txPr"] = txPr;
|
|
1928
2438
|
};
|
|
2439
|
+
/**
|
|
2440
|
+
* Exploded pie / doughnut — `<c:explosion val="…"/>` on every slice series.
|
|
2441
|
+
* CT_PieSer order: idx, order, tx, spPr, explosion, dPt, dLbls, cat, val.
|
|
2442
|
+
*/
|
|
2443
|
+
const PIE_SER_KEYS_BEFORE_EXPLOSION = new Set([
|
|
2444
|
+
"c:idx",
|
|
2445
|
+
"c:order",
|
|
2446
|
+
"c:tx",
|
|
2447
|
+
"c:spPr",
|
|
2448
|
+
]);
|
|
2449
|
+
const applyExplosion = (plotArea, explosion) => {
|
|
2450
|
+
if (typeof explosion !== "number" || !(explosion > 0))
|
|
2451
|
+
return;
|
|
2452
|
+
const group = plotArea?.["c:pieChart"] ?? plotArea?.["c:doughnutChart"];
|
|
2453
|
+
if (!group)
|
|
2454
|
+
return;
|
|
2455
|
+
const sers = Array.isArray(group["c:ser"])
|
|
2456
|
+
? group["c:ser"]
|
|
2457
|
+
: group["c:ser"]
|
|
2458
|
+
? [group["c:ser"]]
|
|
2459
|
+
: [];
|
|
2460
|
+
const explosionNode = { a$: { val: String(Math.round(explosion)) } };
|
|
2461
|
+
const rebuiltSers = sers.map((ser) => {
|
|
2462
|
+
const rebuilt = {};
|
|
2463
|
+
let placed = false;
|
|
2464
|
+
for (const [key, value] of Object.entries(ser)) {
|
|
2465
|
+
if (key === "c:explosion")
|
|
2466
|
+
continue;
|
|
2467
|
+
if (!placed && !PIE_SER_KEYS_BEFORE_EXPLOSION.has(key)) {
|
|
2468
|
+
rebuilt["c:explosion"] = explosionNode;
|
|
2469
|
+
placed = true;
|
|
2470
|
+
}
|
|
2471
|
+
rebuilt[key] = value;
|
|
2472
|
+
}
|
|
2473
|
+
if (!placed)
|
|
2474
|
+
rebuilt["c:explosion"] = explosionNode;
|
|
2475
|
+
return rebuilt;
|
|
2476
|
+
});
|
|
2477
|
+
group["c:ser"] = Array.isArray(group["c:ser"]) ? rebuiltSers : rebuiltSers[0];
|
|
2478
|
+
};
|
|
2479
|
+
const isCircularChartType = (chartType) => chartType === "pie" || chartType === "doughnut";
|
|
2480
|
+
/** How many points a series' first source range holds (a column or a row of cells). */
|
|
2481
|
+
const sourcePointCount = (source) => {
|
|
2482
|
+
if (!source)
|
|
2483
|
+
return 0;
|
|
2484
|
+
const rows = source.endRowIndex - source.startRowIndex + 1;
|
|
2485
|
+
const cols = source.endColumnIndex - source.startColumnIndex + 1;
|
|
2486
|
+
return Math.max(0, rows > 1 ? rows : cols);
|
|
2487
|
+
};
|
|
2488
|
+
/**
|
|
2489
|
+
* Per-slice colours of a pie / doughnut — the renderer paints each slice
|
|
2490
|
+
* from `dataPointColors` when authored, else from the palette cycle
|
|
2491
|
+
* (`spec.palette`, else the theme accents Excel's `<c:varyColors>` also
|
|
2492
|
+
* cycles). Excel keeps a slice's own colour only as a `<c:dPt>`, so every
|
|
2493
|
+
* slice series gets one per point: authored overrides first, else the
|
|
2494
|
+
* palette colour of each slice index (nothing when the theme cycle is
|
|
2495
|
+
* in effect — `<c:varyColors val="1"/>` reproduces it). Replaces the
|
|
2496
|
+
* template's placeholder `<c:dPt>` and keeps CT_PieSer order (dPt after
|
|
2497
|
+
* spPr / explosion, before dLbls / cat / val).
|
|
2498
|
+
*/
|
|
2499
|
+
const PIE_SER_KEYS_BEFORE_DPT = new Set([
|
|
2500
|
+
"c:idx",
|
|
2501
|
+
"c:order",
|
|
2502
|
+
"c:tx",
|
|
2503
|
+
"c:spPr",
|
|
2504
|
+
"c:explosion",
|
|
2505
|
+
]);
|
|
2506
|
+
const applySliceColors = (plotArea, spec) => {
|
|
2507
|
+
if (!isCircularChartType(spec?.chartType))
|
|
2508
|
+
return;
|
|
2509
|
+
const group = plotArea?.["c:pieChart"] ?? plotArea?.["c:doughnutChart"];
|
|
2510
|
+
if (!group)
|
|
2511
|
+
return;
|
|
2512
|
+
const sers = Array.isArray(group["c:ser"])
|
|
2513
|
+
? group["c:ser"]
|
|
2514
|
+
: group["c:ser"]
|
|
2515
|
+
? [group["c:ser"]]
|
|
2516
|
+
: [];
|
|
2517
|
+
const palette = spec?.palette;
|
|
2518
|
+
const series = (spec?.series ?? []);
|
|
2519
|
+
const rebuiltSers = sers.map((ser, index) => {
|
|
2520
|
+
const entry = series[index];
|
|
2521
|
+
const authored = entry?.dataPointColors;
|
|
2522
|
+
const pointColors = authored?.length
|
|
2523
|
+
? authored
|
|
2524
|
+
: palette?.length
|
|
2525
|
+
? Array.from({ length: sourcePointCount(entry?.sources?.[0]) }, (_, idx) => ({ idx, color: palette[idx % palette.length] }))
|
|
2526
|
+
: undefined;
|
|
2527
|
+
const dPtNodes = buildDataPointNodes(pointColors);
|
|
2528
|
+
const rebuilt = {};
|
|
2529
|
+
let placed = false;
|
|
2530
|
+
for (const [key, value] of Object.entries(ser)) {
|
|
2531
|
+
if (key === "c:dPt")
|
|
2532
|
+
continue;
|
|
2533
|
+
if (dPtNodes && !placed && !PIE_SER_KEYS_BEFORE_DPT.has(key)) {
|
|
2534
|
+
rebuilt["c:dPt"] = dPtNodes;
|
|
2535
|
+
placed = true;
|
|
2536
|
+
}
|
|
2537
|
+
rebuilt[key] = value;
|
|
2538
|
+
}
|
|
2539
|
+
if (dPtNodes && !placed)
|
|
2540
|
+
rebuilt["c:dPt"] = dPtNodes;
|
|
2541
|
+
return rebuilt;
|
|
2542
|
+
});
|
|
2543
|
+
group["c:ser"] = Array.isArray(group["c:ser"]) ? rebuiltSers : rebuiltSers[0];
|
|
2544
|
+
};
|
|
1929
2545
|
const applyDoughnutHole = (plotArea, holeSize) => {
|
|
1930
2546
|
if (typeof holeSize !== "number")
|
|
1931
2547
|
return;
|