@rowsncolumns/toolkit 24.4.4 → 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 +11 -4
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { generateSelectionsFromFormula } from "@rowsncolumns/utils";
|
|
2
2
|
import { uuid } from "@rowsncolumns/utils";
|
|
3
|
+
import { XMLParser } from "fast-xml-parser";
|
|
3
4
|
import { get } from "lodash-es";
|
|
5
|
+
import { anchorPlacementFromEditAs } from "./drawing";
|
|
4
6
|
/**
|
|
5
7
|
* Pick the data sheetId for a parsed selection. Honours the explicit
|
|
6
8
|
* sheet prefix in the chart's formula reference (cross-sheet charts);
|
|
@@ -83,6 +85,26 @@ export async function processChart(chartTarget, drawingPath, anchor, sheetId, zi
|
|
|
83
85
|
roundedCornersVal === true
|
|
84
86
|
? true
|
|
85
87
|
: undefined;
|
|
88
|
+
// Chart Styles gallery id: `<c14:style>` (Office 2010+, id + 100)
|
|
89
|
+
// inside mc:Choice wins over the `<c:style>` fallback. Excel writes
|
|
90
|
+
// 2 / 102 on an unstyled chart, so that default stays implicit.
|
|
91
|
+
const chartStyle = extractChartStyle(chartSpaceRoot);
|
|
92
|
+
// PivotChart binding: `<c:pivotSource><c:name>` names the pivot; the
|
|
93
|
+
// id is resolved once the workbook's pivots are parsed
|
|
94
|
+
// (`bindPivotChartsToPivotTables`). `<c:pivotFmts>` and the c14
|
|
95
|
+
// field-button options ride along on the spec.
|
|
96
|
+
const pivotSourceName = extractPivotSourceName(chartSpaceRoot);
|
|
97
|
+
const pivotSourceFormatId = extractPivotSourceFormatId(chartSpaceRoot);
|
|
98
|
+
const pivotFormats = extractPivotFormats(chart);
|
|
99
|
+
if (pivotFormats)
|
|
100
|
+
attachPivotFormatBodies(pivotFormats, chartContent);
|
|
101
|
+
const pivotFieldButtons = extractPivotFieldButtons(chartSpaceRoot);
|
|
102
|
+
// Format Chart Area fill / border from the chartSpace `<c:spPr>`.
|
|
103
|
+
// Only literal `<a:srgbClr>` counts: Excel's default is a theme
|
|
104
|
+
// `bg1` fill, which must not pin every imported chart to white.
|
|
105
|
+
const chartSpacePr = get(chartSpaceRoot, "c:spPr") ?? get(chartSpaceRoot, "spPr");
|
|
106
|
+
const chartAreaFill = extractSrgbHex(get(chartSpacePr, "a:solidFill") ?? get(chartSpacePr, "solidFill"));
|
|
107
|
+
const chartAreaBorder = extractSrgbHex(get(chartSpacePr, "a:ln.a:solidFill") ?? get(chartSpacePr, "ln.solidFill"));
|
|
86
108
|
const chartId = uuid();
|
|
87
109
|
// Extract chart title (text + formatting)
|
|
88
110
|
const title = extractChartTitle(chart);
|
|
@@ -106,6 +128,10 @@ export async function processChart(chartTarget, drawingPath, anchor, sheetId, zi
|
|
|
106
128
|
const scatterStyle = chartType === "scatter" ? extractScatterStyle(plotArea) : undefined;
|
|
107
129
|
// Extract hidden dimension strategy from plotVisOnly
|
|
108
130
|
const hiddenDimensionStrategy = extractHiddenDimensionStrategy(chart);
|
|
131
|
+
// "Hidden and Empty Cells": only the non-default choices are recorded
|
|
132
|
+
// (Excel writes `gap` and `dispNaAsBlank="1"` into every file).
|
|
133
|
+
const emptyCells = extractEmptyCellsPolicy(chart);
|
|
134
|
+
const showNaAsEmpty = extractShowNaAsEmpty(chart);
|
|
109
135
|
// Extract position from anchor
|
|
110
136
|
const position = extractChartPosition(anchor, sheetId, sheetDimensions);
|
|
111
137
|
if (!position) {
|
|
@@ -116,6 +142,10 @@ export async function processChart(chartTarget, drawingPath, anchor, sheetId, zi
|
|
|
116
142
|
const dataLabels = extractDataLabelsOptions(plotArea, chartType);
|
|
117
143
|
const { valueAxisOptions, secondaryValueAxisOptions, categoryAxisOptions, } = extractAxisOptions(plotArea);
|
|
118
144
|
const doughnutHoleSize = chartType === "doughnut" ? extractDoughnutHoleSize(plotArea) : undefined;
|
|
145
|
+
// Exploded pie / doughnut — the per-series <c:explosion> percentage.
|
|
146
|
+
const explosion = chartType === "pie" || chartType === "doughnut"
|
|
147
|
+
? extractExplosion(plotArea, chartType)
|
|
148
|
+
: undefined;
|
|
119
149
|
// Pie-of-pie / bar-of-pie — <c:ofPieChart> degrades to chartType
|
|
120
150
|
// "pie" with sourceChartType "OF_PIE" plus the split fields the
|
|
121
151
|
// renderer uses to draw the composite layout.
|
|
@@ -135,6 +165,11 @@ export async function processChart(chartTarget, drawingPath, anchor, sheetId, zi
|
|
|
135
165
|
const barLayout = chartType === "bar" || chartType === "column"
|
|
136
166
|
? extractBarLayoutOptions(plotArea)
|
|
137
167
|
: undefined;
|
|
168
|
+
// Line-only — <c:grouping> (stacked / percentStacked) and the
|
|
169
|
+
// chart-level <c:marker> flag behind Excel's "Line with Markers".
|
|
170
|
+
const lineLayout = chartType === "line" ? extractLineChartOptions(plotArea, series) : undefined;
|
|
171
|
+
// Stock-only — HLC / OHLC / VHLC / VOHLC from the series layout.
|
|
172
|
+
const stockVariant = chartType === "stock" ? extractStockVariant(plotArea) : undefined;
|
|
138
173
|
return {
|
|
139
174
|
chartId,
|
|
140
175
|
spec: {
|
|
@@ -145,6 +180,8 @@ export async function processChart(chartTarget, drawingPath, anchor, sheetId, zi
|
|
|
145
180
|
series,
|
|
146
181
|
dataRange,
|
|
147
182
|
hiddenDimensionStrategy,
|
|
183
|
+
...(emptyCells && emptyCells !== "gaps" ? { emptyCells } : {}),
|
|
184
|
+
...(showNaAsEmpty === false ? { showNaAsEmpty } : {}),
|
|
148
185
|
// Scatter/bubble must conform to BubbleChartSpec which requires
|
|
149
186
|
// `bubbleLabels`. We don't currently extract per-point labels
|
|
150
187
|
// from OOXML (rare in real files); set an empty array so the
|
|
@@ -167,6 +204,7 @@ export async function processChart(chartTarget, drawingPath, anchor, sheetId, zi
|
|
|
167
204
|
...(typeof doughnutHoleSize === "number"
|
|
168
205
|
? { doughnutHoleSize }
|
|
169
206
|
: {}),
|
|
207
|
+
...(typeof explosion === "number" ? { explosion } : {}),
|
|
170
208
|
...(ofPieOptions
|
|
171
209
|
? { sourceChartType: "OF_PIE", ...ofPieOptions }
|
|
172
210
|
: {}),
|
|
@@ -184,9 +222,23 @@ export async function processChart(chartTarget, drawingPath, anchor, sheetId, zi
|
|
|
184
222
|
...(typeof barLayout?.gapWidth === "number"
|
|
185
223
|
? { gapWidth: barLayout.gapWidth }
|
|
186
224
|
: {}),
|
|
225
|
+
...(lineLayout?.stackedType
|
|
226
|
+
? { stackedType: lineLayout.stackedType }
|
|
227
|
+
: {}),
|
|
228
|
+
...(lineLayout?.showMarkers ? { showMarkers: true } : {}),
|
|
229
|
+
...(stockVariant ? { stockVariant } : {}),
|
|
187
230
|
...(roundedCorners ? { roundedCorners } : {}),
|
|
231
|
+
...(chartStyle !== undefined ? { chartStyle } : {}),
|
|
232
|
+
...(chartAreaFill ? { backgroundColor: chartAreaFill } : {}),
|
|
233
|
+
...(pivotSourceName ? { [PIVOT_SOURCE_NAME_KEY]: pivotSourceName } : {}),
|
|
234
|
+
...(pivotSourceFormatId
|
|
235
|
+
? { [PIVOT_SOURCE_FORMAT_ID_KEY]: pivotSourceFormatId }
|
|
236
|
+
: {}),
|
|
237
|
+
...(pivotFormats ? { pivotFormats } : {}),
|
|
238
|
+
...(pivotFieldButtons ? { pivotFieldButtons } : {}),
|
|
188
239
|
},
|
|
189
240
|
position,
|
|
241
|
+
...(chartAreaBorder ? { borderColor: chartAreaBorder } : {}),
|
|
190
242
|
id: chartId,
|
|
191
243
|
type: "chart",
|
|
192
244
|
};
|
|
@@ -256,8 +308,20 @@ function getSeriesElements(plotArea, chartType) {
|
|
|
256
308
|
]);
|
|
257
309
|
case "radar":
|
|
258
310
|
return getWithFallback(plotArea, ["c:radarChart.c:ser", "radarChart.ser"]);
|
|
259
|
-
case "stock":
|
|
260
|
-
|
|
311
|
+
case "stock": {
|
|
312
|
+
// VHLC / VOHLC: the volume series lives in a sibling <c:barChart>
|
|
313
|
+
// and comes first (utils/stock.ts STOCK_ROLES); the price series
|
|
314
|
+
// follow in the <c:stockChart>.
|
|
315
|
+
const toArray = (value) => value === undefined || value === null
|
|
316
|
+
? []
|
|
317
|
+
: Array.isArray(value)
|
|
318
|
+
? value
|
|
319
|
+
: [value];
|
|
320
|
+
return [
|
|
321
|
+
...toArray(getWithFallback(plotArea, ["c:barChart.c:ser", "barChart.ser"])),
|
|
322
|
+
...toArray(getWithFallback(plotArea, ["c:stockChart.c:ser", "stockChart.ser"])),
|
|
323
|
+
];
|
|
324
|
+
}
|
|
261
325
|
case "scatter":
|
|
262
326
|
return getWithFallback(plotArea, [
|
|
263
327
|
"c:scatterChart.c:ser",
|
|
@@ -456,6 +520,51 @@ export function extractHiddenDimensionStrategy(chart) {
|
|
|
456
520
|
const skipHidden = plotVisOnly === "1" || plotVisOnly === "true" || plotVisOnly === true;
|
|
457
521
|
return skipHidden ? "SKIP_HIDDEN_ROWS_AND_COLUMNS" : "SHOW_ALL";
|
|
458
522
|
}
|
|
523
|
+
/**
|
|
524
|
+
* `<c:dispBlanksAs val="gap|zero|span"/>` — Excel's "Show empty cells as"
|
|
525
|
+
* (Select Data → Hidden and Empty Cells). `span` is "Connect data points
|
|
526
|
+
* with line".
|
|
527
|
+
*/
|
|
528
|
+
export function extractEmptyCellsPolicy(chart) {
|
|
529
|
+
const val = getWithFallback(chart, [
|
|
530
|
+
"c:dispBlanksAs.a$.val",
|
|
531
|
+
"dispBlanksAs.a$.val",
|
|
532
|
+
]);
|
|
533
|
+
switch (val) {
|
|
534
|
+
case "gap":
|
|
535
|
+
return "gaps";
|
|
536
|
+
case "zero":
|
|
537
|
+
return "zero";
|
|
538
|
+
case "span":
|
|
539
|
+
return "connect";
|
|
540
|
+
default:
|
|
541
|
+
return undefined;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Excel 2016+ "Show #N/A as an empty cell":
|
|
546
|
+
* `<c:chart><c:extLst><c:ext uri="{56B9EC1D-…}"><c16r3:dataDisplayOptions16>
|
|
547
|
+
* <c16r3:dispNaAsBlank val="0|1"/>`. Undefined when the file predates the
|
|
548
|
+
* option (Excel then treats #N/A the legacy way — as if `val="0"`, but the
|
|
549
|
+
* spec leaves the default to the renderer).
|
|
550
|
+
*/
|
|
551
|
+
export function extractShowNaAsEmpty(chart) {
|
|
552
|
+
const extLst = getWithFallback(chart, ["c:extLst", "extLst"]);
|
|
553
|
+
if (!extLst)
|
|
554
|
+
return undefined;
|
|
555
|
+
const extNode = getWithFallback(extLst, ["c:ext", "ext"]);
|
|
556
|
+
const exts = Array.isArray(extNode) ? extNode : extNode ? [extNode] : [];
|
|
557
|
+
for (const ext of exts) {
|
|
558
|
+
const val = getWithFallback(ext, [
|
|
559
|
+
"c16r3:dataDisplayOptions16.c16r3:dispNaAsBlank.a$.val",
|
|
560
|
+
"dataDisplayOptions16.dispNaAsBlank.a$.val",
|
|
561
|
+
]);
|
|
562
|
+
if (val !== undefined) {
|
|
563
|
+
return val === "1" || val === "true" || val === true;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return undefined;
|
|
567
|
+
}
|
|
459
568
|
/**
|
|
460
569
|
* Read the chart title's text formatting. Excel chart titles can be
|
|
461
570
|
* styled two ways:
|
|
@@ -467,6 +576,46 @@ export function extractHiddenDimensionStrategy(chart) {
|
|
|
467
576
|
* the more explicit signal). Returns `undefined` when neither carries
|
|
468
577
|
* formatting so the renderer falls back to its theme defaults.
|
|
469
578
|
*/
|
|
579
|
+
/**
|
|
580
|
+
* `<c:chartSpace>` → the Chart Styles id, from `<c14:style>` in the
|
|
581
|
+
* mc:Choice (id + 100) or the plain `<c:style>`. Excel's unstyled default
|
|
582
|
+
* (2) is returned as `undefined` so it stays implicit on the spec.
|
|
583
|
+
*/
|
|
584
|
+
function extractChartStyle(chartSpaceRoot) {
|
|
585
|
+
const alternates = [
|
|
586
|
+
getWithFallback(chartSpaceRoot, ["mc:AlternateContent", "AlternateContent"]),
|
|
587
|
+
]
|
|
588
|
+
.flat()
|
|
589
|
+
.filter(Boolean);
|
|
590
|
+
let raw;
|
|
591
|
+
for (const alternate of alternates) {
|
|
592
|
+
const c14 = getWithFallback(alternate, [
|
|
593
|
+
"mc:Choice.c14:style.a$.val",
|
|
594
|
+
"Choice.c14:style.a$.val",
|
|
595
|
+
"Choice.style.a$.val",
|
|
596
|
+
]);
|
|
597
|
+
if (c14 !== undefined) {
|
|
598
|
+
const value = Number(c14);
|
|
599
|
+
raw = value > 100 ? value - 100 : value;
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (raw === undefined) {
|
|
604
|
+
raw = getWithFallback(chartSpaceRoot, ["c:style.a$.val", "style.a$.val"]);
|
|
605
|
+
}
|
|
606
|
+
const style = Number(raw);
|
|
607
|
+
if (!Number.isInteger(style) || style < 1 || style > 48 || style === 2) {
|
|
608
|
+
return undefined;
|
|
609
|
+
}
|
|
610
|
+
return style;
|
|
611
|
+
}
|
|
612
|
+
/** `<a:solidFill><a:srgbClr val="RRGGBB"/>` → `#RRGGBB`; theme fills are ignored. */
|
|
613
|
+
function extractSrgbHex(solidFill) {
|
|
614
|
+
const raw = get(solidFill, "a:srgbClr.a$.val") ?? get(solidFill, "srgbClr.a$.val");
|
|
615
|
+
return typeof raw === "string" && /^[0-9a-fA-F]{6}$/.test(raw)
|
|
616
|
+
? `#${raw.toUpperCase()}`
|
|
617
|
+
: undefined;
|
|
618
|
+
}
|
|
470
619
|
function extractChartTitleTextFormat(chart) {
|
|
471
620
|
const title = get(chart, "c:title") ?? get(chart, "title");
|
|
472
621
|
if (!title)
|
|
@@ -478,6 +627,310 @@ function extractChartTitleTextFormat(chart) {
|
|
|
478
627
|
const txPr = get(title, "c:txPr") ?? get(title, "txPr");
|
|
479
628
|
return extractTextProperties(txPr);
|
|
480
629
|
}
|
|
630
|
+
/**
|
|
631
|
+
* Transient spec key carrying `<c:pivotSource><c:name>` until the workbook's
|
|
632
|
+
* pivots are known; `bindPivotChartsToPivotTables` turns it into
|
|
633
|
+
* `pivotTableId` and removes it.
|
|
634
|
+
*/
|
|
635
|
+
export const PIVOT_SOURCE_NAME_KEY = "__pivotSourceName";
|
|
636
|
+
/**
|
|
637
|
+
* Transient spec key carrying `<c:pivotSource><c:fmtId>` alongside the
|
|
638
|
+
* name; becomes `pivotSource.formatId` once the chart binds to its pivot.
|
|
639
|
+
*/
|
|
640
|
+
export const PIVOT_SOURCE_FORMAT_ID_KEY = "__pivotSourceFormatId";
|
|
641
|
+
/**
|
|
642
|
+
* `<c:pivotSource><c:fmtId val="3"/>` — the id the pivot's `<chartFormats>`
|
|
643
|
+
* name this chart by. `0` is the exporter's default and stays implicit.
|
|
644
|
+
*/
|
|
645
|
+
export function extractPivotSourceFormatId(chartSpaceRoot) {
|
|
646
|
+
const raw = getWithFallback(chartSpaceRoot, [
|
|
647
|
+
"c:pivotSource.c:fmtId.a$.val",
|
|
648
|
+
"pivotSource.fmtId.a$.val",
|
|
649
|
+
]);
|
|
650
|
+
const id = toInt(raw);
|
|
651
|
+
return Number.isFinite(id) && id > 0 ? id : undefined;
|
|
652
|
+
}
|
|
653
|
+
/** `<c:pivotSource><c:name>[Book1.xlsx]Sheet1!PivotTable1</c:name>` → the name text. */
|
|
654
|
+
export function extractPivotSourceName(chartSpaceRoot) {
|
|
655
|
+
const raw = getWithFallback(chartSpaceRoot, [
|
|
656
|
+
"c:pivotSource.c:name.t$",
|
|
657
|
+
"c:pivotSource.c:name",
|
|
658
|
+
"pivotSource.name.t$",
|
|
659
|
+
"pivotSource.name",
|
|
660
|
+
]);
|
|
661
|
+
const name = typeof raw === "string" ? raw.trim() : undefined;
|
|
662
|
+
return name ? name : undefined;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Split a pivotSource name into its sheet and pivot-table parts. The
|
|
666
|
+
* `[workbook]` prefix is dropped (it names the file the chart lived in);
|
|
667
|
+
* the sheet may be quoted (`'My Sheet'!PivotTable1`).
|
|
668
|
+
*/
|
|
669
|
+
export function parsePivotSourceName(name) {
|
|
670
|
+
const withoutBook = name.replace(/^\[[^\]]*\]/, "");
|
|
671
|
+
const bang = withoutBook.lastIndexOf("!");
|
|
672
|
+
if (bang === -1) {
|
|
673
|
+
return withoutBook ? { pivotName: withoutBook } : undefined;
|
|
674
|
+
}
|
|
675
|
+
const pivotName = withoutBook.slice(bang + 1);
|
|
676
|
+
if (!pivotName)
|
|
677
|
+
return undefined;
|
|
678
|
+
let sheetTitle = withoutBook.slice(0, bang);
|
|
679
|
+
if (sheetTitle.startsWith("'") && sheetTitle.endsWith("'")) {
|
|
680
|
+
sheetTitle = sheetTitle.slice(1, -1).replace(/''/g, "'");
|
|
681
|
+
}
|
|
682
|
+
return { sheetTitle: sheetTitle || undefined, pivotName };
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Resolve every imported chart's `<c:pivotSource>` name to the id of the
|
|
686
|
+
* imported PivotTable it names (matched by pivot name, and by the pivot's
|
|
687
|
+
* target sheet when the name carries one). A name that matches no pivot
|
|
688
|
+
* leaves a plain chart — Excel converts a PivotChart whose pivot is gone
|
|
689
|
+
* to a static chart too. The transient key is always removed.
|
|
690
|
+
*/
|
|
691
|
+
export function bindPivotChartsToPivotTables(charts, pivotTables, sheetTitleById) {
|
|
692
|
+
for (const chart of charts) {
|
|
693
|
+
const spec = chart.spec;
|
|
694
|
+
const raw = spec[PIVOT_SOURCE_NAME_KEY];
|
|
695
|
+
const formatId = spec[PIVOT_SOURCE_FORMAT_ID_KEY];
|
|
696
|
+
delete spec[PIVOT_SOURCE_FORMAT_ID_KEY];
|
|
697
|
+
if (raw === undefined)
|
|
698
|
+
continue;
|
|
699
|
+
delete spec[PIVOT_SOURCE_NAME_KEY];
|
|
700
|
+
const parsed = typeof raw === "string" ? parsePivotSourceName(raw) : undefined;
|
|
701
|
+
if (!parsed)
|
|
702
|
+
continue;
|
|
703
|
+
const byName = pivotTables.filter((pivot) => (pivot.name ?? `PivotTable${pivot.pivotId}`) === parsed.pivotName);
|
|
704
|
+
const match = byName.find((pivot) => parsed.sheetTitle === undefined ||
|
|
705
|
+
sheetTitleById.get(pivot.targetPosition?.sheetId) === parsed.sheetTitle) ?? (parsed.sheetTitle === undefined ? byName[0] : undefined);
|
|
706
|
+
if (!match)
|
|
707
|
+
continue;
|
|
708
|
+
spec.pivotTableId = match.pivotId;
|
|
709
|
+
if (!spec.pivotSource) {
|
|
710
|
+
spec.pivotSource = { categoryAxis: "rows" };
|
|
711
|
+
}
|
|
712
|
+
if (typeof formatId === "number") {
|
|
713
|
+
spec.pivotSource.formatId = formatId;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* `<c:chart><c:pivotFmts><c:pivotFmt><c:idx val/>…` — the per pivot-item
|
|
719
|
+
* formats Excel re-applies on a re-plot; only the index and a literal
|
|
720
|
+
* fill colour are kept.
|
|
721
|
+
*/
|
|
722
|
+
export function extractPivotFormats(chart) {
|
|
723
|
+
const raw = getWithFallback(chart, ["c:pivotFmts.c:pivotFmt", "pivotFmts.pivotFmt"]);
|
|
724
|
+
if (raw === undefined)
|
|
725
|
+
return undefined;
|
|
726
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
727
|
+
const formats = [];
|
|
728
|
+
for (const fmt of list) {
|
|
729
|
+
const idx = toInt(getWithFallback(fmt, ["c:idx.a$.val", "idx.a$.val"]));
|
|
730
|
+
const fill = getWithFallback(fmt, [
|
|
731
|
+
"c:spPr.a:solidFill.a:srgbClr.a$.val",
|
|
732
|
+
"spPr.solidFill.srgbClr.a$.val",
|
|
733
|
+
]);
|
|
734
|
+
formats.push({
|
|
735
|
+
index: idx,
|
|
736
|
+
...(typeof fill === "string" && fill ? { color: `#${fill.toUpperCase()}` } : {}),
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
return formats.length ? formats : undefined;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Namespace URI → the prefix Excel writes for it. Bodies are stored with these
|
|
743
|
+
* canonical prefixes so a file that declared, say, `xmlns:c="…"` as `cs:` still
|
|
744
|
+
* round-trips through an exporter that declares `c:` / `a:` at the root.
|
|
745
|
+
*/
|
|
746
|
+
export const CHART_XML_PREFIXES = {
|
|
747
|
+
"http://schemas.openxmlformats.org/drawingml/2006/chart": "c",
|
|
748
|
+
"http://schemas.openxmlformats.org/drawingml/2006/main": "a",
|
|
749
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships": "r",
|
|
750
|
+
"http://schemas.openxmlformats.org/markup-compatibility/2006": "mc",
|
|
751
|
+
"http://schemas.microsoft.com/office/drawing/2007/8/2/chart": "c14",
|
|
752
|
+
"http://schemas.microsoft.com/office/drawing/2012/chart": "c15",
|
|
753
|
+
"http://schemas.microsoft.com/office/drawing/2014/chart": "c16",
|
|
754
|
+
"http://schemas.microsoft.com/office/drawing/2015/06/chart": "c16r2",
|
|
755
|
+
"http://schemas.microsoft.com/office/drawing/2017/03/chart": "c16r3",
|
|
756
|
+
"http://schemas.microsoft.com/office/drawing/2010/main": "a14",
|
|
757
|
+
"http://schemas.microsoft.com/office/drawing/2014/main": "a16",
|
|
758
|
+
};
|
|
759
|
+
// Ordered parse of a chart part — the object parse the plugin uses drops the
|
|
760
|
+
// order of differently named siblings, which DrawingML schemas fix
|
|
761
|
+
// (`<a:gsLst>` before `<a:lin>`, `<a:bodyPr>` before `<a:p>`), so the verbatim
|
|
762
|
+
// bodies are read from a `preserveOrder` pass instead.
|
|
763
|
+
const orderedChartParser = new XMLParser({
|
|
764
|
+
ignoreAttributes: false,
|
|
765
|
+
preserveOrder: true,
|
|
766
|
+
attributeNamePrefix: "",
|
|
767
|
+
textNodeName: "t$",
|
|
768
|
+
trimValues: false,
|
|
769
|
+
processEntities: false,
|
|
770
|
+
});
|
|
771
|
+
const localName = (tag) => tag.slice(tag.indexOf(":") + 1);
|
|
772
|
+
/**
|
|
773
|
+
* `preserveOrder` node → `ChartXmlElement`, prefixes canonicalised through the
|
|
774
|
+
* `xmlns:*` declarations seen so far (`declared`: prefix → URI); namespace
|
|
775
|
+
* declarations themselves are dropped — the exporters re-emit them.
|
|
776
|
+
*/
|
|
777
|
+
const toChartXmlElement = (node, declared) => {
|
|
778
|
+
const tag = Object.keys(node).find((k) => k !== ":@");
|
|
779
|
+
if (!tag)
|
|
780
|
+
return undefined;
|
|
781
|
+
const rawAttrs = (node[":@"] ?? {});
|
|
782
|
+
const scope = new Map(declared);
|
|
783
|
+
for (const [k, v] of Object.entries(rawAttrs)) {
|
|
784
|
+
if (k === "xmlns")
|
|
785
|
+
scope.set("", String(v));
|
|
786
|
+
else if (k.startsWith("xmlns:"))
|
|
787
|
+
scope.set(k.slice(6), String(v));
|
|
788
|
+
}
|
|
789
|
+
const canonical = (name) => {
|
|
790
|
+
const at = name.indexOf(":");
|
|
791
|
+
const prefix = at === -1 ? "" : name.slice(0, at);
|
|
792
|
+
const uri = scope.get(prefix);
|
|
793
|
+
const known = uri ? CHART_XML_PREFIXES[uri] : undefined;
|
|
794
|
+
if (!known)
|
|
795
|
+
return name;
|
|
796
|
+
return `${known}:${at === -1 ? name : name.slice(at + 1)}`;
|
|
797
|
+
};
|
|
798
|
+
const attributes = {};
|
|
799
|
+
for (const [k, v] of Object.entries(rawAttrs)) {
|
|
800
|
+
if (k === "xmlns" || k.startsWith("xmlns:"))
|
|
801
|
+
continue;
|
|
802
|
+
attributes[k.includes(":") ? canonical(k) : k] = String(v);
|
|
803
|
+
}
|
|
804
|
+
const element = { tag: canonical(tag) };
|
|
805
|
+
if (Object.keys(attributes).length)
|
|
806
|
+
element.attributes = attributes;
|
|
807
|
+
const kids = Array.isArray(node[tag]) ? node[tag] : [];
|
|
808
|
+
const children = [];
|
|
809
|
+
let text;
|
|
810
|
+
for (const kid of kids) {
|
|
811
|
+
if (!kid || typeof kid !== "object")
|
|
812
|
+
continue;
|
|
813
|
+
if ("t$" in kid) {
|
|
814
|
+
text = (text ?? "") + String(kid.t$);
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
const child = toChartXmlElement(kid, scope);
|
|
818
|
+
if (child)
|
|
819
|
+
children.push(child);
|
|
820
|
+
}
|
|
821
|
+
if (children.length)
|
|
822
|
+
element.children = children;
|
|
823
|
+
else if (text !== undefined && text !== "")
|
|
824
|
+
element.text = text;
|
|
825
|
+
return element;
|
|
826
|
+
};
|
|
827
|
+
/**
|
|
828
|
+
* Read every `<c:pivotFmt>` body (its children after `<c:idx>`) verbatim from
|
|
829
|
+
* the chart part and attach it to the matching `pivotFormats` entry by index
|
|
830
|
+
* (rowsncolumns/spreadsheet#668 C-83). Gradient fills, theme colours, markers
|
|
831
|
+
* and data-label formatting Excel keeps per pivot item survive the round-trip
|
|
832
|
+
* that `color` (a literal solid fill) alone cannot carry.
|
|
833
|
+
*/
|
|
834
|
+
export function attachPivotFormatBodies(formats, chartContent) {
|
|
835
|
+
let ordered;
|
|
836
|
+
try {
|
|
837
|
+
ordered = orderedChartParser.parse(chartContent);
|
|
838
|
+
}
|
|
839
|
+
catch {
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
const findPivotFmts = (nodes, declared) => {
|
|
843
|
+
for (const node of nodes) {
|
|
844
|
+
if (!node || typeof node !== "object")
|
|
845
|
+
continue;
|
|
846
|
+
const tag = Object.keys(node).find((k) => k !== ":@");
|
|
847
|
+
// Text nodes (`{ t$ }`) have no element children to search.
|
|
848
|
+
if (!tag || !Array.isArray(node[tag]))
|
|
849
|
+
continue;
|
|
850
|
+
const scope = new Map(declared);
|
|
851
|
+
for (const [k, v] of Object.entries((node[":@"] ?? {}))) {
|
|
852
|
+
if (k === "xmlns")
|
|
853
|
+
scope.set("", String(v));
|
|
854
|
+
else if (k.startsWith("xmlns:"))
|
|
855
|
+
scope.set(k.slice(6), String(v));
|
|
856
|
+
}
|
|
857
|
+
if (localName(tag) === "pivotFmts")
|
|
858
|
+
return { node, declared: scope };
|
|
859
|
+
const found = findPivotFmts((node[tag] ?? []), scope);
|
|
860
|
+
if (found)
|
|
861
|
+
return found;
|
|
862
|
+
}
|
|
863
|
+
return undefined;
|
|
864
|
+
};
|
|
865
|
+
const found = findPivotFmts(ordered, new Map());
|
|
866
|
+
if (!found)
|
|
867
|
+
return;
|
|
868
|
+
const fmtsTag = Object.keys(found.node).find((k) => k !== ":@");
|
|
869
|
+
const byIndex = new Map();
|
|
870
|
+
for (const fmt of (found.node[fmtsTag] ?? [])) {
|
|
871
|
+
if (!fmt || typeof fmt !== "object")
|
|
872
|
+
continue;
|
|
873
|
+
const fmtTag = Object.keys(fmt).find((k) => k !== ":@");
|
|
874
|
+
if (!fmtTag || localName(fmtTag) !== "pivotFmt" || !Array.isArray(fmt[fmtTag]))
|
|
875
|
+
continue;
|
|
876
|
+
const kids = fmt[fmtTag];
|
|
877
|
+
let index;
|
|
878
|
+
const body = [];
|
|
879
|
+
for (const kid of kids) {
|
|
880
|
+
if (!kid || typeof kid !== "object")
|
|
881
|
+
continue;
|
|
882
|
+
const kidTag = Object.keys(kid).find((k) => k !== ":@");
|
|
883
|
+
if (!kidTag)
|
|
884
|
+
continue;
|
|
885
|
+
if (localName(kidTag) === "idx") {
|
|
886
|
+
index = toInt(kid[":@"]?.val);
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
const element = toChartXmlElement(kid, found.declared);
|
|
890
|
+
if (element)
|
|
891
|
+
body.push(element);
|
|
892
|
+
}
|
|
893
|
+
if (index !== undefined && body.length)
|
|
894
|
+
byIndex.set(index, body);
|
|
895
|
+
}
|
|
896
|
+
for (const format of formats) {
|
|
897
|
+
const body = byIndex.get(format.index);
|
|
898
|
+
if (body)
|
|
899
|
+
format.body = body;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const PIVOT_OPTIONS_EXT_URI = "{781A3756-C4B2-4CAC-9D66-4F8BD8637D16}";
|
|
903
|
+
/**
|
|
904
|
+
* Excel's Field Buttons choices — `<c14:pivotOptions>` in the chartSpace
|
|
905
|
+
* `<c:extLst>`: `dropZoneFilter` / `dropZoneCategories` / `dropZoneData` /
|
|
906
|
+
* `dropZoneSeries` / `dropZonesVisible`, each `1` unless hidden. Only the
|
|
907
|
+
* hidden ones are recorded (every button shows by default).
|
|
908
|
+
*/
|
|
909
|
+
export function extractPivotFieldButtons(chartSpaceRoot) {
|
|
910
|
+
const extList = getWithFallback(chartSpaceRoot, ["c:extLst.c:ext", "extLst.ext"]);
|
|
911
|
+
if (extList === undefined)
|
|
912
|
+
return undefined;
|
|
913
|
+
const exts = Array.isArray(extList) ? extList : [extList];
|
|
914
|
+
const ext = exts.find((e) => e?.a$?.uri === PIVOT_OPTIONS_EXT_URI);
|
|
915
|
+
const options = getWithFallback(ext, ["c14:pivotOptions", "pivotOptions"]);
|
|
916
|
+
if (!options)
|
|
917
|
+
return undefined;
|
|
918
|
+
const isHidden = (element) => {
|
|
919
|
+
const val = getWithFallback(options, [
|
|
920
|
+
`c14:${element}.a$.val`,
|
|
921
|
+
`${element}.a$.val`,
|
|
922
|
+
]);
|
|
923
|
+
return val === "0" || val === "false" || val === false;
|
|
924
|
+
};
|
|
925
|
+
const buttons = {
|
|
926
|
+
...(isHidden("dropZonesVisible") ? { visible: false } : {}),
|
|
927
|
+
...(isHidden("dropZoneFilter") ? { reportFilter: false } : {}),
|
|
928
|
+
...(isHidden("dropZoneCategories") ? { axis: false } : {}),
|
|
929
|
+
...(isHidden("dropZoneSeries") ? { legend: false } : {}),
|
|
930
|
+
...(isHidden("dropZoneData") ? { value: false } : {}),
|
|
931
|
+
};
|
|
932
|
+
return Object.keys(buttons).length ? buttons : undefined;
|
|
933
|
+
}
|
|
481
934
|
export function extractChartTitle(chart) {
|
|
482
935
|
// Try different possible paths for chart title
|
|
483
936
|
const title =
|
|
@@ -532,12 +985,19 @@ export function determineChartType(chart) {
|
|
|
532
985
|
const c15Type = detectC15ChartType(plotArea);
|
|
533
986
|
if (c15Type)
|
|
534
987
|
return c15Type;
|
|
535
|
-
|
|
988
|
+
// Excel's VHLC / VOHLC stock charts pair a <c:barChart> (the volume
|
|
989
|
+
// series) with the <c:stockChart>, so the stock group must win over the
|
|
990
|
+
// bar scan below.
|
|
991
|
+
if (getWithFallback(plotArea, ["c:stockChart", "stockChart"]))
|
|
992
|
+
return "stock";
|
|
993
|
+
// A chart whose secondary axis group is split into its own container
|
|
994
|
+
// carries an array here; the primary group comes first.
|
|
995
|
+
const barChart = firstGroup(getWithFallback(plotArea, [
|
|
536
996
|
"c:barChart",
|
|
537
997
|
"barChart",
|
|
538
998
|
"c:bar3DChart",
|
|
539
999
|
"bar3DChart",
|
|
540
|
-
]);
|
|
1000
|
+
]));
|
|
541
1001
|
if (barChart) {
|
|
542
1002
|
const direction = get(barChart, "c:barDir.a$.val") ||
|
|
543
1003
|
get(barChart, "c:barDir.a$.t$") ||
|
|
@@ -585,8 +1045,6 @@ export function determineChartType(chart) {
|
|
|
585
1045
|
return "area";
|
|
586
1046
|
if (getWithFallback(plotArea, ["c:radarChart", "radarChart"]))
|
|
587
1047
|
return "radar";
|
|
588
|
-
if (getWithFallback(plotArea, ["c:stockChart", "stockChart"]))
|
|
589
|
-
return "stock";
|
|
590
1048
|
// Scatter and bubble — both use c:xVal/c:yVal series shape (not
|
|
591
1049
|
// c:cat/c:val like other charts). Bubble additionally has c:bubbleSize.
|
|
592
1050
|
if (getWithFallback(plotArea, ["c:scatterChart", "scatterChart"]))
|
|
@@ -778,6 +1236,77 @@ const CHART_TYPE_CONTAINERS = [
|
|
|
778
1236
|
isXY: true,
|
|
779
1237
|
},
|
|
780
1238
|
];
|
|
1239
|
+
/**
|
|
1240
|
+
* A series' position in the chart — `<c:order>` (Excel's plot / legend
|
|
1241
|
+
* order), falling back to `<c:idx>`; NaN when the series carries neither.
|
|
1242
|
+
*/
|
|
1243
|
+
function seriesOrderOf(ser) {
|
|
1244
|
+
const raw = get(ser, "c:order.a$.val") ??
|
|
1245
|
+
get(ser, "order.a$.val") ??
|
|
1246
|
+
get(ser, "c:idx.a$.val") ??
|
|
1247
|
+
get(ser, "idx.a$.val");
|
|
1248
|
+
const n = Number(raw);
|
|
1249
|
+
return raw === undefined || raw === null || raw === "" ? NaN : n;
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Put the collected series back in Excel's `<c:order>`: a chart split into
|
|
1253
|
+
* several containers (one per chart type and per axis group) lists them
|
|
1254
|
+
* container by container, which is not the authored order. Only sorts
|
|
1255
|
+
* when every series carries an order (older / hand-written files without
|
|
1256
|
+
* one keep their document order).
|
|
1257
|
+
*/
|
|
1258
|
+
function sortSeriesByOrder(series, orders) {
|
|
1259
|
+
if (series.length < 2 || orders.length !== series.length)
|
|
1260
|
+
return series;
|
|
1261
|
+
if (orders.some((order) => !Number.isFinite(order)))
|
|
1262
|
+
return series;
|
|
1263
|
+
return series
|
|
1264
|
+
.map((entry, index) => ({ entry, order: orders[index], index }))
|
|
1265
|
+
.sort((a, b) => a.order - b.order || a.index - b.index)
|
|
1266
|
+
.map(({ entry }) => entry);
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* The first chart-type container of a group that may be split into one
|
|
1270
|
+
* container per axis group (primary first, then the secondary group).
|
|
1271
|
+
*/
|
|
1272
|
+
function firstGroup(node) {
|
|
1273
|
+
return Array.isArray(node) ? node[0] : node;
|
|
1274
|
+
}
|
|
1275
|
+
/** `<c:axId val>` values of an axis element or a chart-type container. */
|
|
1276
|
+
function axisIdsOf(node) {
|
|
1277
|
+
const raw = getWithFallback(node, ["c:axId", "axId"]);
|
|
1278
|
+
if (!raw)
|
|
1279
|
+
return [];
|
|
1280
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
1281
|
+
return list
|
|
1282
|
+
.map((entry) => entry?.a$?.val ?? entry?.a$?.t$ ?? entry)
|
|
1283
|
+
.filter((id) => id != null)
|
|
1284
|
+
.map(String);
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* The axis ids of the plot area's SECONDARY axis group — every `<c:valAx>`,
|
|
1288
|
+
* `<c:catAx>` and `<c:dateAx>` after the first of its kind. Excel keeps one
|
|
1289
|
+
* category axis and one value axis per axis group; a chart-type container
|
|
1290
|
+
* whose `<c:axId>`s name any of these ids plots its series on the
|
|
1291
|
+
* secondary group (Excel's "Plot Series On: Secondary Axis").
|
|
1292
|
+
*/
|
|
1293
|
+
function secondaryAxisGroupIds(plotArea) {
|
|
1294
|
+
const ids = new Set();
|
|
1295
|
+
for (const key of ["c:valAx", "valAx", "c:catAx", "catAx", "c:dateAx", "dateAx"]) {
|
|
1296
|
+
const axes = get(plotArea, key);
|
|
1297
|
+
if (!axes)
|
|
1298
|
+
continue;
|
|
1299
|
+
const list = Array.isArray(axes) ? axes : [axes];
|
|
1300
|
+
list.slice(1).forEach((axis) => axisIdsOf(axis).forEach((id) => ids.add(id)));
|
|
1301
|
+
}
|
|
1302
|
+
return ids;
|
|
1303
|
+
}
|
|
1304
|
+
/** True when a chart-type container's `<c:axId>`s bind it to the secondary axis group. */
|
|
1305
|
+
function isSecondaryGroupContainer(container, secondaryIds) {
|
|
1306
|
+
if (!secondaryIds.size)
|
|
1307
|
+
return false;
|
|
1308
|
+
return axisIdsOf(container).some((id) => secondaryIds.has(id));
|
|
1309
|
+
}
|
|
781
1310
|
/**
|
|
782
1311
|
* Resolve a chart-formula range string (e.g.
|
|
783
1312
|
* `'Data for the Chart'!$A$7:$A$26`) into the sources array our chart
|
|
@@ -838,9 +1367,13 @@ function extractInlineValues(node) {
|
|
|
838
1367
|
}
|
|
839
1368
|
export function extractChartSeries(plotArea, chartType, sheetId, resolveSheetId) {
|
|
840
1369
|
const series = [];
|
|
1370
|
+
const orders = [];
|
|
841
1371
|
let seriesIndex = 0;
|
|
842
|
-
|
|
843
|
-
|
|
1372
|
+
const secondaryIds = secondaryAxisGroupIds(plotArea);
|
|
1373
|
+
// Iterate through all chart type containers to support combo charts.
|
|
1374
|
+
// A stock chart's <c:barChart> is its volume series, not a combo member
|
|
1375
|
+
// — the stock path below reads both groups in role order.
|
|
1376
|
+
for (const container of chartType === "stock" ? [] : CHART_TYPE_CONTAINERS) {
|
|
844
1377
|
const chartElement = getWithFallback(plotArea, container.paths);
|
|
845
1378
|
if (!chartElement)
|
|
846
1379
|
continue;
|
|
@@ -849,6 +1382,9 @@ export function extractChartSeries(plotArea, chartType, sheetId, resolveSheetId)
|
|
|
849
1382
|
? chartElement
|
|
850
1383
|
: [chartElement];
|
|
851
1384
|
for (const singleChartElement of chartElements) {
|
|
1385
|
+
// Series in a container bound to the secondary axis group plot on
|
|
1386
|
+
// the secondary axes (Excel's "Plot Series On: Secondary Axis").
|
|
1387
|
+
const useSecondaryAxis = isSecondaryGroupContainer(singleChartElement, secondaryIds);
|
|
852
1388
|
// Determine the actual chart type for this container
|
|
853
1389
|
let seriesChartType = container.chartType;
|
|
854
1390
|
if (container.isBar) {
|
|
@@ -911,6 +1447,7 @@ export function extractChartSeries(plotArea, chartType, sheetId, resolveSheetId)
|
|
|
911
1447
|
const categorySources = serCatRef
|
|
912
1448
|
? resolveSourcesFromRef(serCatRef, sheetId, resolveSheetId)
|
|
913
1449
|
: [];
|
|
1450
|
+
orders.push(seriesOrderOf(ser));
|
|
914
1451
|
series.push({
|
|
915
1452
|
dataLabel: seriesTitle || `Series ${seriesIndex + 1}`,
|
|
916
1453
|
...(nameRef ? { dataLabelRef: nameRef } : {}),
|
|
@@ -926,6 +1463,7 @@ export function extractChartSeries(plotArea, chartType, sheetId, resolveSheetId)
|
|
|
926
1463
|
// aren't part of combo mixing — leave it unset there so the
|
|
927
1464
|
// renderer falls back to the chart-level chartType.
|
|
928
1465
|
...(container.isXY ? {} : { seriesChartType: seriesChartType }),
|
|
1466
|
+
...(useSecondaryAxis ? { useSecondaryAxis: true } : {}),
|
|
929
1467
|
sources,
|
|
930
1468
|
...(inlineValues && inlineValues.length ? { inlineValues } : {}),
|
|
931
1469
|
...(trendlines && trendlines.length ? { trendlines } : {}),
|
|
@@ -978,6 +1516,7 @@ export function extractChartSeries(plotArea, chartType, sheetId, resolveSheetId)
|
|
|
978
1516
|
const categorySources = serCatRef
|
|
979
1517
|
? resolveSourcesFromRef(serCatRef, sheetId, resolveSheetId)
|
|
980
1518
|
: [];
|
|
1519
|
+
orders.push(seriesOrderOf(ser));
|
|
981
1520
|
series.push({
|
|
982
1521
|
dataLabel: seriesTitle || `Series ${index + 1}`,
|
|
983
1522
|
...(nameRef ? { dataLabelRef: nameRef } : {}),
|
|
@@ -997,7 +1536,9 @@ export function extractChartSeries(plotArea, chartType, sheetId, resolveSheetId)
|
|
|
997
1536
|
});
|
|
998
1537
|
}
|
|
999
1538
|
}
|
|
1000
|
-
|
|
1539
|
+
// Stock charts read their price roles in document order; every other
|
|
1540
|
+
// chart lists its series in Excel's <c:order>.
|
|
1541
|
+
return chartType === "stock" ? series : sortSeriesByOrder(series, orders);
|
|
1001
1542
|
}
|
|
1002
1543
|
/**
|
|
1003
1544
|
* Extract per-data-point color overrides — `<c:dPt>` array inside
|
|
@@ -1247,10 +1788,12 @@ function extractTextProperties(txPr) {
|
|
|
1247
1788
|
* renderer keeps its defaults.
|
|
1248
1789
|
*/
|
|
1249
1790
|
function extractBarLayoutOptions(plotArea) {
|
|
1250
|
-
|
|
1791
|
+
// The primary axis group's container when the secondary group is split
|
|
1792
|
+
// into its own (Excel writes one container per axis group).
|
|
1793
|
+
const bar = firstGroup(get(plotArea, "c:barChart") ??
|
|
1251
1794
|
get(plotArea, "barChart") ??
|
|
1252
1795
|
get(plotArea, "c:bar3DChart") ??
|
|
1253
|
-
get(plotArea, "bar3DChart");
|
|
1796
|
+
get(plotArea, "bar3DChart"));
|
|
1254
1797
|
if (!bar)
|
|
1255
1798
|
return undefined;
|
|
1256
1799
|
// `<c:grouping val="clustered|stacked|percentStacked|standard"/>`.
|
|
@@ -1284,6 +1827,66 @@ function extractBarLayoutOptions(plotArea) {
|
|
|
1284
1827
|
result.gapWidth = gapWidthVal;
|
|
1285
1828
|
return Object.keys(result).length ? result : undefined;
|
|
1286
1829
|
}
|
|
1830
|
+
/**
|
|
1831
|
+
* Read `<c:lineChart>` / `<c:line3DChart>` `<c:grouping>` (standard /
|
|
1832
|
+
* stacked / percentStacked → `stackedType`) and the chart-level
|
|
1833
|
+
* `<c:marker val>` flag. Excel writes `<c:marker val="1"/>` on every line
|
|
1834
|
+
* chart and hides a plain "Line"'s markers per series through
|
|
1835
|
+
* `<c:marker><c:symbol val="none"/>`, so `showMarkers` is only reported
|
|
1836
|
+
* when the flag is set and at least one series is not "none".
|
|
1837
|
+
*/
|
|
1838
|
+
function extractLineChartOptions(plotArea, series) {
|
|
1839
|
+
// The primary axis group's container when the secondary group is split
|
|
1840
|
+
// into its own (Excel writes one container per axis group).
|
|
1841
|
+
const line = firstGroup(get(plotArea, "c:lineChart") ??
|
|
1842
|
+
get(plotArea, "lineChart") ??
|
|
1843
|
+
get(plotArea, "c:line3DChart") ??
|
|
1844
|
+
get(plotArea, "line3DChart"));
|
|
1845
|
+
if (!line)
|
|
1846
|
+
return undefined;
|
|
1847
|
+
const groupingVal = get(line, "c:grouping.a$.val") ??
|
|
1848
|
+
get(line, "grouping.a$.val") ??
|
|
1849
|
+
get(line, "c:grouping.a$.t$") ??
|
|
1850
|
+
get(line, "grouping.a$.t$");
|
|
1851
|
+
const result = {};
|
|
1852
|
+
switch (groupingVal) {
|
|
1853
|
+
case "stacked":
|
|
1854
|
+
result.stackedType = "STACKED";
|
|
1855
|
+
break;
|
|
1856
|
+
case "percentStacked":
|
|
1857
|
+
result.stackedType = "PERCENT_STACKED";
|
|
1858
|
+
break;
|
|
1859
|
+
case "standard":
|
|
1860
|
+
result.stackedType = "UNSTACKED";
|
|
1861
|
+
break;
|
|
1862
|
+
}
|
|
1863
|
+
const markerVal = get(line, "c:marker.a$.val") ?? get(line, "marker.a$.val");
|
|
1864
|
+
const markerFlag = markerVal === "1" ||
|
|
1865
|
+
markerVal === 1 ||
|
|
1866
|
+
markerVal === "true" ||
|
|
1867
|
+
markerVal === true;
|
|
1868
|
+
if (markerFlag &&
|
|
1869
|
+
series.length > 0 &&
|
|
1870
|
+
series.some((entry) => entry.marker?.symbol !== "none")) {
|
|
1871
|
+
result.showMarkers = true;
|
|
1872
|
+
}
|
|
1873
|
+
return Object.keys(result).length ? result : undefined;
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Which of Excel's four stock layouts a `<c:stockChart>` is: a sibling
|
|
1877
|
+
* `<c:barChart>` carries the volume series (V-variants), and four price
|
|
1878
|
+
* series (open/high/low/close) instead of three mean OHLC.
|
|
1879
|
+
*/
|
|
1880
|
+
function extractStockVariant(plotArea) {
|
|
1881
|
+
const prices = getWithFallback(plotArea, [
|
|
1882
|
+
"c:stockChart.c:ser",
|
|
1883
|
+
"stockChart.ser",
|
|
1884
|
+
]);
|
|
1885
|
+
const priceCount = Array.isArray(prices) ? prices.length : prices ? 1 : 0;
|
|
1886
|
+
const hasVolume = Boolean(getWithFallback(plotArea, ["c:barChart", "barChart"]));
|
|
1887
|
+
const body = priceCount >= 4 ? "OHLC" : "HLC";
|
|
1888
|
+
return hasVolume ? `V${body}` : body;
|
|
1889
|
+
}
|
|
1287
1890
|
/**
|
|
1288
1891
|
* Extract chart-level `<c:scatterStyle val=…>`. Excel default is
|
|
1289
1892
|
* "lineMarker" (straight lines + markers). Returns undefined for
|
|
@@ -1537,6 +2140,12 @@ function extractSeriesColorInfo(ser) {
|
|
|
1537
2140
|
}
|
|
1538
2141
|
export function extractChartDomains(plotArea, chartType, sheetId, resolveSheetId) {
|
|
1539
2142
|
const domains = [];
|
|
2143
|
+
// Non-XY: Excel keeps one category axis per axis group. The primary
|
|
2144
|
+
// group's first `<c:cat>` is domains[0]; when a container bound to the
|
|
2145
|
+
// secondary axis group carries a different `<c:cat>`, that list is
|
|
2146
|
+
// domains[1] — the secondary group's category axis.
|
|
2147
|
+
const secondaryIds = secondaryAxisGroupIds(plotArea);
|
|
2148
|
+
let primaryRef;
|
|
1540
2149
|
// Search all chart type containers for domains. For XY charts the
|
|
1541
2150
|
// "domain" is the X values (c:xVal); for everything else it's the
|
|
1542
2151
|
// category axis (c:cat). Per-container loop covers combo charts.
|
|
@@ -1557,23 +2166,42 @@ export function extractChartDomains(plotArea, chartType, sheetId, resolveSheetId
|
|
|
1557
2166
|
const seriesArray = Array.isArray(seriesElements)
|
|
1558
2167
|
? seriesElements
|
|
1559
2168
|
: [seriesElements];
|
|
2169
|
+
const secondaryGroup = !container.isXY &&
|
|
2170
|
+
isSecondaryGroupContainer(singleChartElement, secondaryIds);
|
|
1560
2171
|
for (const ser of seriesArray) {
|
|
1561
2172
|
const domainRef = container.isXY
|
|
1562
2173
|
? getWithFallback(ser, X_VAL_REF_PATHS)
|
|
1563
2174
|
: getWithFallback(ser, CAT_REF_PATHS);
|
|
1564
2175
|
if (domainRef) {
|
|
2176
|
+
if (!container.isXY && domains.length) {
|
|
2177
|
+
// Primary already found: only a secondary-group container
|
|
2178
|
+
// with a different category list adds domains[1].
|
|
2179
|
+
if (secondaryGroup && domainRef !== primaryRef) {
|
|
2180
|
+
const sources = resolveSourcesFromRef(domainRef, sheetId, resolveSheetId);
|
|
2181
|
+
if (sources.length) {
|
|
2182
|
+
domains.push({ sources });
|
|
2183
|
+
return domains;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
break;
|
|
2187
|
+
}
|
|
1565
2188
|
const sources = resolveSourcesFromRef(domainRef, sheetId, resolveSheetId);
|
|
1566
2189
|
if (sources.length) {
|
|
1567
2190
|
domains.push({ sources });
|
|
1568
2191
|
// XY charts may carry per-series domains that differ — keep
|
|
1569
2192
|
// the first one we encounter for the dataRange anchor, but
|
|
1570
2193
|
// continue collecting so the renderer has a per-series X
|
|
1571
|
-
// when it needs one. For non-XY,
|
|
1572
|
-
|
|
1573
|
-
|
|
2194
|
+
// when it needs one. For non-XY, remember the primary ref
|
|
2195
|
+
// and keep scanning for a secondary group's own list.
|
|
2196
|
+
if (!container.isXY) {
|
|
2197
|
+
primaryRef = domainRef;
|
|
2198
|
+
if (!secondaryIds.size)
|
|
2199
|
+
return domains;
|
|
2200
|
+
break;
|
|
2201
|
+
}
|
|
1574
2202
|
}
|
|
1575
2203
|
}
|
|
1576
|
-
else if (!container.isXY) {
|
|
2204
|
+
else if (!container.isXY && !domains.length) {
|
|
1577
2205
|
// No cell ref — check for an inline `<c:strLit>`/`<c:numLit>`
|
|
1578
2206
|
// category list (e.g. `["Estimated", "Actual"]` embedded
|
|
1579
2207
|
// directly in the chart XML). Same for the series val side,
|
|
@@ -1650,6 +2278,28 @@ export function extractChartPosition(anchor, sheetId, sheetDimensions) {
|
|
|
1650
2278
|
// Handle oneCellAnchor
|
|
1651
2279
|
const oneCellFrom = getWithFallback(anchor, ["xdr:from", "from"]) ?? undefined;
|
|
1652
2280
|
const hasTwoCellTarget = getWithFallback(anchor, ["xdr:to", "to"]) !== undefined;
|
|
2281
|
+
// absoluteAnchor: `<xdr:pos>` + `<xdr:ext>` — "Don't move or size with
|
|
2282
|
+
// cells"; kept as A1-relative pixel offsets (#668 C-06)
|
|
2283
|
+
const absolutePos = getWithFallback(anchor, ["xdr:pos", "pos"]);
|
|
2284
|
+
if (!oneCellFrom && absolutePos) {
|
|
2285
|
+
const posAttributes = absolutePos.a$ ?? absolutePos;
|
|
2286
|
+
const extNode = getWithFallback(anchor, ["xdr:ext", "ext"]);
|
|
2287
|
+
const extAttributes = (extNode && (extNode.a$ ?? extNode)) || undefined;
|
|
2288
|
+
const emuToPx = (value, fallback) => value !== undefined && value !== null
|
|
2289
|
+
? Math.round(parseInt(String(value), 10) / 9525)
|
|
2290
|
+
: fallback;
|
|
2291
|
+
return {
|
|
2292
|
+
sheetId,
|
|
2293
|
+
overlayPosition: {
|
|
2294
|
+
anchorCell: { rowIndex: 1, columnIndex: 1 },
|
|
2295
|
+
offsetXPixels: emuToPx(posAttributes?.x, 0),
|
|
2296
|
+
offsetYPixels: emuToPx(posAttributes?.y, 0),
|
|
2297
|
+
widthPixels: emuToPx(extAttributes?.cx, 400),
|
|
2298
|
+
heightPixels: emuToPx(extAttributes?.cy, 300),
|
|
2299
|
+
placement: "absolute",
|
|
2300
|
+
},
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
1653
2303
|
if (oneCellFrom && !hasTwoCellTarget) {
|
|
1654
2304
|
const extNode = getWithFallback(anchor, ["xdr:ext", "ext"]);
|
|
1655
2305
|
const extAttributes = (extNode && (extNode.a$ ?? extNode)) || undefined;
|
|
@@ -1675,6 +2325,7 @@ export function extractChartPosition(anchor, sheetId, sheetDimensions) {
|
|
|
1675
2325
|
offsetYPixels: Math.round(rowOff / 9525),
|
|
1676
2326
|
widthPixels: width,
|
|
1677
2327
|
heightPixels: height,
|
|
2328
|
+
placement: "moveOnly",
|
|
1678
2329
|
},
|
|
1679
2330
|
};
|
|
1680
2331
|
}
|
|
@@ -1722,6 +2373,10 @@ export function extractChartPosition(anchor, sheetId, sheetDimensions) {
|
|
|
1722
2373
|
heightPx += Math.round(toRowOff / 9525);
|
|
1723
2374
|
const width = Math.max(widthPx, 100);
|
|
1724
2375
|
const height = Math.max(heightPx, 100);
|
|
2376
|
+
// `editAs` is the placement mode; the default "Move and size with
|
|
2377
|
+
// cells" keeps `to` as the second anchor so the frame follows the
|
|
2378
|
+
// cells between the corners (#668 C-06)
|
|
2379
|
+
const placement = anchorPlacementFromEditAs(getWithFallback(anchor, ["a$.editAs"]));
|
|
1725
2380
|
return {
|
|
1726
2381
|
sheetId,
|
|
1727
2382
|
overlayPosition: {
|
|
@@ -1733,6 +2388,13 @@ export function extractChartPosition(anchor, sheetId, sheetDimensions) {
|
|
|
1733
2388
|
offsetYPixels: Math.round(rowOff / 9525),
|
|
1734
2389
|
widthPixels: width,
|
|
1735
2390
|
heightPixels: height,
|
|
2391
|
+
...(placement === "moveAndSize"
|
|
2392
|
+
? {
|
|
2393
|
+
toCell: { rowIndex: toRow + 1, columnIndex: toCol + 1 },
|
|
2394
|
+
toOffsetXPixels: Math.round(toColOff / 9525),
|
|
2395
|
+
toOffsetYPixels: Math.round(toRowOff / 9525),
|
|
2396
|
+
}
|
|
2397
|
+
: { placement }),
|
|
1736
2398
|
},
|
|
1737
2399
|
};
|
|
1738
2400
|
}
|
|
@@ -1839,7 +2501,7 @@ function extractDataLabelsOptions(plotArea, chartType) {
|
|
|
1839
2501
|
const keys = containerKeys[chartType] ?? [];
|
|
1840
2502
|
let container;
|
|
1841
2503
|
for (const key of keys) {
|
|
1842
|
-
container = container ?? get(plotArea, key);
|
|
2504
|
+
container = container ?? firstGroup(get(plotArea, key));
|
|
1843
2505
|
}
|
|
1844
2506
|
const dLbls = get(container, "c:dLbls") ??
|
|
1845
2507
|
get(container, "dLbls") ??
|
|
@@ -1925,6 +2587,17 @@ function readAxis(axis) {
|
|
|
1925
2587
|
const textFormat = extractTextProperties(get(axis, "c:txPr") ?? get(axis, "txPr"));
|
|
1926
2588
|
if (textFormat)
|
|
1927
2589
|
result.textFormat = textFormat;
|
|
2590
|
+
// <c:numFmt formatCode="…" sourceLinked="0"/> — an explicit tick-label
|
|
2591
|
+
// format. `sourceLinked="1"` (Excel's default) means the axis inherits
|
|
2592
|
+
// the source cells' format, which the renderer resolves at draw time,
|
|
2593
|
+
// so it is intentionally NOT copied into the spec.
|
|
2594
|
+
const numFmt = get(axis, "c:numFmt.a$") ?? get(axis, "numFmt.a$");
|
|
2595
|
+
const formatCode = numFmt?.formatCode ?? numFmt?.t$;
|
|
2596
|
+
if (typeof formatCode === "string" &&
|
|
2597
|
+
formatCode &&
|
|
2598
|
+
!readBoolean(numFmt?.sourceLinked)) {
|
|
2599
|
+
result.numberFormatPattern = formatCode;
|
|
2600
|
+
}
|
|
1928
2601
|
return Object.keys(result).length ? result : undefined;
|
|
1929
2602
|
}
|
|
1930
2603
|
export function extractAxisOptions(plotArea) {
|
|
@@ -1982,6 +2655,25 @@ function extractDoughnutHoleSize(plotArea) {
|
|
|
1982
2655
|
return undefined;
|
|
1983
2656
|
return Math.min(0.9, Math.max(0, holeSize / 100));
|
|
1984
2657
|
}
|
|
2658
|
+
/**
|
|
2659
|
+
* Exploded pie / doughnut — `<c:ser><c:explosion val="25"/>` is the
|
|
2660
|
+
* slice offset as a percentage of the radius. Excel writes it on every
|
|
2661
|
+
* series of an exploded chart; the spec keeps one chart-level value, so
|
|
2662
|
+
* the largest authored explosion wins. Undefined when no series is
|
|
2663
|
+
* exploded (Excel omits the element for a plain pie).
|
|
2664
|
+
*/
|
|
2665
|
+
function extractExplosion(plotArea, chartType) {
|
|
2666
|
+
const sers = getSeriesElements(plotArea, chartType);
|
|
2667
|
+
const list = Array.isArray(sers) ? sers : sers ? [sers] : [];
|
|
2668
|
+
let explosion;
|
|
2669
|
+
for (const ser of list) {
|
|
2670
|
+
const value = readNumber(get(ser, "c:explosion.a$.val") ?? get(ser, "explosion.a$.val"));
|
|
2671
|
+
if (typeof value === "number" && value > 0) {
|
|
2672
|
+
explosion = Math.max(explosion ?? 0, value);
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
return explosion;
|
|
2676
|
+
}
|
|
1985
2677
|
// -------------------------- Phase 5.2 helpers --------------------------
|
|
1986
2678
|
const mapTrendlineType = (val) => {
|
|
1987
2679
|
switch (val) {
|