@niicojs/excel 0.3.1 → 0.3.3
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/LICENSE +20 -20
- package/README.md +585 -585
- package/dist/index.cjs +1004 -438
- package/dist/index.d.cts +53 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1005 -439
- package/package.json +3 -3
- package/src/cell.ts +20 -6
- package/src/index.ts +45 -45
- package/src/pivot-cache.ts +501 -300
- package/src/pivot-table.ts +680 -684
- package/src/range.ts +154 -154
- package/src/shared-strings.ts +185 -178
- package/src/styles.ts +819 -819
- package/src/table.ts +386 -386
- package/src/types.ts +54 -36
- package/src/utils/address.ts +121 -121
- package/src/utils/format.ts +356 -0
- package/src/utils/xml.ts +153 -140
- package/src/utils/zip.ts +29 -5
- package/src/workbook.ts +1412 -1390
- package/src/worksheet.ts +85 -84
package/src/pivot-table.ts
CHANGED
|
@@ -1,684 +1,680 @@
|
|
|
1
|
-
import type { AggregationType, PivotFieldAxis, PivotFieldFilter, PivotSortOrder, PivotValueConfig } from './types';
|
|
2
|
-
import type { Styles } from './styles';
|
|
3
|
-
import { PivotCache } from './pivot-cache';
|
|
4
|
-
import { createElement, stringifyXml, XmlNode } from './utils/xml';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Internal structure for tracking field assignments
|
|
8
|
-
*/
|
|
9
|
-
interface FieldAssignment {
|
|
10
|
-
fieldName: string;
|
|
11
|
-
fieldIndex: number;
|
|
12
|
-
axis: PivotFieldAxis;
|
|
13
|
-
aggregation?: AggregationType;
|
|
14
|
-
displayName?: string;
|
|
15
|
-
numFmtId?: number;
|
|
16
|
-
sortOrder?: PivotSortOrder;
|
|
17
|
-
filter?: PivotFieldFilter;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Represents an Excel pivot table with a fluent API for configuration.
|
|
22
|
-
*/
|
|
23
|
-
export class PivotTable {
|
|
24
|
-
private _name: string;
|
|
25
|
-
private _cache: PivotCache;
|
|
26
|
-
private _targetSheet: string;
|
|
27
|
-
private _targetCell: string;
|
|
28
|
-
private _targetRow: number;
|
|
29
|
-
private _targetCol: number;
|
|
30
|
-
|
|
31
|
-
private _rowFields: FieldAssignment[] = [];
|
|
32
|
-
private _columnFields: FieldAssignment[] = [];
|
|
33
|
-
private _valueFields: FieldAssignment[] = [];
|
|
34
|
-
private _filterFields: FieldAssignment[] = [];
|
|
35
|
-
private _fieldAssignments: Map<number, FieldAssignment> = new Map();
|
|
36
|
-
|
|
37
|
-
private _pivotTableIndex: number;
|
|
38
|
-
private _cacheFileIndex: number;
|
|
39
|
-
private _styles: Styles | null = null;
|
|
40
|
-
|
|
41
|
-
constructor(
|
|
42
|
-
name: string,
|
|
43
|
-
cache: PivotCache,
|
|
44
|
-
targetSheet: string,
|
|
45
|
-
targetCell: string,
|
|
46
|
-
targetRow: number,
|
|
47
|
-
targetCol: number,
|
|
48
|
-
pivotTableIndex: number,
|
|
49
|
-
cacheFileIndex: number,
|
|
50
|
-
) {
|
|
51
|
-
this._name = name;
|
|
52
|
-
this._cache = cache;
|
|
53
|
-
this._targetSheet = targetSheet;
|
|
54
|
-
this._targetCell = targetCell;
|
|
55
|
-
this._targetRow = targetRow;
|
|
56
|
-
this._targetCol = targetCol;
|
|
57
|
-
this._pivotTableIndex = pivotTableIndex;
|
|
58
|
-
this._cacheFileIndex = cacheFileIndex;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Get the pivot table name
|
|
63
|
-
*/
|
|
64
|
-
get name(): string {
|
|
65
|
-
return this._name;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Get the target sheet name
|
|
70
|
-
*/
|
|
71
|
-
get targetSheet(): string {
|
|
72
|
-
return this._targetSheet;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Get the target cell address
|
|
77
|
-
*/
|
|
78
|
-
get targetCell(): string {
|
|
79
|
-
return this._targetCell;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Get the pivot cache
|
|
84
|
-
*/
|
|
85
|
-
get cache(): PivotCache {
|
|
86
|
-
return this._cache;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Get the pivot table index (for file naming)
|
|
91
|
-
*/
|
|
92
|
-
get index(): number {
|
|
93
|
-
return this._pivotTableIndex;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Get the pivot cache file index used for rels.
|
|
98
|
-
* @internal
|
|
99
|
-
*/
|
|
100
|
-
get cacheFileIndex(): number {
|
|
101
|
-
return this._cacheFileIndex;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Set the styles reference for number format resolution
|
|
106
|
-
* @internal
|
|
107
|
-
*/
|
|
108
|
-
setStyles(styles: Styles): this {
|
|
109
|
-
this._styles = styles;
|
|
110
|
-
return this;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Add a field to the row area
|
|
115
|
-
* @param fieldName - Name of the source field (column header)
|
|
116
|
-
*/
|
|
117
|
-
addRowField(fieldName: string): this {
|
|
118
|
-
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
119
|
-
if (fieldIndex < 0) {
|
|
120
|
-
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const assignment: FieldAssignment = {
|
|
124
|
-
fieldName,
|
|
125
|
-
fieldIndex,
|
|
126
|
-
axis: 'row',
|
|
127
|
-
};
|
|
128
|
-
this._rowFields.push(assignment);
|
|
129
|
-
this._fieldAssignments.set(fieldIndex, assignment);
|
|
130
|
-
|
|
131
|
-
return this;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Add a field to the column area
|
|
136
|
-
* @param fieldName - Name of the source field (column header)
|
|
137
|
-
*/
|
|
138
|
-
addColumnField(fieldName: string): this {
|
|
139
|
-
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
140
|
-
if (fieldIndex < 0) {
|
|
141
|
-
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const assignment: FieldAssignment = {
|
|
145
|
-
fieldName,
|
|
146
|
-
fieldIndex,
|
|
147
|
-
axis: 'column',
|
|
148
|
-
};
|
|
149
|
-
this._columnFields.push(assignment);
|
|
150
|
-
this._fieldAssignments.set(fieldIndex, assignment);
|
|
151
|
-
|
|
152
|
-
return this;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Add a field to the values area with aggregation.
|
|
157
|
-
*
|
|
158
|
-
* Supports two call signatures:
|
|
159
|
-
* - Positional: `addValueField(fieldName, aggregation?, displayName?, numberFormat?)`
|
|
160
|
-
* - Object: `addValueField({ field, aggregation?, name?, numberFormat? })`
|
|
161
|
-
*
|
|
162
|
-
* @example
|
|
163
|
-
* // Positional arguments
|
|
164
|
-
* pivot.addValueField('Sales', 'sum', 'Total Sales', '$#,##0.00');
|
|
165
|
-
*
|
|
166
|
-
* // Object form
|
|
167
|
-
* pivot.addValueField({ field: 'Sales', aggregation: 'sum', name: 'Total Sales', numberFormat: '$#,##0.00' });
|
|
168
|
-
*/
|
|
169
|
-
addValueField(config: PivotValueConfig): this;
|
|
170
|
-
addValueField(fieldName: string, aggregation?: AggregationType, displayName?: string, numberFormat?: string): this;
|
|
171
|
-
addValueField(
|
|
172
|
-
fieldNameOrConfig: string | PivotValueConfig,
|
|
173
|
-
aggregation: AggregationType = 'sum',
|
|
174
|
-
displayName?: string,
|
|
175
|
-
numberFormat?: string,
|
|
176
|
-
): this {
|
|
177
|
-
// Normalize arguments to a common form
|
|
178
|
-
let fieldName: string;
|
|
179
|
-
let agg: AggregationType;
|
|
180
|
-
let name: string | undefined;
|
|
181
|
-
let format: string | undefined;
|
|
182
|
-
|
|
183
|
-
if (typeof fieldNameOrConfig === 'object') {
|
|
184
|
-
fieldName = fieldNameOrConfig.field;
|
|
185
|
-
agg = fieldNameOrConfig.aggregation ?? 'sum';
|
|
186
|
-
name = fieldNameOrConfig.name;
|
|
187
|
-
format = fieldNameOrConfig.numberFormat;
|
|
188
|
-
} else {
|
|
189
|
-
fieldName = fieldNameOrConfig;
|
|
190
|
-
agg = aggregation;
|
|
191
|
-
name = displayName;
|
|
192
|
-
format = numberFormat;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
196
|
-
if (fieldIndex < 0) {
|
|
197
|
-
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const defaultName = `${agg.charAt(0).toUpperCase() + agg.slice(1)} of ${fieldName}`;
|
|
201
|
-
|
|
202
|
-
// Resolve numFmtId immediately if format is provided and styles are available
|
|
203
|
-
let numFmtId: number | undefined;
|
|
204
|
-
if (format && this._styles) {
|
|
205
|
-
numFmtId = this._styles.getOrCreateNumFmtId(format);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const assignment: FieldAssignment = {
|
|
209
|
-
fieldName,
|
|
210
|
-
fieldIndex,
|
|
211
|
-
axis: 'value',
|
|
212
|
-
aggregation: agg,
|
|
213
|
-
displayName: name || defaultName,
|
|
214
|
-
numFmtId,
|
|
215
|
-
};
|
|
216
|
-
this._valueFields.push(assignment);
|
|
217
|
-
this._fieldAssignments.set(fieldIndex, assignment);
|
|
218
|
-
|
|
219
|
-
return this;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Add a field to the filter (page) area
|
|
224
|
-
* @param fieldName - Name of the source field (column header)
|
|
225
|
-
*/
|
|
226
|
-
addFilterField(fieldName: string): this {
|
|
227
|
-
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
228
|
-
if (fieldIndex < 0) {
|
|
229
|
-
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const assignment: FieldAssignment = {
|
|
233
|
-
fieldName,
|
|
234
|
-
fieldIndex,
|
|
235
|
-
axis: 'filter',
|
|
236
|
-
};
|
|
237
|
-
this._filterFields.push(assignment);
|
|
238
|
-
this._fieldAssignments.set(fieldIndex, assignment);
|
|
239
|
-
|
|
240
|
-
return this;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Set a sort order for a row or column field
|
|
245
|
-
* @param fieldName - Name of the field to sort
|
|
246
|
-
* @param order - Sort order ('asc' or 'desc')
|
|
247
|
-
*/
|
|
248
|
-
sortField(fieldName: string, order: PivotSortOrder): this {
|
|
249
|
-
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
250
|
-
if (fieldIndex < 0) {
|
|
251
|
-
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const assignment = this._fieldAssignments.get(fieldIndex);
|
|
255
|
-
if (!assignment) {
|
|
256
|
-
throw new Error(`Field is not assigned to pivot table: ${fieldName}`);
|
|
257
|
-
}
|
|
258
|
-
if (assignment.axis !== 'row' && assignment.axis !== 'column') {
|
|
259
|
-
throw new Error(`Sort is only supported for row or column fields: ${fieldName}`);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
assignment.sortOrder = order;
|
|
263
|
-
return this;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Filter items for a row, column, or filter field
|
|
268
|
-
* @param fieldName - Name of the field to filter
|
|
269
|
-
* @param filter - Filter configuration with include or exclude list
|
|
270
|
-
*/
|
|
271
|
-
filterField(fieldName: string, filter: PivotFieldFilter): this {
|
|
272
|
-
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
273
|
-
if (fieldIndex < 0) {
|
|
274
|
-
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const assignment = this._fieldAssignments.get(fieldIndex);
|
|
278
|
-
if (!assignment) {
|
|
279
|
-
throw new Error(`Field is not assigned to pivot table: ${fieldName}`);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (filter.include && filter.exclude) {
|
|
283
|
-
throw new Error('Cannot use both include and exclude in the same filter');
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
assignment.filter = filter;
|
|
287
|
-
return this;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Generate the pivotTableDefinition XML
|
|
292
|
-
*/
|
|
293
|
-
toXml(): string {
|
|
294
|
-
const children: XmlNode[] = [];
|
|
295
|
-
|
|
296
|
-
// Calculate location (estimate based on fields)
|
|
297
|
-
const locationRef = this._calculateLocationRef();
|
|
298
|
-
|
|
299
|
-
// Calculate first data row/col offsets (1-based, relative to pivot table)
|
|
300
|
-
// firstHeaderRow: row offset of column headers (usually 1)
|
|
301
|
-
// firstDataRow: row offset where data starts (after filters and column headers)
|
|
302
|
-
// firstDataCol: column offset where data starts (after row labels)
|
|
303
|
-
const filterRowCount = this._filterFields.length > 0 ? this._filterFields.length + 1 : 0;
|
|
304
|
-
const headerRows = this._columnFields.length > 0 ? 1 : 0;
|
|
305
|
-
const firstDataRow = filterRowCount + headerRows + 1;
|
|
306
|
-
const firstDataCol = this._rowFields.length > 0 ? this._rowFields.length : 1;
|
|
307
|
-
|
|
308
|
-
const locationNode = createElement(
|
|
309
|
-
'location',
|
|
310
|
-
{
|
|
311
|
-
ref: locationRef,
|
|
312
|
-
firstHeaderRow: String(filterRowCount + 1),
|
|
313
|
-
firstDataRow: String(firstDataRow),
|
|
314
|
-
firstDataCol: String(firstDataCol),
|
|
315
|
-
},
|
|
316
|
-
[],
|
|
317
|
-
);
|
|
318
|
-
children.push(locationNode);
|
|
319
|
-
|
|
320
|
-
// Build pivotFields (one per source field)
|
|
321
|
-
const pivotFieldNodes: XmlNode[] = [];
|
|
322
|
-
for (const cacheField of this._cache.fields) {
|
|
323
|
-
const fieldNode = this._buildPivotFieldNode(cacheField.index);
|
|
324
|
-
pivotFieldNodes.push(fieldNode);
|
|
325
|
-
}
|
|
326
|
-
children.push(createElement('pivotFields', { count: String(pivotFieldNodes.length) }, pivotFieldNodes));
|
|
327
|
-
|
|
328
|
-
// Row fields
|
|
329
|
-
if (this._rowFields.length > 0) {
|
|
330
|
-
const rowFieldNodes = this._rowFields.map((f) => createElement('field', { x: String(f.fieldIndex) }, []));
|
|
331
|
-
children.push(createElement('rowFields', { count: String(rowFieldNodes.length) }, rowFieldNodes));
|
|
332
|
-
|
|
333
|
-
// Row items
|
|
334
|
-
const rowItemNodes = this._buildRowItems();
|
|
335
|
-
children.push(createElement('rowItems', { count: String(rowItemNodes.length) }, rowItemNodes));
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Column fields
|
|
339
|
-
if (this._columnFields.length > 0) {
|
|
340
|
-
const colFieldNodes = this._columnFields.map((f) => createElement('field', { x: String(f.fieldIndex) }, []));
|
|
341
|
-
// If we have multiple value fields, add -2 to indicate where "Values" header goes
|
|
342
|
-
if (this._valueFields.length > 1) {
|
|
343
|
-
colFieldNodes.push(createElement('field', { x: '-2' }, []));
|
|
344
|
-
}
|
|
345
|
-
children.push(createElement('colFields', { count: String(colFieldNodes.length) }, colFieldNodes));
|
|
346
|
-
|
|
347
|
-
// Column items - need to account for multiple value fields
|
|
348
|
-
const colItemNodes = this._buildColItems();
|
|
349
|
-
children.push(createElement('colItems', { count: String(colItemNodes.length) }, colItemNodes));
|
|
350
|
-
} else if (this._valueFields.length > 1) {
|
|
351
|
-
// If no column fields but we have multiple values, need colFields with -2 (data field indicator)
|
|
352
|
-
children.push(createElement('colFields', { count: '1' }, [createElement('field', { x: '-2' }, [])]));
|
|
353
|
-
|
|
354
|
-
// Column items for each value field
|
|
355
|
-
const colItemNodes: XmlNode[] = [];
|
|
356
|
-
for (let i = 0; i < this._valueFields.length; i++) {
|
|
357
|
-
colItemNodes.push(createElement('i', {}, [createElement('x', i === 0 ? {} : { v: String(i) }, [])]));
|
|
358
|
-
}
|
|
359
|
-
children.push(createElement('colItems', { count: String(colItemNodes.length) }, colItemNodes));
|
|
360
|
-
} else if (this._valueFields.length === 1) {
|
|
361
|
-
// Single value field - just add a single column item
|
|
362
|
-
children.push(createElement('colItems', { count: '1' }, [createElement('i', {}, [])]));
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Page (filter) fields
|
|
366
|
-
if (this._filterFields.length > 0) {
|
|
367
|
-
const pageFieldNodes = this._filterFields.map((f) =>
|
|
368
|
-
createElement('pageField', { fld: String(f.fieldIndex), hier: '-1' }, []),
|
|
369
|
-
);
|
|
370
|
-
children.push(createElement('pageFields', { count: String(pageFieldNodes.length) }, pageFieldNodes));
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Data fields (values)
|
|
374
|
-
if (this._valueFields.length > 0) {
|
|
375
|
-
const dataFieldNodes = this._valueFields.map((f) => {
|
|
376
|
-
const attrs: Record<string, string> = {
|
|
377
|
-
name: f.displayName || f.fieldName,
|
|
378
|
-
fld: String(f.fieldIndex),
|
|
379
|
-
baseField: '0',
|
|
380
|
-
baseItem: '0',
|
|
381
|
-
subtotal: f.aggregation || 'sum',
|
|
382
|
-
};
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
const
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
return result;
|
|
683
|
-
}
|
|
684
|
-
}
|
|
1
|
+
import type { AggregationType, PivotFieldAxis, PivotFieldFilter, PivotSortOrder, PivotValueConfig } from './types';
|
|
2
|
+
import type { Styles } from './styles';
|
|
3
|
+
import { PivotCache } from './pivot-cache';
|
|
4
|
+
import { createElement, stringifyXml, XmlNode } from './utils/xml';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal structure for tracking field assignments
|
|
8
|
+
*/
|
|
9
|
+
interface FieldAssignment {
|
|
10
|
+
fieldName: string;
|
|
11
|
+
fieldIndex: number;
|
|
12
|
+
axis: PivotFieldAxis;
|
|
13
|
+
aggregation?: AggregationType;
|
|
14
|
+
displayName?: string;
|
|
15
|
+
numFmtId?: number;
|
|
16
|
+
sortOrder?: PivotSortOrder;
|
|
17
|
+
filter?: PivotFieldFilter;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Represents an Excel pivot table with a fluent API for configuration.
|
|
22
|
+
*/
|
|
23
|
+
export class PivotTable {
|
|
24
|
+
private _name: string;
|
|
25
|
+
private _cache: PivotCache;
|
|
26
|
+
private _targetSheet: string;
|
|
27
|
+
private _targetCell: string;
|
|
28
|
+
private _targetRow: number;
|
|
29
|
+
private _targetCol: number;
|
|
30
|
+
|
|
31
|
+
private _rowFields: FieldAssignment[] = [];
|
|
32
|
+
private _columnFields: FieldAssignment[] = [];
|
|
33
|
+
private _valueFields: FieldAssignment[] = [];
|
|
34
|
+
private _filterFields: FieldAssignment[] = [];
|
|
35
|
+
private _fieldAssignments: Map<number, FieldAssignment> = new Map();
|
|
36
|
+
|
|
37
|
+
private _pivotTableIndex: number;
|
|
38
|
+
private _cacheFileIndex: number;
|
|
39
|
+
private _styles: Styles | null = null;
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
name: string,
|
|
43
|
+
cache: PivotCache,
|
|
44
|
+
targetSheet: string,
|
|
45
|
+
targetCell: string,
|
|
46
|
+
targetRow: number,
|
|
47
|
+
targetCol: number,
|
|
48
|
+
pivotTableIndex: number,
|
|
49
|
+
cacheFileIndex: number,
|
|
50
|
+
) {
|
|
51
|
+
this._name = name;
|
|
52
|
+
this._cache = cache;
|
|
53
|
+
this._targetSheet = targetSheet;
|
|
54
|
+
this._targetCell = targetCell;
|
|
55
|
+
this._targetRow = targetRow;
|
|
56
|
+
this._targetCol = targetCol;
|
|
57
|
+
this._pivotTableIndex = pivotTableIndex;
|
|
58
|
+
this._cacheFileIndex = cacheFileIndex;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get the pivot table name
|
|
63
|
+
*/
|
|
64
|
+
get name(): string {
|
|
65
|
+
return this._name;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get the target sheet name
|
|
70
|
+
*/
|
|
71
|
+
get targetSheet(): string {
|
|
72
|
+
return this._targetSheet;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get the target cell address
|
|
77
|
+
*/
|
|
78
|
+
get targetCell(): string {
|
|
79
|
+
return this._targetCell;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get the pivot cache
|
|
84
|
+
*/
|
|
85
|
+
get cache(): PivotCache {
|
|
86
|
+
return this._cache;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get the pivot table index (for file naming)
|
|
91
|
+
*/
|
|
92
|
+
get index(): number {
|
|
93
|
+
return this._pivotTableIndex;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get the pivot cache file index used for rels.
|
|
98
|
+
* @internal
|
|
99
|
+
*/
|
|
100
|
+
get cacheFileIndex(): number {
|
|
101
|
+
return this._cacheFileIndex;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Set the styles reference for number format resolution
|
|
106
|
+
* @internal
|
|
107
|
+
*/
|
|
108
|
+
setStyles(styles: Styles): this {
|
|
109
|
+
this._styles = styles;
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Add a field to the row area
|
|
115
|
+
* @param fieldName - Name of the source field (column header)
|
|
116
|
+
*/
|
|
117
|
+
addRowField(fieldName: string): this {
|
|
118
|
+
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
119
|
+
if (fieldIndex < 0) {
|
|
120
|
+
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const assignment: FieldAssignment = {
|
|
124
|
+
fieldName,
|
|
125
|
+
fieldIndex,
|
|
126
|
+
axis: 'row',
|
|
127
|
+
};
|
|
128
|
+
this._rowFields.push(assignment);
|
|
129
|
+
this._fieldAssignments.set(fieldIndex, assignment);
|
|
130
|
+
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Add a field to the column area
|
|
136
|
+
* @param fieldName - Name of the source field (column header)
|
|
137
|
+
*/
|
|
138
|
+
addColumnField(fieldName: string): this {
|
|
139
|
+
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
140
|
+
if (fieldIndex < 0) {
|
|
141
|
+
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const assignment: FieldAssignment = {
|
|
145
|
+
fieldName,
|
|
146
|
+
fieldIndex,
|
|
147
|
+
axis: 'column',
|
|
148
|
+
};
|
|
149
|
+
this._columnFields.push(assignment);
|
|
150
|
+
this._fieldAssignments.set(fieldIndex, assignment);
|
|
151
|
+
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Add a field to the values area with aggregation.
|
|
157
|
+
*
|
|
158
|
+
* Supports two call signatures:
|
|
159
|
+
* - Positional: `addValueField(fieldName, aggregation?, displayName?, numberFormat?)`
|
|
160
|
+
* - Object: `addValueField({ field, aggregation?, name?, numberFormat? })`
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* // Positional arguments
|
|
164
|
+
* pivot.addValueField('Sales', 'sum', 'Total Sales', '$#,##0.00');
|
|
165
|
+
*
|
|
166
|
+
* // Object form
|
|
167
|
+
* pivot.addValueField({ field: 'Sales', aggregation: 'sum', name: 'Total Sales', numberFormat: '$#,##0.00' });
|
|
168
|
+
*/
|
|
169
|
+
addValueField(config: PivotValueConfig): this;
|
|
170
|
+
addValueField(fieldName: string, aggregation?: AggregationType, displayName?: string, numberFormat?: string): this;
|
|
171
|
+
addValueField(
|
|
172
|
+
fieldNameOrConfig: string | PivotValueConfig,
|
|
173
|
+
aggregation: AggregationType = 'sum',
|
|
174
|
+
displayName?: string,
|
|
175
|
+
numberFormat?: string,
|
|
176
|
+
): this {
|
|
177
|
+
// Normalize arguments to a common form
|
|
178
|
+
let fieldName: string;
|
|
179
|
+
let agg: AggregationType;
|
|
180
|
+
let name: string | undefined;
|
|
181
|
+
let format: string | undefined;
|
|
182
|
+
|
|
183
|
+
if (typeof fieldNameOrConfig === 'object') {
|
|
184
|
+
fieldName = fieldNameOrConfig.field;
|
|
185
|
+
agg = fieldNameOrConfig.aggregation ?? 'sum';
|
|
186
|
+
name = fieldNameOrConfig.name;
|
|
187
|
+
format = fieldNameOrConfig.numberFormat;
|
|
188
|
+
} else {
|
|
189
|
+
fieldName = fieldNameOrConfig;
|
|
190
|
+
agg = aggregation;
|
|
191
|
+
name = displayName;
|
|
192
|
+
format = numberFormat;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
196
|
+
if (fieldIndex < 0) {
|
|
197
|
+
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const defaultName = `${agg.charAt(0).toUpperCase() + agg.slice(1)} of ${fieldName}`;
|
|
201
|
+
|
|
202
|
+
// Resolve numFmtId immediately if format is provided and styles are available
|
|
203
|
+
let numFmtId: number | undefined;
|
|
204
|
+
if (format && this._styles) {
|
|
205
|
+
numFmtId = this._styles.getOrCreateNumFmtId(format);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const assignment: FieldAssignment = {
|
|
209
|
+
fieldName,
|
|
210
|
+
fieldIndex,
|
|
211
|
+
axis: 'value',
|
|
212
|
+
aggregation: agg,
|
|
213
|
+
displayName: name || defaultName,
|
|
214
|
+
numFmtId,
|
|
215
|
+
};
|
|
216
|
+
this._valueFields.push(assignment);
|
|
217
|
+
this._fieldAssignments.set(fieldIndex, assignment);
|
|
218
|
+
|
|
219
|
+
return this;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Add a field to the filter (page) area
|
|
224
|
+
* @param fieldName - Name of the source field (column header)
|
|
225
|
+
*/
|
|
226
|
+
addFilterField(fieldName: string): this {
|
|
227
|
+
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
228
|
+
if (fieldIndex < 0) {
|
|
229
|
+
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const assignment: FieldAssignment = {
|
|
233
|
+
fieldName,
|
|
234
|
+
fieldIndex,
|
|
235
|
+
axis: 'filter',
|
|
236
|
+
};
|
|
237
|
+
this._filterFields.push(assignment);
|
|
238
|
+
this._fieldAssignments.set(fieldIndex, assignment);
|
|
239
|
+
|
|
240
|
+
return this;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Set a sort order for a row or column field
|
|
245
|
+
* @param fieldName - Name of the field to sort
|
|
246
|
+
* @param order - Sort order ('asc' or 'desc')
|
|
247
|
+
*/
|
|
248
|
+
sortField(fieldName: string, order: PivotSortOrder): this {
|
|
249
|
+
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
250
|
+
if (fieldIndex < 0) {
|
|
251
|
+
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const assignment = this._fieldAssignments.get(fieldIndex);
|
|
255
|
+
if (!assignment) {
|
|
256
|
+
throw new Error(`Field is not assigned to pivot table: ${fieldName}`);
|
|
257
|
+
}
|
|
258
|
+
if (assignment.axis !== 'row' && assignment.axis !== 'column') {
|
|
259
|
+
throw new Error(`Sort is only supported for row or column fields: ${fieldName}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
assignment.sortOrder = order;
|
|
263
|
+
return this;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Filter items for a row, column, or filter field
|
|
268
|
+
* @param fieldName - Name of the field to filter
|
|
269
|
+
* @param filter - Filter configuration with include or exclude list
|
|
270
|
+
*/
|
|
271
|
+
filterField(fieldName: string, filter: PivotFieldFilter): this {
|
|
272
|
+
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
273
|
+
if (fieldIndex < 0) {
|
|
274
|
+
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const assignment = this._fieldAssignments.get(fieldIndex);
|
|
278
|
+
if (!assignment) {
|
|
279
|
+
throw new Error(`Field is not assigned to pivot table: ${fieldName}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (filter.include && filter.exclude) {
|
|
283
|
+
throw new Error('Cannot use both include and exclude in the same filter');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
assignment.filter = filter;
|
|
287
|
+
return this;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Generate the pivotTableDefinition XML
|
|
292
|
+
*/
|
|
293
|
+
toXml(): string {
|
|
294
|
+
const children: XmlNode[] = [];
|
|
295
|
+
|
|
296
|
+
// Calculate location (estimate based on fields)
|
|
297
|
+
const locationRef = this._calculateLocationRef();
|
|
298
|
+
|
|
299
|
+
// Calculate first data row/col offsets (1-based, relative to pivot table)
|
|
300
|
+
// firstHeaderRow: row offset of column headers (usually 1)
|
|
301
|
+
// firstDataRow: row offset where data starts (after filters and column headers)
|
|
302
|
+
// firstDataCol: column offset where data starts (after row labels)
|
|
303
|
+
const filterRowCount = this._filterFields.length > 0 ? this._filterFields.length + 1 : 0;
|
|
304
|
+
const headerRows = this._columnFields.length > 0 ? 1 : 0;
|
|
305
|
+
const firstDataRow = filterRowCount + headerRows + 1;
|
|
306
|
+
const firstDataCol = this._rowFields.length > 0 ? this._rowFields.length : 1;
|
|
307
|
+
|
|
308
|
+
const locationNode = createElement(
|
|
309
|
+
'location',
|
|
310
|
+
{
|
|
311
|
+
ref: locationRef,
|
|
312
|
+
firstHeaderRow: String(filterRowCount + 1),
|
|
313
|
+
firstDataRow: String(firstDataRow),
|
|
314
|
+
firstDataCol: String(firstDataCol),
|
|
315
|
+
},
|
|
316
|
+
[],
|
|
317
|
+
);
|
|
318
|
+
children.push(locationNode);
|
|
319
|
+
|
|
320
|
+
// Build pivotFields (one per source field)
|
|
321
|
+
const pivotFieldNodes: XmlNode[] = [];
|
|
322
|
+
for (const cacheField of this._cache.fields) {
|
|
323
|
+
const fieldNode = this._buildPivotFieldNode(cacheField.index);
|
|
324
|
+
pivotFieldNodes.push(fieldNode);
|
|
325
|
+
}
|
|
326
|
+
children.push(createElement('pivotFields', { count: String(pivotFieldNodes.length) }, pivotFieldNodes));
|
|
327
|
+
|
|
328
|
+
// Row fields
|
|
329
|
+
if (this._rowFields.length > 0) {
|
|
330
|
+
const rowFieldNodes = this._rowFields.map((f) => createElement('field', { x: String(f.fieldIndex) }, []));
|
|
331
|
+
children.push(createElement('rowFields', { count: String(rowFieldNodes.length) }, rowFieldNodes));
|
|
332
|
+
|
|
333
|
+
// Row items
|
|
334
|
+
const rowItemNodes = this._buildRowItems();
|
|
335
|
+
children.push(createElement('rowItems', { count: String(rowItemNodes.length) }, rowItemNodes));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Column fields
|
|
339
|
+
if (this._columnFields.length > 0) {
|
|
340
|
+
const colFieldNodes = this._columnFields.map((f) => createElement('field', { x: String(f.fieldIndex) }, []));
|
|
341
|
+
// If we have multiple value fields, add -2 to indicate where "Values" header goes
|
|
342
|
+
if (this._valueFields.length > 1) {
|
|
343
|
+
colFieldNodes.push(createElement('field', { x: '-2' }, []));
|
|
344
|
+
}
|
|
345
|
+
children.push(createElement('colFields', { count: String(colFieldNodes.length) }, colFieldNodes));
|
|
346
|
+
|
|
347
|
+
// Column items - need to account for multiple value fields
|
|
348
|
+
const colItemNodes = this._buildColItems();
|
|
349
|
+
children.push(createElement('colItems', { count: String(colItemNodes.length) }, colItemNodes));
|
|
350
|
+
} else if (this._valueFields.length > 1) {
|
|
351
|
+
// If no column fields but we have multiple values, need colFields with -2 (data field indicator)
|
|
352
|
+
children.push(createElement('colFields', { count: '1' }, [createElement('field', { x: '-2' }, [])]));
|
|
353
|
+
|
|
354
|
+
// Column items for each value field
|
|
355
|
+
const colItemNodes: XmlNode[] = [];
|
|
356
|
+
for (let i = 0; i < this._valueFields.length; i++) {
|
|
357
|
+
colItemNodes.push(createElement('i', {}, [createElement('x', i === 0 ? {} : { v: String(i) }, [])]));
|
|
358
|
+
}
|
|
359
|
+
children.push(createElement('colItems', { count: String(colItemNodes.length) }, colItemNodes));
|
|
360
|
+
} else if (this._valueFields.length === 1) {
|
|
361
|
+
// Single value field - just add a single column item
|
|
362
|
+
children.push(createElement('colItems', { count: '1' }, [createElement('i', {}, [])]));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Page (filter) fields
|
|
366
|
+
if (this._filterFields.length > 0) {
|
|
367
|
+
const pageFieldNodes = this._filterFields.map((f) =>
|
|
368
|
+
createElement('pageField', { fld: String(f.fieldIndex), hier: '-1' }, []),
|
|
369
|
+
);
|
|
370
|
+
children.push(createElement('pageFields', { count: String(pageFieldNodes.length) }, pageFieldNodes));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Data fields (values)
|
|
374
|
+
if (this._valueFields.length > 0) {
|
|
375
|
+
const dataFieldNodes = this._valueFields.map((f) => {
|
|
376
|
+
const attrs: Record<string, string> = {
|
|
377
|
+
name: f.displayName || f.fieldName,
|
|
378
|
+
fld: String(f.fieldIndex),
|
|
379
|
+
baseField: '0',
|
|
380
|
+
baseItem: '0',
|
|
381
|
+
subtotal: f.aggregation || 'sum',
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
if (f.numFmtId !== undefined) {
|
|
385
|
+
attrs.numFmtId = String(f.numFmtId);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return createElement('dataField', attrs, []);
|
|
389
|
+
});
|
|
390
|
+
children.push(createElement('dataFields', { count: String(dataFieldNodes.length) }, dataFieldNodes));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Pivot table style
|
|
394
|
+
children.push(
|
|
395
|
+
createElement(
|
|
396
|
+
'pivotTableStyleInfo',
|
|
397
|
+
{
|
|
398
|
+
name: 'PivotStyleMedium9',
|
|
399
|
+
showRowHeaders: '1',
|
|
400
|
+
showColHeaders: '1',
|
|
401
|
+
showRowStripes: '0',
|
|
402
|
+
showColStripes: '0',
|
|
403
|
+
showLastColumn: '1',
|
|
404
|
+
},
|
|
405
|
+
[],
|
|
406
|
+
),
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
const pivotTableNode = createElement(
|
|
410
|
+
'pivotTableDefinition',
|
|
411
|
+
{
|
|
412
|
+
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
413
|
+
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
414
|
+
name: this._name,
|
|
415
|
+
cacheId: String(this._cache.cacheId),
|
|
416
|
+
applyNumberFormats: '1',
|
|
417
|
+
applyBorderFormats: '0',
|
|
418
|
+
applyFontFormats: '0',
|
|
419
|
+
applyPatternFormats: '0',
|
|
420
|
+
applyAlignmentFormats: '0',
|
|
421
|
+
applyWidthHeightFormats: '1',
|
|
422
|
+
dataCaption: 'Values',
|
|
423
|
+
updatedVersion: '8',
|
|
424
|
+
minRefreshableVersion: '3',
|
|
425
|
+
useAutoFormatting: '1',
|
|
426
|
+
rowGrandTotals: '1',
|
|
427
|
+
colGrandTotals: '1',
|
|
428
|
+
itemPrintTitles: '1',
|
|
429
|
+
createdVersion: '8',
|
|
430
|
+
indent: '0',
|
|
431
|
+
outline: '1',
|
|
432
|
+
outlineData: '1',
|
|
433
|
+
multipleFieldFilters: '0',
|
|
434
|
+
},
|
|
435
|
+
children,
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([pivotTableNode])}`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Build a pivotField node for a given field index
|
|
443
|
+
*/
|
|
444
|
+
private _buildPivotFieldNode(fieldIndex: number): XmlNode {
|
|
445
|
+
const attrs: Record<string, string> = {};
|
|
446
|
+
const children: XmlNode[] = [];
|
|
447
|
+
|
|
448
|
+
// Check if this field is assigned to an axis
|
|
449
|
+
const rowField = this._rowFields.find((f) => f.fieldIndex === fieldIndex);
|
|
450
|
+
const colField = this._columnFields.find((f) => f.fieldIndex === fieldIndex);
|
|
451
|
+
const filterField = this._filterFields.find((f) => f.fieldIndex === fieldIndex);
|
|
452
|
+
const valueField = this._valueFields.find((f) => f.fieldIndex === fieldIndex);
|
|
453
|
+
|
|
454
|
+
// Get the assignment to check for sort/filter options
|
|
455
|
+
const assignment = rowField || colField || filterField;
|
|
456
|
+
|
|
457
|
+
if (rowField) {
|
|
458
|
+
attrs.axis = 'axisRow';
|
|
459
|
+
attrs.showAll = '0';
|
|
460
|
+
|
|
461
|
+
// Add sort order if specified
|
|
462
|
+
if (rowField.sortOrder) {
|
|
463
|
+
attrs.sortType = rowField.sortOrder === 'asc' ? 'ascending' : 'descending';
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Add items for shared values
|
|
467
|
+
const cacheField = this._cache.fields[fieldIndex];
|
|
468
|
+
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
469
|
+
const itemNodes = this._buildItemNodes(cacheField.sharedItems, assignment?.filter);
|
|
470
|
+
children.push(createElement('items', { count: String(itemNodes.length) }, itemNodes));
|
|
471
|
+
}
|
|
472
|
+
} else if (colField) {
|
|
473
|
+
attrs.axis = 'axisCol';
|
|
474
|
+
attrs.showAll = '0';
|
|
475
|
+
|
|
476
|
+
// Add sort order if specified
|
|
477
|
+
if (colField.sortOrder) {
|
|
478
|
+
attrs.sortType = colField.sortOrder === 'asc' ? 'ascending' : 'descending';
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const cacheField = this._cache.fields[fieldIndex];
|
|
482
|
+
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
483
|
+
const itemNodes = this._buildItemNodes(cacheField.sharedItems, assignment?.filter);
|
|
484
|
+
children.push(createElement('items', { count: String(itemNodes.length) }, itemNodes));
|
|
485
|
+
}
|
|
486
|
+
} else if (filterField) {
|
|
487
|
+
attrs.axis = 'axisPage';
|
|
488
|
+
attrs.showAll = '0';
|
|
489
|
+
const cacheField = this._cache.fields[fieldIndex];
|
|
490
|
+
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
491
|
+
const itemNodes = this._buildItemNodes(cacheField.sharedItems, assignment?.filter);
|
|
492
|
+
children.push(createElement('items', { count: String(itemNodes.length) }, itemNodes));
|
|
493
|
+
}
|
|
494
|
+
} else if (valueField) {
|
|
495
|
+
attrs.dataField = '1';
|
|
496
|
+
attrs.showAll = '0';
|
|
497
|
+
} else {
|
|
498
|
+
attrs.showAll = '0';
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return createElement('pivotField', attrs, children);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Build item nodes for a pivot field, with optional filtering
|
|
506
|
+
*/
|
|
507
|
+
private _buildItemNodes(sharedItems: string[], filter?: PivotFieldFilter): XmlNode[] {
|
|
508
|
+
const itemNodes: XmlNode[] = [];
|
|
509
|
+
|
|
510
|
+
for (let i = 0; i < sharedItems.length; i++) {
|
|
511
|
+
const itemValue = sharedItems[i];
|
|
512
|
+
const itemAttrs: Record<string, string> = { x: String(i) };
|
|
513
|
+
|
|
514
|
+
// Check if this item should be hidden
|
|
515
|
+
if (filter) {
|
|
516
|
+
let hidden = false;
|
|
517
|
+
if (filter.exclude && filter.exclude.includes(itemValue)) {
|
|
518
|
+
hidden = true;
|
|
519
|
+
} else if (filter.include && !filter.include.includes(itemValue)) {
|
|
520
|
+
hidden = true;
|
|
521
|
+
}
|
|
522
|
+
if (hidden) {
|
|
523
|
+
itemAttrs.h = '1';
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
itemNodes.push(createElement('item', itemAttrs, []));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Add default subtotal item
|
|
531
|
+
itemNodes.push(createElement('item', { t: 'default' }, []));
|
|
532
|
+
|
|
533
|
+
return itemNodes;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Build row items based on unique values in row fields
|
|
538
|
+
*/
|
|
539
|
+
private _buildRowItems(): XmlNode[] {
|
|
540
|
+
const items: XmlNode[] = [];
|
|
541
|
+
|
|
542
|
+
if (this._rowFields.length === 0) return items;
|
|
543
|
+
|
|
544
|
+
// Get unique values from first row field
|
|
545
|
+
const firstRowField = this._rowFields[0];
|
|
546
|
+
const cacheField = this._cache.fields[firstRowField.fieldIndex];
|
|
547
|
+
|
|
548
|
+
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
549
|
+
for (let i = 0; i < cacheField.sharedItems.length; i++) {
|
|
550
|
+
items.push(createElement('i', {}, [createElement('x', i === 0 ? {} : { v: String(i) }, [])]));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Add grand total row
|
|
555
|
+
items.push(createElement('i', { t: 'grand' }, [createElement('x', {}, [])]));
|
|
556
|
+
|
|
557
|
+
return items;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Build column items based on unique values in column fields
|
|
562
|
+
*/
|
|
563
|
+
private _buildColItems(): XmlNode[] {
|
|
564
|
+
const items: XmlNode[] = [];
|
|
565
|
+
|
|
566
|
+
if (this._columnFields.length === 0) return items;
|
|
567
|
+
|
|
568
|
+
// Get unique values from first column field
|
|
569
|
+
const firstColField = this._columnFields[0];
|
|
570
|
+
const cacheField = this._cache.fields[firstColField.fieldIndex];
|
|
571
|
+
|
|
572
|
+
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
573
|
+
if (this._valueFields.length > 1) {
|
|
574
|
+
// Multiple value fields - need nested items for each column value + value field combination
|
|
575
|
+
for (let colIdx = 0; colIdx < cacheField.sharedItems.length; colIdx++) {
|
|
576
|
+
for (let valIdx = 0; valIdx < this._valueFields.length; valIdx++) {
|
|
577
|
+
const xNodes: XmlNode[] = [
|
|
578
|
+
createElement('x', colIdx === 0 ? {} : { v: String(colIdx) }, []),
|
|
579
|
+
createElement('x', valIdx === 0 ? {} : { v: String(valIdx) }, []),
|
|
580
|
+
];
|
|
581
|
+
items.push(createElement('i', {}, xNodes));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
} else {
|
|
585
|
+
// Single value field - simple column items
|
|
586
|
+
for (let i = 0; i < cacheField.sharedItems.length; i++) {
|
|
587
|
+
items.push(createElement('i', {}, [createElement('x', i === 0 ? {} : { v: String(i) }, [])]));
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Add grand total column(s)
|
|
593
|
+
if (this._valueFields.length > 1) {
|
|
594
|
+
// Grand total for each value field
|
|
595
|
+
for (let valIdx = 0; valIdx < this._valueFields.length; valIdx++) {
|
|
596
|
+
const xNodes: XmlNode[] = [
|
|
597
|
+
createElement('x', {}, []),
|
|
598
|
+
createElement('x', valIdx === 0 ? {} : { v: String(valIdx) }, []),
|
|
599
|
+
];
|
|
600
|
+
items.push(createElement('i', { t: 'grand' }, xNodes));
|
|
601
|
+
}
|
|
602
|
+
} else {
|
|
603
|
+
items.push(createElement('i', { t: 'grand' }, [createElement('x', {}, [])]));
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return items;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Calculate the location reference for the pivot table output
|
|
611
|
+
*/
|
|
612
|
+
private _calculateLocationRef(): string {
|
|
613
|
+
// Estimate output size based on fields
|
|
614
|
+
const numRows = this._estimateRowCount();
|
|
615
|
+
const numCols = this._estimateColCount();
|
|
616
|
+
|
|
617
|
+
const startRow = this._targetRow;
|
|
618
|
+
const startCol = this._targetCol;
|
|
619
|
+
const endRow = startRow + numRows - 1;
|
|
620
|
+
const endCol = startCol + numCols - 1;
|
|
621
|
+
|
|
622
|
+
return `${this._colToLetter(startCol)}${startRow}:${this._colToLetter(endCol)}${endRow}`;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Estimate number of rows in pivot table output
|
|
627
|
+
*/
|
|
628
|
+
private _estimateRowCount(): number {
|
|
629
|
+
let count = 1; // Header row
|
|
630
|
+
|
|
631
|
+
// Add filter area rows
|
|
632
|
+
count += this._filterFields.length;
|
|
633
|
+
|
|
634
|
+
// Add row labels (unique values in row fields)
|
|
635
|
+
if (this._rowFields.length > 0) {
|
|
636
|
+
const firstRowField = this._rowFields[0];
|
|
637
|
+
const cacheField = this._cache.fields[firstRowField.fieldIndex];
|
|
638
|
+
count += (cacheField?.sharedItems.length || 1) + 1; // +1 for grand total
|
|
639
|
+
} else {
|
|
640
|
+
count += 1; // At least one data row
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return Math.max(count, 3);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Estimate number of columns in pivot table output
|
|
648
|
+
*/
|
|
649
|
+
private _estimateColCount(): number {
|
|
650
|
+
let count = 0;
|
|
651
|
+
|
|
652
|
+
// Row label columns
|
|
653
|
+
count += Math.max(this._rowFields.length, 1);
|
|
654
|
+
|
|
655
|
+
// Column labels (unique values in column fields)
|
|
656
|
+
if (this._columnFields.length > 0) {
|
|
657
|
+
const firstColField = this._columnFields[0];
|
|
658
|
+
const cacheField = this._cache.fields[firstColField.fieldIndex];
|
|
659
|
+
count += (cacheField?.sharedItems.length || 1) + 1; // +1 for grand total
|
|
660
|
+
} else {
|
|
661
|
+
// Value columns
|
|
662
|
+
count += Math.max(this._valueFields.length, 1);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return Math.max(count, 2);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Convert 0-based column index to letter (A, B, ..., Z, AA, etc.)
|
|
670
|
+
*/
|
|
671
|
+
private _colToLetter(col: number): string {
|
|
672
|
+
let result = '';
|
|
673
|
+
let n = col;
|
|
674
|
+
while (n >= 0) {
|
|
675
|
+
result = String.fromCharCode((n % 26) + 65) + result;
|
|
676
|
+
n = Math.floor(n / 26) - 1;
|
|
677
|
+
}
|
|
678
|
+
return result;
|
|
679
|
+
}
|
|
680
|
+
}
|