@lotics/xlsx 0.1.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/package.json +30 -0
- package/src/auto_sum.test.ts +71 -0
- package/src/auto_sum.ts +83 -0
- package/src/canvas_cf.ts +135 -0
- package/src/canvas_chart.ts +687 -0
- package/src/canvas_fill.ts +156 -0
- package/src/canvas_images.ts +99 -0
- package/src/canvas_layout.test.ts +159 -0
- package/src/canvas_layout.ts +239 -0
- package/src/canvas_renderer.ts +1010 -0
- package/src/canvas_shape.ts +242 -0
- package/src/canvas_sparkline.ts +163 -0
- package/src/canvas_text.ts +454 -0
- package/src/cell_editor.ts +558 -0
- package/src/clipboard.test.ts +145 -0
- package/src/clipboard.ts +341 -0
- package/src/command_history.ts +102 -0
- package/src/csv_parser.test.ts +130 -0
- package/src/csv_parser.ts +172 -0
- package/src/data_validation.test.ts +95 -0
- package/src/data_validation.ts +114 -0
- package/src/editing_layer.test.ts +309 -0
- package/src/excel_cf_evaluate.test.ts +293 -0
- package/src/excel_cf_evaluate.ts +719 -0
- package/src/excel_cf_icons.ts +108 -0
- package/src/excel_cf_types.ts +67 -0
- package/src/excel_numfmt.test.ts +607 -0
- package/src/excel_numfmt.ts +1033 -0
- package/src/excel_parser.test.ts +1061 -0
- package/src/excel_parser.ts +393 -0
- package/src/excel_pattern_fills.ts +64 -0
- package/src/excel_table_styles.ts +160 -0
- package/src/excel_utils.test.ts +108 -0
- package/src/excel_utils.ts +162 -0
- package/src/fast-formula-parser.d.ts +53 -0
- package/src/fill_logic.ts +257 -0
- package/src/fill_patterns.test.ts +90 -0
- package/src/fill_patterns.ts +71 -0
- package/src/flash_fill.test.ts +75 -0
- package/src/flash_fill.ts +221 -0
- package/src/formula_deps.ts +189 -0
- package/src/formula_engine.test.ts +348 -0
- package/src/formula_engine.ts +401 -0
- package/src/formula_highlight.test.ts +81 -0
- package/src/formula_highlight.ts +139 -0
- package/src/formula_suggest.ts +100 -0
- package/src/hidden_sheets.test.ts +49 -0
- package/src/index.ts +149 -0
- package/src/keyboard_nav.test.ts +394 -0
- package/src/keyboard_nav.ts +891 -0
- package/src/move_logic.ts +63 -0
- package/src/numfmt_type.test.ts +158 -0
- package/src/numfmt_type.ts +231 -0
- package/src/ooxml_cell_format.ts +70 -0
- package/src/ooxml_chart.test.ts +85 -0
- package/src/ooxml_chart.ts +347 -0
- package/src/ooxml_chart_types.ts +207 -0
- package/src/ooxml_color.ts +339 -0
- package/src/ooxml_drawing.test.ts +87 -0
- package/src/ooxml_drawing.ts +287 -0
- package/src/ooxml_pivot.test.ts +195 -0
- package/src/ooxml_pivot.ts +468 -0
- package/src/ooxml_pivot_types.ts +165 -0
- package/src/ooxml_shape.ts +355 -0
- package/src/ooxml_sheet.ts +1271 -0
- package/src/ooxml_styles.ts +556 -0
- package/src/ooxml_table.test.ts +70 -0
- package/src/ooxml_table.ts +131 -0
- package/src/ooxml_workbook.test.ts +40 -0
- package/src/ooxml_workbook.ts +259 -0
- package/src/ooxml_zip.ts +33 -0
- package/src/pivot_model.ts +237 -0
- package/src/pivot_recompute.test.ts +210 -0
- package/src/pivot_recompute.ts +413 -0
- package/src/selection_model.ts +364 -0
- package/src/spreadsheet_commands.test.ts +772 -0
- package/src/spreadsheet_commands.ts +1805 -0
- package/src/structured_refs.test.ts +128 -0
- package/src/structured_refs.ts +160 -0
- package/src/style_helpers.test.ts +33 -0
- package/src/style_helpers.ts +30 -0
- package/src/template_builder.test.ts +139 -0
- package/src/template_builder.ts +36 -0
- package/src/types.ts +239 -0
- package/src/workbook_model.test.ts +536 -0
- package/src/workbook_model.ts +911 -0
- package/src/xlsx_round_trip.test.ts +279 -0
- package/src/xlsx_writer.test.ts +488 -0
- package/src/xlsx_writer.ts +1549 -0
- package/src/xml_entities.ts +23 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// OOXML Chart Parser
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Parses xl/charts/chartN.xml files into ParsedChart objects.
|
|
6
|
+
// Handles: bar, column, line, pie, doughnut, area, scatter, bubble, radar,
|
|
7
|
+
// stock, combo charts. Extracts series data, axes, legend, formatting.
|
|
8
|
+
// =============================================================================
|
|
9
|
+
|
|
10
|
+
import type { ParsedChart, ChartType, ChartSeries, ChartAxis } from "./ooxml_chart_types";
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Parser
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse a chart XML string into a ParsedChart (without anchor — caller sets it).
|
|
18
|
+
*/
|
|
19
|
+
export function parseChartXml(
|
|
20
|
+
xml: string,
|
|
21
|
+
parseXml: (xml: string) => Record<string, unknown>,
|
|
22
|
+
): Omit<ParsedChart, "anchor"> {
|
|
23
|
+
const doc = parseXml(xml);
|
|
24
|
+
const chartSpace = (getObj(doc, "c:chartSpace") ?? getObj(doc, "chartSpace") ?? doc) as Record<string, unknown>;
|
|
25
|
+
const chart = (getObj(chartSpace, "c:chart") ?? getObj(chartSpace, "chart") ?? chartSpace) as Record<string, unknown>;
|
|
26
|
+
const plotArea = (getObj(chart, "c:plotArea") ?? getObj(chart, "plotArea") ?? chart) as Record<string, unknown>;
|
|
27
|
+
|
|
28
|
+
// Detect chart type from plot area child elements
|
|
29
|
+
const { chartType, series, categories } = parseChartData(plotArea as Record<string, unknown>);
|
|
30
|
+
|
|
31
|
+
// Title
|
|
32
|
+
const titleObj = getObj(chart as Record<string, unknown>, "c:title")
|
|
33
|
+
?? getObj(chart as Record<string, unknown>, "title");
|
|
34
|
+
const title = extractTitle(titleObj);
|
|
35
|
+
|
|
36
|
+
// Legend
|
|
37
|
+
const legendObj = getObj(chart as Record<string, unknown>, "c:legend")
|
|
38
|
+
?? getObj(chart as Record<string, unknown>, "legend");
|
|
39
|
+
const legendPosition = extractLegendPosition(legendObj);
|
|
40
|
+
|
|
41
|
+
// Axes
|
|
42
|
+
const axes = parseAxes(plotArea as Record<string, unknown>);
|
|
43
|
+
|
|
44
|
+
return { chartType, series, title, legendPosition, axes, categories };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// =============================================================================
|
|
48
|
+
// Chart data extraction
|
|
49
|
+
// =============================================================================
|
|
50
|
+
|
|
51
|
+
const CHART_TYPE_MAP: Record<string, ChartType> = {
|
|
52
|
+
"c:barChart": "col",
|
|
53
|
+
"c:bar3DChart": "col",
|
|
54
|
+
"c:lineChart": "line",
|
|
55
|
+
"c:line3DChart": "line",
|
|
56
|
+
"c:pieChart": "pie",
|
|
57
|
+
"c:pie3DChart": "pie",
|
|
58
|
+
"c:doughnutChart": "doughnut",
|
|
59
|
+
"c:areaChart": "area",
|
|
60
|
+
"c:area3DChart": "area",
|
|
61
|
+
"c:scatterChart": "scatter",
|
|
62
|
+
"c:bubbleChart": "bubble",
|
|
63
|
+
"c:radarChart": "radar",
|
|
64
|
+
"c:stockChart": "stock",
|
|
65
|
+
"c:surfaceChart": "surface",
|
|
66
|
+
"c:surface3DChart": "surface",
|
|
67
|
+
"barChart": "col",
|
|
68
|
+
"bar3DChart": "col",
|
|
69
|
+
"lineChart": "line",
|
|
70
|
+
"line3DChart": "line",
|
|
71
|
+
"pieChart": "pie",
|
|
72
|
+
"pie3DChart": "pie",
|
|
73
|
+
"doughnutChart": "doughnut",
|
|
74
|
+
"areaChart": "area",
|
|
75
|
+
"area3DChart": "area",
|
|
76
|
+
"scatterChart": "scatter",
|
|
77
|
+
"bubbleChart": "bubble",
|
|
78
|
+
"radarChart": "radar",
|
|
79
|
+
"stockChart": "stock",
|
|
80
|
+
"surfaceChart": "surface",
|
|
81
|
+
"surface3DChart": "surface",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function parseChartData(plotArea: Record<string, unknown>): { chartType: ChartType; series: ChartSeries[]; categories?: string[] } {
|
|
85
|
+
// Detect combo charts — multiple chart type children in plot area
|
|
86
|
+
const foundTypes: { key: string; type: ChartType; node: Record<string, unknown> }[] = [];
|
|
87
|
+
for (const [key, type] of Object.entries(CHART_TYPE_MAP)) {
|
|
88
|
+
const chartNode = plotArea[key];
|
|
89
|
+
if (!chartNode) continue;
|
|
90
|
+
let chartType = type;
|
|
91
|
+
const obj = chartNode as Record<string, unknown>;
|
|
92
|
+
if ((key === "c:barChart" || key === "barChart") &&
|
|
93
|
+
(getAttr(obj, "c:barDir", "val") === "bar" || getAttr(obj, "barDir", "val") === "bar")) {
|
|
94
|
+
chartType = "bar";
|
|
95
|
+
}
|
|
96
|
+
foundTypes.push({ key, type: chartType, node: obj });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (foundTypes.length === 0) {
|
|
100
|
+
return { chartType: "unknown", series: [] };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Single chart type
|
|
104
|
+
if (foundTypes.length === 1) {
|
|
105
|
+
const { type, node } = foundTypes[0];
|
|
106
|
+
const series = extractSeries(node);
|
|
107
|
+
const categories = extractCategories(node);
|
|
108
|
+
return { chartType: type, series, categories };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Combo chart — merge series from all types, tag each with its chart type
|
|
112
|
+
const allSeries: ChartSeries[] = [];
|
|
113
|
+
let categories: string[] | undefined;
|
|
114
|
+
for (const { type, node } of foundTypes) {
|
|
115
|
+
const series = extractSeries(node);
|
|
116
|
+
for (const s of series) {
|
|
117
|
+
s.seriesChartType = type;
|
|
118
|
+
}
|
|
119
|
+
allSeries.push(...series);
|
|
120
|
+
if (!categories) categories = extractCategories(node);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { chartType: "combo", series: allSeries, categories };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function extractSeries(chartNode: Record<string, unknown>): ChartSeries[] {
|
|
127
|
+
const serNodes = ensureArray(chartNode["c:ser"] ?? chartNode["ser"]);
|
|
128
|
+
const series: ChartSeries[] = [];
|
|
129
|
+
|
|
130
|
+
for (const ser of serNodes) {
|
|
131
|
+
if (!ser || typeof ser !== "object") continue;
|
|
132
|
+
const s = ser as Record<string, unknown>;
|
|
133
|
+
|
|
134
|
+
// Series name
|
|
135
|
+
const txNode = getObj(s, "c:tx") ?? getObj(s, "tx");
|
|
136
|
+
const name = extractSeriesName(txNode);
|
|
137
|
+
|
|
138
|
+
// Values
|
|
139
|
+
const valNode = getObj(s, "c:val") ?? getObj(s, "val")
|
|
140
|
+
?? getObj(s, "c:yVal") ?? getObj(s, "yVal");
|
|
141
|
+
const values = extractNumericValues(valNode);
|
|
142
|
+
|
|
143
|
+
// Bubble sizes
|
|
144
|
+
const bubbleSizeNode = getObj(s, "c:bubbleSize") ?? getObj(s, "bubbleSize");
|
|
145
|
+
const bubbleSizes = extractNumericValues(bubbleSizeNode);
|
|
146
|
+
|
|
147
|
+
// Color from shape properties
|
|
148
|
+
const spPr = getObj(s, "c:spPr") ?? getObj(s, "spPr");
|
|
149
|
+
const color = extractColor(spPr);
|
|
150
|
+
|
|
151
|
+
const entry: ChartSeries = { name, values, color };
|
|
152
|
+
if (bubbleSizes.length > 0) entry.bubbleSizes = bubbleSizes;
|
|
153
|
+
series.push(entry);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return series;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function extractSeriesName(txNode: unknown): string | undefined {
|
|
160
|
+
if (!txNode || typeof txNode !== "object") return undefined;
|
|
161
|
+
const tx = txNode as Record<string, unknown>;
|
|
162
|
+
const strRef = getObj(tx, "c:strRef") ?? getObj(tx, "strRef");
|
|
163
|
+
if (strRef) {
|
|
164
|
+
const cache = getObj(strRef as Record<string, unknown>, "c:strCache") ?? getObj(strRef as Record<string, unknown>, "strCache");
|
|
165
|
+
if (cache) {
|
|
166
|
+
const pt = ensureArray((cache as Record<string, unknown>)["c:pt"] ?? (cache as Record<string, unknown>)["pt"]);
|
|
167
|
+
if (pt[0] && typeof pt[0] === "object") {
|
|
168
|
+
const v = (pt[0] as Record<string, unknown>)["c:v"] ?? (pt[0] as Record<string, unknown>)["v"];
|
|
169
|
+
if (typeof v === "string") return v;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const v = getObj(tx, "c:v") ?? getObj(tx, "v");
|
|
174
|
+
if (typeof v === "string") return v;
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function extractNumericValues(valNode: unknown): number[] {
|
|
179
|
+
if (!valNode || typeof valNode !== "object") return [];
|
|
180
|
+
const val = valNode as Record<string, unknown>;
|
|
181
|
+
const numRef = getObj(val, "c:numRef") ?? getObj(val, "numRef");
|
|
182
|
+
const cache = numRef
|
|
183
|
+
? getObj(numRef as Record<string, unknown>, "c:numCache") ?? getObj(numRef as Record<string, unknown>, "numCache")
|
|
184
|
+
: getObj(val, "c:numLit") ?? getObj(val, "numLit");
|
|
185
|
+
|
|
186
|
+
if (!cache || typeof cache !== "object") return [];
|
|
187
|
+
const pts = ensureArray((cache as Record<string, unknown>)["c:pt"] ?? (cache as Record<string, unknown>)["pt"]);
|
|
188
|
+
const values: number[] = [];
|
|
189
|
+
for (const pt of pts) {
|
|
190
|
+
if (!pt || typeof pt !== "object") continue;
|
|
191
|
+
const v = (pt as Record<string, unknown>)["c:v"] ?? (pt as Record<string, unknown>)["v"];
|
|
192
|
+
const num = typeof v === "string" ? parseFloat(v) : typeof v === "number" ? v : NaN;
|
|
193
|
+
values.push(isNaN(num) ? 0 : num);
|
|
194
|
+
}
|
|
195
|
+
return values;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function extractCategories(chartNode: Record<string, unknown>): string[] | undefined {
|
|
199
|
+
const serNodes = ensureArray(chartNode["c:ser"] ?? chartNode["ser"]);
|
|
200
|
+
for (const ser of serNodes) {
|
|
201
|
+
if (!ser || typeof ser !== "object") continue;
|
|
202
|
+
const s = ser as Record<string, unknown>;
|
|
203
|
+
const catNode = getObj(s, "c:cat") ?? getObj(s, "cat");
|
|
204
|
+
if (!catNode || typeof catNode !== "object") continue;
|
|
205
|
+
const cat = catNode as Record<string, unknown>;
|
|
206
|
+
const strRef = getObj(cat, "c:strRef") ?? getObj(cat, "strRef");
|
|
207
|
+
if (strRef && typeof strRef === "object") {
|
|
208
|
+
const cache = getObj(strRef as Record<string, unknown>, "c:strCache")
|
|
209
|
+
?? getObj(strRef as Record<string, unknown>, "strCache");
|
|
210
|
+
if (cache && typeof cache === "object") {
|
|
211
|
+
const pts = ensureArray((cache as Record<string, unknown>)["c:pt"] ?? (cache as Record<string, unknown>)["pt"]);
|
|
212
|
+
const cats: string[] = [];
|
|
213
|
+
for (const pt of pts) {
|
|
214
|
+
if (!pt || typeof pt !== "object") continue;
|
|
215
|
+
const v = (pt as Record<string, unknown>)["c:v"] ?? (pt as Record<string, unknown>)["v"];
|
|
216
|
+
cats.push(typeof v === "string" ? v : String(v ?? ""));
|
|
217
|
+
}
|
|
218
|
+
if (cats.length > 0) return cats;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function extractColor(spPr: unknown): string | undefined {
|
|
226
|
+
if (!spPr || typeof spPr !== "object") return undefined;
|
|
227
|
+
const obj = spPr as Record<string, unknown>;
|
|
228
|
+
const solidFill = getObj(obj, "a:solidFill") ?? getObj(obj, "solidFill");
|
|
229
|
+
if (!solidFill || typeof solidFill !== "object") return undefined;
|
|
230
|
+
const srgb = getObj(solidFill as Record<string, unknown>, "a:srgbClr") ?? getObj(solidFill as Record<string, unknown>, "srgbClr");
|
|
231
|
+
if (srgb && typeof srgb === "object") {
|
|
232
|
+
const val = (srgb as Record<string, unknown>)["@_val"];
|
|
233
|
+
if (typeof val === "string") return `#${val}`;
|
|
234
|
+
}
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// =============================================================================
|
|
239
|
+
// Title
|
|
240
|
+
// =============================================================================
|
|
241
|
+
|
|
242
|
+
function extractTitle(titleNode: unknown): string | undefined {
|
|
243
|
+
if (!titleNode || typeof titleNode !== "object") return undefined;
|
|
244
|
+
const tx = getObj(titleNode as Record<string, unknown>, "c:tx") ?? getObj(titleNode as Record<string, unknown>, "tx");
|
|
245
|
+
if (!tx || typeof tx !== "object") return undefined;
|
|
246
|
+
const rich = getObj(tx as Record<string, unknown>, "c:rich") ?? getObj(tx as Record<string, unknown>, "rich");
|
|
247
|
+
if (!rich || typeof rich !== "object") return undefined;
|
|
248
|
+
const paras = ensureArray((rich as Record<string, unknown>)["a:p"] ?? (rich as Record<string, unknown>)["p"]);
|
|
249
|
+
const texts: string[] = [];
|
|
250
|
+
for (const para of paras) {
|
|
251
|
+
if (!para || typeof para !== "object") continue;
|
|
252
|
+
const runs = ensureArray((para as Record<string, unknown>)["a:r"] ?? (para as Record<string, unknown>)["r"]);
|
|
253
|
+
for (const run of runs) {
|
|
254
|
+
if (!run || typeof run !== "object") continue;
|
|
255
|
+
const t = (run as Record<string, unknown>)["a:t"] ?? (run as Record<string, unknown>)["t"];
|
|
256
|
+
if (typeof t === "string") texts.push(t);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return texts.length > 0 ? texts.join("") : undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// =============================================================================
|
|
263
|
+
// Legend
|
|
264
|
+
// =============================================================================
|
|
265
|
+
|
|
266
|
+
function extractLegendPosition(legendNode: unknown): ParsedChart["legendPosition"] {
|
|
267
|
+
if (!legendNode || typeof legendNode !== "object") return undefined;
|
|
268
|
+
const pos = getObj(legendNode as Record<string, unknown>, "c:legendPos") ?? getObj(legendNode as Record<string, unknown>, "legendPos");
|
|
269
|
+
if (!pos || typeof pos !== "object") return undefined;
|
|
270
|
+
const val = (pos as Record<string, unknown>)["@_val"];
|
|
271
|
+
const map: Record<string, ParsedChart["legendPosition"]> = { t: "top", b: "bottom", l: "left", r: "right" };
|
|
272
|
+
return typeof val === "string" ? map[val] ?? "right" : "right";
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// =============================================================================
|
|
276
|
+
// Axes
|
|
277
|
+
// =============================================================================
|
|
278
|
+
|
|
279
|
+
function parseAxes(plotArea: Record<string, unknown>): ChartAxis[] {
|
|
280
|
+
const axes: ChartAxis[] = [];
|
|
281
|
+
|
|
282
|
+
const catAx = ensureArray(plotArea["c:catAx"] ?? plotArea["catAx"]);
|
|
283
|
+
for (const ax of catAx) {
|
|
284
|
+
if (!ax || typeof ax !== "object") continue;
|
|
285
|
+
axes.push(parseAxis(ax as Record<string, unknown>, "category"));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const valAx = ensureArray(plotArea["c:valAx"] ?? plotArea["valAx"]);
|
|
289
|
+
for (const ax of valAx) {
|
|
290
|
+
if (!ax || typeof ax !== "object") continue;
|
|
291
|
+
axes.push(parseAxis(ax as Record<string, unknown>, "value"));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const dateAx = ensureArray(plotArea["c:dateAx"] ?? plotArea["dateAx"]);
|
|
295
|
+
for (const ax of dateAx) {
|
|
296
|
+
if (!ax || typeof ax !== "object") continue;
|
|
297
|
+
axes.push(parseAxis(ax as Record<string, unknown>, "date"));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return axes;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function parseAxis(ax: Record<string, unknown>, type: ChartAxis["type"]): ChartAxis {
|
|
304
|
+
const titleNode = getObj(ax, "c:title") ?? getObj(ax, "title");
|
|
305
|
+
const title = extractTitle(titleNode);
|
|
306
|
+
|
|
307
|
+
const numFmtNode = getObj(ax, "c:numFmt") ?? getObj(ax, "numFmt");
|
|
308
|
+
const numFmt = numFmtNode ? getAttr(numFmtNode as Record<string, unknown>, "", "formatCode") ?? (numFmtNode as Record<string, unknown>)["@_formatCode"] as string | undefined : undefined;
|
|
309
|
+
|
|
310
|
+
const scalingNode = getObj(ax, "c:scaling") ?? getObj(ax, "scaling");
|
|
311
|
+
let min: number | undefined;
|
|
312
|
+
let max: number | undefined;
|
|
313
|
+
if (scalingNode && typeof scalingNode === "object") {
|
|
314
|
+
const minNode = getObj(scalingNode as Record<string, unknown>, "c:min") ?? getObj(scalingNode as Record<string, unknown>, "min");
|
|
315
|
+
if (minNode && typeof minNode === "object") {
|
|
316
|
+
const v = (minNode as Record<string, unknown>)["@_val"];
|
|
317
|
+
if (typeof v === "string") min = parseFloat(v);
|
|
318
|
+
}
|
|
319
|
+
const maxNode = getObj(scalingNode as Record<string, unknown>, "c:max") ?? getObj(scalingNode as Record<string, unknown>, "max");
|
|
320
|
+
if (maxNode && typeof maxNode === "object") {
|
|
321
|
+
const v = (maxNode as Record<string, unknown>)["@_val"];
|
|
322
|
+
if (typeof v === "string") max = parseFloat(v);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return { type, title, numFmt, min, max };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// =============================================================================
|
|
330
|
+
// Helpers
|
|
331
|
+
// =============================================================================
|
|
332
|
+
|
|
333
|
+
function getObj(parent: Record<string, unknown>, key: string): unknown {
|
|
334
|
+
return parent[key];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function getAttr(obj: Record<string, unknown>, childKey: string, attr: string): string | undefined {
|
|
338
|
+
const child = childKey ? obj[childKey] : obj;
|
|
339
|
+
if (!child || typeof child !== "object") return undefined;
|
|
340
|
+
const val = (child as Record<string, unknown>)[`@_${attr}`];
|
|
341
|
+
return typeof val === "string" ? val : undefined;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function ensureArray(val: unknown): unknown[] {
|
|
345
|
+
if (val === undefined || val === null) return [];
|
|
346
|
+
return Array.isArray(val) ? val : [val];
|
|
347
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Chart & Drawing Types
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Types for parsed chart, sparkline, table, and drawing objects from OOXML.
|
|
6
|
+
// These are read from xl/charts/*.xml, xl/tables/*.xml, and
|
|
7
|
+
// xl/drawings/*.xml during workbook loading.
|
|
8
|
+
// =============================================================================
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Charts
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
export interface ParsedChart {
|
|
15
|
+
/** Chart title text. */
|
|
16
|
+
title?: string;
|
|
17
|
+
/** Chart type (bar, line, pie, scatter, area, etc.). */
|
|
18
|
+
chartType: ChartType;
|
|
19
|
+
/** Data series. */
|
|
20
|
+
series: ChartSeries[];
|
|
21
|
+
/** Category labels (X-axis or pie labels). */
|
|
22
|
+
categories?: string[];
|
|
23
|
+
/** Axis configuration. */
|
|
24
|
+
axes?: ChartAxis[];
|
|
25
|
+
/** Legend position. */
|
|
26
|
+
legendPosition?: "top" | "bottom" | "left" | "right" | "none";
|
|
27
|
+
/** Anchor position in the sheet (row/col for top-left and bottom-right). */
|
|
28
|
+
anchor: DrawingAnchor;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ChartType =
|
|
32
|
+
| "bar"
|
|
33
|
+
| "col"
|
|
34
|
+
| "line"
|
|
35
|
+
| "pie"
|
|
36
|
+
| "doughnut"
|
|
37
|
+
| "area"
|
|
38
|
+
| "scatter"
|
|
39
|
+
| "bubble"
|
|
40
|
+
| "radar"
|
|
41
|
+
| "stock"
|
|
42
|
+
| "surface"
|
|
43
|
+
| "combo"
|
|
44
|
+
| "unknown";
|
|
45
|
+
|
|
46
|
+
export interface ChartSeries {
|
|
47
|
+
/** Series name/label. */
|
|
48
|
+
name?: string;
|
|
49
|
+
/** Data values. */
|
|
50
|
+
values: number[];
|
|
51
|
+
/** Series color (resolved from theme/style). */
|
|
52
|
+
color?: string;
|
|
53
|
+
/** Bubble sizes (for bubble charts). */
|
|
54
|
+
bubbleSizes?: number[];
|
|
55
|
+
/** Per-series chart type (for combo charts). */
|
|
56
|
+
seriesChartType?: ChartType;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ChartAxis {
|
|
60
|
+
/** Axis type. */
|
|
61
|
+
type: "category" | "value" | "date" | "series";
|
|
62
|
+
/** Axis title. */
|
|
63
|
+
title?: string;
|
|
64
|
+
/** Number format for axis labels. */
|
|
65
|
+
numFmt?: string;
|
|
66
|
+
/** Min/max bounds. */
|
|
67
|
+
min?: number;
|
|
68
|
+
max?: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// =============================================================================
|
|
72
|
+
// Sparklines
|
|
73
|
+
// =============================================================================
|
|
74
|
+
|
|
75
|
+
export interface ParsedSparklineGroup {
|
|
76
|
+
/** Sparkline type. */
|
|
77
|
+
type: "line" | "column" | "stacked";
|
|
78
|
+
/** Individual sparklines in the group. */
|
|
79
|
+
sparklines: ParsedSparkline[];
|
|
80
|
+
/** Series color. */
|
|
81
|
+
color?: string;
|
|
82
|
+
/** Min/max axis settings. */
|
|
83
|
+
minAxis?: "individual" | "group" | "custom";
|
|
84
|
+
maxAxis?: "individual" | "group" | "custom";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ParsedSparkline {
|
|
88
|
+
/** Data range (e.g., "Sheet1!A1:A10"). */
|
|
89
|
+
dataRange: string;
|
|
90
|
+
/** Cell where the sparkline is rendered. */
|
|
91
|
+
locationRef: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// =============================================================================
|
|
95
|
+
// Structured Tables
|
|
96
|
+
// =============================================================================
|
|
97
|
+
|
|
98
|
+
export interface ParsedTable {
|
|
99
|
+
/** Table name (used in structured references like TableName[Column]). */
|
|
100
|
+
name: string;
|
|
101
|
+
/** Display name. */
|
|
102
|
+
displayName: string;
|
|
103
|
+
/** Cell range (e.g., "A1:D10"). */
|
|
104
|
+
ref: string;
|
|
105
|
+
/** Column definitions. */
|
|
106
|
+
columns: TableColumn[];
|
|
107
|
+
/** Whether the table has a header row. */
|
|
108
|
+
headerRow: boolean;
|
|
109
|
+
/** Whether the table has a totals row. */
|
|
110
|
+
totalsRow: boolean;
|
|
111
|
+
/** Table style name (e.g., "TableStyleMedium2"). */
|
|
112
|
+
styleName?: string;
|
|
113
|
+
/** Auto-filter enabled. */
|
|
114
|
+
autoFilter: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface TableColumn {
|
|
118
|
+
/** Column ID (1-based within the table). */
|
|
119
|
+
id: number;
|
|
120
|
+
/** Column header text. */
|
|
121
|
+
name: string;
|
|
122
|
+
/** Totals row function (sum, count, average, etc.). */
|
|
123
|
+
totalsFunction?: string;
|
|
124
|
+
/** Totals row formula. */
|
|
125
|
+
totalsFormula?: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// =============================================================================
|
|
129
|
+
// Drawings (shapes, text boxes, etc.)
|
|
130
|
+
// =============================================================================
|
|
131
|
+
|
|
132
|
+
export interface ParsedDrawing {
|
|
133
|
+
/** Drawing type. */
|
|
134
|
+
type: "shape" | "textbox" | "connector" | "group" | "unknown";
|
|
135
|
+
/** Shape geometry preset (rect, roundRect, ellipse, etc.). */
|
|
136
|
+
geometry?: string;
|
|
137
|
+
/** Shape text content. */
|
|
138
|
+
text?: string;
|
|
139
|
+
/** Fill color. */
|
|
140
|
+
fillColor?: string;
|
|
141
|
+
/** Outline color. */
|
|
142
|
+
outlineColor?: string;
|
|
143
|
+
/** Outline width in points. */
|
|
144
|
+
outlineWidth?: number;
|
|
145
|
+
/** Anchor position. */
|
|
146
|
+
anchor: DrawingAnchor;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// =============================================================================
|
|
150
|
+
// Drawing anchors — position relative to cells
|
|
151
|
+
// =============================================================================
|
|
152
|
+
|
|
153
|
+
export interface DrawingAnchor {
|
|
154
|
+
/** Top-left cell. */
|
|
155
|
+
from: { row: number; col: number; rowOffset?: number; colOffset?: number };
|
|
156
|
+
/** Bottom-right cell. */
|
|
157
|
+
to: { row: number; col: number; rowOffset?: number; colOffset?: number };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// =============================================================================
|
|
161
|
+
// Auto-filters
|
|
162
|
+
// =============================================================================
|
|
163
|
+
|
|
164
|
+
export interface AutoFilterRange {
|
|
165
|
+
/** Range reference (e.g., "A1:D10"). */
|
|
166
|
+
ref: string;
|
|
167
|
+
/** Per-column filter criteria. */
|
|
168
|
+
columns: AutoFilterColumn[];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface AutoFilterColumn {
|
|
172
|
+
/** Column index within the filter range (0-based). */
|
|
173
|
+
colIndex: number;
|
|
174
|
+
/** Filter values (for value-based filters). */
|
|
175
|
+
filterValues?: string[];
|
|
176
|
+
/** Custom filter condition. */
|
|
177
|
+
customFilter?: { operator: string; value: string };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// =============================================================================
|
|
181
|
+
// Data Validation
|
|
182
|
+
// =============================================================================
|
|
183
|
+
|
|
184
|
+
export interface ParsedDataValidation {
|
|
185
|
+
/** Cell range this validation applies to. */
|
|
186
|
+
ref: string;
|
|
187
|
+
/** Validation type. */
|
|
188
|
+
type: "list" | "whole" | "decimal" | "date" | "time" | "textLength" | "custom" | "none";
|
|
189
|
+
/** Comparison operator. */
|
|
190
|
+
operator?: "between" | "notBetween" | "equal" | "notEqual" | "greaterThan" | "lessThan" | "greaterThanOrEqual" | "lessThanOrEqual";
|
|
191
|
+
/** Formula for value 1. */
|
|
192
|
+
formula1?: string;
|
|
193
|
+
/** Formula for value 2 (for between/notBetween). */
|
|
194
|
+
formula2?: string;
|
|
195
|
+
/** Whether to show dropdown for list validation. */
|
|
196
|
+
showDropdown: boolean;
|
|
197
|
+
/** Error alert style. */
|
|
198
|
+
errorStyle?: "stop" | "warning" | "information";
|
|
199
|
+
/** Error alert title. */
|
|
200
|
+
errorTitle?: string;
|
|
201
|
+
/** Error alert message. */
|
|
202
|
+
errorMessage?: string;
|
|
203
|
+
/** Input prompt title. */
|
|
204
|
+
promptTitle?: string;
|
|
205
|
+
/** Input prompt message. */
|
|
206
|
+
promptMessage?: string;
|
|
207
|
+
}
|