@niicojs/excel 0.3.4 → 0.3.6
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 +8 -2
- package/dist/index.cjs +1191 -1266
- package/dist/index.d.cts +171 -324
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +171 -324
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1191 -1267
- package/package.json +4 -4
- package/src/index.ts +8 -10
- package/src/pivot-table.ts +619 -524
- package/src/shared-strings.ts +33 -9
- package/src/styles.ts +38 -9
- package/src/types.ts +295 -323
- package/src/utils/address.ts +48 -0
- package/src/utils/format.ts +8 -7
- package/src/utils/xml.ts +7 -4
- package/src/utils/zip.ts +153 -11
- package/src/workbook.ts +330 -350
- package/src/worksheet.ts +1003 -935
- package/src/pivot-cache.ts +0 -449
package/src/pivot-table.ts
CHANGED
|
@@ -1,396 +1,480 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type {
|
|
2
|
+
PivotAggregationType,
|
|
3
|
+
PivotFieldFilter,
|
|
4
|
+
PivotSortOrder,
|
|
5
|
+
PivotTableConfig,
|
|
6
|
+
PivotValueConfig,
|
|
7
|
+
RangeAddress,
|
|
8
|
+
CellValue,
|
|
9
|
+
} from './types';
|
|
10
|
+
import type { Workbook } from './workbook';
|
|
11
|
+
import type { Worksheet } from './worksheet';
|
|
4
12
|
import { createElement, stringifyXml, XmlNode } from './utils/xml';
|
|
13
|
+
import { toAddress, toRange } from './utils/address';
|
|
14
|
+
|
|
15
|
+
const AGGREGATION_TO_XML: Record<PivotAggregationType, string> = {
|
|
16
|
+
sum: 'sum',
|
|
17
|
+
count: 'count',
|
|
18
|
+
average: 'average',
|
|
19
|
+
min: 'min',
|
|
20
|
+
max: 'max',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const SORT_TO_XML: Record<PivotSortOrder, 'ascending' | 'descending'> = {
|
|
24
|
+
asc: 'ascending',
|
|
25
|
+
desc: 'descending',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
interface PivotValueField {
|
|
29
|
+
field: string;
|
|
30
|
+
aggregation: PivotAggregationType;
|
|
31
|
+
name: string;
|
|
32
|
+
numberFormat?: string;
|
|
33
|
+
}
|
|
5
34
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
35
|
+
interface PivotFieldMeta {
|
|
36
|
+
name: string;
|
|
37
|
+
sourceCol: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface PivotNumericInfo {
|
|
41
|
+
nonNullCount: number;
|
|
42
|
+
numericCount: number;
|
|
43
|
+
min: number;
|
|
44
|
+
max: number;
|
|
45
|
+
hasNumeric: boolean;
|
|
46
|
+
allIntegers: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface PivotCacheData {
|
|
50
|
+
rowCount: number;
|
|
51
|
+
recordNodes: XmlNode[];
|
|
52
|
+
sharedItemIndexByField: Array<Map<string, number> | null>;
|
|
53
|
+
sharedItemsByField: Array<XmlNode[] | null>;
|
|
54
|
+
distinctItemsByField: Array<Exclude<CellValue, null>[] | null>;
|
|
55
|
+
numericInfoByField: PivotNumericInfo[];
|
|
56
|
+
isAxisFieldByIndex: boolean[];
|
|
57
|
+
isValueFieldByIndex: boolean[];
|
|
18
58
|
}
|
|
19
59
|
|
|
20
60
|
/**
|
|
21
|
-
* Represents an Excel
|
|
61
|
+
* Represents an Excel PivotTable with a fluent configuration API.
|
|
22
62
|
*/
|
|
23
63
|
export class PivotTable {
|
|
64
|
+
private _workbook: Workbook;
|
|
24
65
|
private _name: string;
|
|
25
|
-
private
|
|
26
|
-
private
|
|
27
|
-
private
|
|
28
|
-
private
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
private
|
|
32
|
-
private
|
|
33
|
-
private
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
private
|
|
38
|
-
private
|
|
39
|
-
private
|
|
66
|
+
private _sourceSheetName: string;
|
|
67
|
+
private _sourceSheet: Worksheet;
|
|
68
|
+
private _sourceRange: RangeAddress;
|
|
69
|
+
private _targetSheetName: string;
|
|
70
|
+
private _targetCell: { row: number; col: number };
|
|
71
|
+
private _refreshOnLoad: boolean;
|
|
72
|
+
private _cacheId: number;
|
|
73
|
+
private _pivotId: number;
|
|
74
|
+
private _cachePartIndex: number;
|
|
75
|
+
private _fields: PivotFieldMeta[];
|
|
76
|
+
|
|
77
|
+
private _rowFields: string[] = [];
|
|
78
|
+
private _columnFields: string[] = [];
|
|
79
|
+
private _filterFields: string[] = [];
|
|
80
|
+
private _valueFields: PivotValueField[] = [];
|
|
81
|
+
private _sortOrders: Map<string, PivotSortOrder> = new Map();
|
|
82
|
+
private _filters: Map<string, PivotFieldFilter> = new Map();
|
|
40
83
|
|
|
41
84
|
constructor(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
85
|
+
workbook: Workbook,
|
|
86
|
+
config: PivotTableConfig,
|
|
87
|
+
sourceSheetName: string,
|
|
88
|
+
sourceSheet: Worksheet,
|
|
89
|
+
sourceRange: RangeAddress,
|
|
90
|
+
targetSheetName: string,
|
|
91
|
+
targetCell: { row: number; col: number },
|
|
92
|
+
cacheId: number,
|
|
93
|
+
pivotId: number,
|
|
94
|
+
cachePartIndex: number,
|
|
95
|
+
fields: PivotFieldMeta[],
|
|
50
96
|
) {
|
|
51
|
-
this.
|
|
52
|
-
this.
|
|
53
|
-
this.
|
|
97
|
+
this._workbook = workbook;
|
|
98
|
+
this._name = config.name;
|
|
99
|
+
this._sourceSheetName = sourceSheetName;
|
|
100
|
+
this._sourceSheet = sourceSheet;
|
|
101
|
+
this._sourceRange = sourceRange;
|
|
102
|
+
this._targetSheetName = targetSheetName;
|
|
54
103
|
this._targetCell = targetCell;
|
|
55
|
-
this.
|
|
56
|
-
this.
|
|
57
|
-
this.
|
|
58
|
-
this.
|
|
104
|
+
this._refreshOnLoad = config.refreshOnLoad !== false;
|
|
105
|
+
this._cacheId = cacheId;
|
|
106
|
+
this._pivotId = pivotId;
|
|
107
|
+
this._cachePartIndex = cachePartIndex;
|
|
108
|
+
this._fields = fields;
|
|
59
109
|
}
|
|
60
110
|
|
|
61
|
-
/**
|
|
62
|
-
* Get the pivot table name
|
|
63
|
-
*/
|
|
64
111
|
get name(): string {
|
|
65
112
|
return this._name;
|
|
66
113
|
}
|
|
67
114
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
*/
|
|
71
|
-
get targetSheet(): string {
|
|
72
|
-
return this._targetSheet;
|
|
115
|
+
get sourceSheetName(): string {
|
|
116
|
+
return this._sourceSheetName;
|
|
73
117
|
}
|
|
74
118
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
*/
|
|
78
|
-
get targetCell(): string {
|
|
79
|
-
return this._targetCell;
|
|
119
|
+
get sourceRange(): RangeAddress {
|
|
120
|
+
return { start: { ...this._sourceRange.start }, end: { ...this._sourceRange.end } };
|
|
80
121
|
}
|
|
81
122
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
*/
|
|
85
|
-
get cache(): PivotCache {
|
|
86
|
-
return this._cache;
|
|
123
|
+
get targetSheetName(): string {
|
|
124
|
+
return this._targetSheetName;
|
|
87
125
|
}
|
|
88
126
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
*/
|
|
92
|
-
get index(): number {
|
|
93
|
-
return this._pivotTableIndex;
|
|
127
|
+
get targetCell(): { row: number; col: number } {
|
|
128
|
+
return { ...this._targetCell };
|
|
94
129
|
}
|
|
95
130
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
* @internal
|
|
99
|
-
*/
|
|
100
|
-
get cacheFileIndex(): number {
|
|
101
|
-
return this._cacheFileIndex;
|
|
131
|
+
get refreshOnLoad(): boolean {
|
|
132
|
+
return this._refreshOnLoad;
|
|
102
133
|
}
|
|
103
134
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
* @internal
|
|
107
|
-
*/
|
|
108
|
-
setStyles(styles: Styles): this {
|
|
109
|
-
this._styles = styles;
|
|
110
|
-
return this;
|
|
135
|
+
get cacheId(): number {
|
|
136
|
+
return this._cacheId;
|
|
111
137
|
}
|
|
112
138
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
}
|
|
139
|
+
get pivotId(): number {
|
|
140
|
+
return this._pivotId;
|
|
141
|
+
}
|
|
122
142
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
axis: 'row',
|
|
127
|
-
};
|
|
128
|
-
this._rowFields.push(assignment);
|
|
129
|
-
this._fieldAssignments.set(fieldIndex, assignment);
|
|
143
|
+
get cachePartIndex(): number {
|
|
144
|
+
return this._cachePartIndex;
|
|
145
|
+
}
|
|
130
146
|
|
|
147
|
+
addRowField(fieldName: string): this {
|
|
148
|
+
this._assertFieldExists(fieldName);
|
|
149
|
+
if (!this._rowFields.includes(fieldName)) {
|
|
150
|
+
this._rowFields.push(fieldName);
|
|
151
|
+
}
|
|
131
152
|
return this;
|
|
132
153
|
}
|
|
133
154
|
|
|
134
|
-
/**
|
|
135
|
-
* Add a field to the column area
|
|
136
|
-
* @param fieldName - Name of the source field (column header)
|
|
137
|
-
*/
|
|
138
155
|
addColumnField(fieldName: string): this {
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
|
|
156
|
+
this._assertFieldExists(fieldName);
|
|
157
|
+
if (!this._columnFields.includes(fieldName)) {
|
|
158
|
+
this._columnFields.push(fieldName);
|
|
142
159
|
}
|
|
160
|
+
return this;
|
|
161
|
+
}
|
|
143
162
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
this._columnFields.push(assignment);
|
|
150
|
-
this._fieldAssignments.set(fieldIndex, assignment);
|
|
151
|
-
|
|
163
|
+
addFilterField(fieldName: string): this {
|
|
164
|
+
this._assertFieldExists(fieldName);
|
|
165
|
+
if (!this._filterFields.includes(fieldName)) {
|
|
166
|
+
this._filterFields.push(fieldName);
|
|
167
|
+
}
|
|
152
168
|
return this;
|
|
153
169
|
}
|
|
154
170
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
*/
|
|
171
|
+
addValueField(
|
|
172
|
+
fieldName: string,
|
|
173
|
+
aggregation?: PivotAggregationType,
|
|
174
|
+
displayName?: string,
|
|
175
|
+
numberFormat?: string,
|
|
176
|
+
): this;
|
|
169
177
|
addValueField(config: PivotValueConfig): this;
|
|
170
|
-
addValueField(fieldName: string, aggregation?: AggregationType, displayName?: string, numberFormat?: string): this;
|
|
171
178
|
addValueField(
|
|
172
179
|
fieldNameOrConfig: string | PivotValueConfig,
|
|
173
|
-
aggregation:
|
|
180
|
+
aggregation: PivotAggregationType = 'sum',
|
|
174
181
|
displayName?: string,
|
|
175
182
|
numberFormat?: string,
|
|
176
183
|
): this {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
name = fieldNameOrConfig.name;
|
|
187
|
-
format = fieldNameOrConfig.numberFormat;
|
|
184
|
+
let config: PivotValueConfig;
|
|
185
|
+
|
|
186
|
+
if (typeof fieldNameOrConfig === 'string') {
|
|
187
|
+
config = {
|
|
188
|
+
field: fieldNameOrConfig,
|
|
189
|
+
aggregation,
|
|
190
|
+
name: displayName,
|
|
191
|
+
numberFormat,
|
|
192
|
+
};
|
|
188
193
|
} else {
|
|
189
|
-
|
|
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}`);
|
|
194
|
+
config = fieldNameOrConfig;
|
|
198
195
|
}
|
|
199
196
|
|
|
200
|
-
|
|
197
|
+
this._assertFieldExists(config.field);
|
|
201
198
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (format && this._styles) {
|
|
205
|
-
numFmtId = this._styles.getOrCreateNumFmtId(format);
|
|
206
|
-
}
|
|
199
|
+
const resolvedAggregation = config.aggregation ?? 'sum';
|
|
200
|
+
const resolvedName = config.name ?? `${this._aggregationLabel(resolvedAggregation)} of ${config.field}`;
|
|
207
201
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
numFmtId,
|
|
215
|
-
};
|
|
216
|
-
this._valueFields.push(assignment);
|
|
217
|
-
this._fieldAssignments.set(fieldIndex, assignment);
|
|
202
|
+
this._valueFields.push({
|
|
203
|
+
field: config.field,
|
|
204
|
+
aggregation: resolvedAggregation,
|
|
205
|
+
name: resolvedName,
|
|
206
|
+
numberFormat: config.numberFormat,
|
|
207
|
+
});
|
|
218
208
|
|
|
219
209
|
return this;
|
|
220
210
|
}
|
|
221
211
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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}`);
|
|
212
|
+
sortField(fieldName: string, order: PivotSortOrder): this {
|
|
213
|
+
this._assertFieldExists(fieldName);
|
|
214
|
+
if (!this._rowFields.includes(fieldName) && !this._columnFields.includes(fieldName)) {
|
|
215
|
+
throw new Error(`Cannot sort field "${fieldName}": only row or column fields can be sorted`);
|
|
230
216
|
}
|
|
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
|
-
|
|
217
|
+
this._sortOrders.set(fieldName, order);
|
|
240
218
|
return this;
|
|
241
219
|
}
|
|
242
220
|
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
}
|
|
221
|
+
filterField(fieldName: string, filter: PivotFieldFilter): this {
|
|
222
|
+
this._assertFieldExists(fieldName);
|
|
253
223
|
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
224
|
+
const hasInclude = 'include' in filter;
|
|
225
|
+
const hasExclude = 'exclude' in filter;
|
|
226
|
+
if ((hasInclude && hasExclude) || (!hasInclude && !hasExclude)) {
|
|
227
|
+
throw new Error('Pivot filter must contain either include or exclude');
|
|
257
228
|
}
|
|
258
|
-
|
|
259
|
-
|
|
229
|
+
|
|
230
|
+
const values = hasInclude ? filter.include : filter.exclude;
|
|
231
|
+
if (!values || values.length === 0) {
|
|
232
|
+
throw new Error('Pivot filter values cannot be empty');
|
|
260
233
|
}
|
|
261
234
|
|
|
262
|
-
|
|
235
|
+
this._filters.set(fieldName, filter);
|
|
263
236
|
return this;
|
|
264
237
|
}
|
|
265
238
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
}
|
|
239
|
+
toPivotCacheDefinitionXml(): string {
|
|
240
|
+
const cacheData = this._buildPivotCacheData();
|
|
241
|
+
return this._buildPivotCacheDefinitionXml(cacheData);
|
|
242
|
+
}
|
|
276
243
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
244
|
+
toPivotCacheRecordsXml(): string {
|
|
245
|
+
const cacheData = this._buildPivotCacheData();
|
|
246
|
+
return this._buildPivotCacheRecordsXml(cacheData);
|
|
247
|
+
}
|
|
281
248
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
249
|
+
toPivotCacheDefinitionRelsXml(): string {
|
|
250
|
+
const relsRoot = createElement(
|
|
251
|
+
'Relationships',
|
|
252
|
+
{ xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },
|
|
253
|
+
[
|
|
254
|
+
createElement(
|
|
255
|
+
'Relationship',
|
|
256
|
+
{
|
|
257
|
+
Id: 'rId1',
|
|
258
|
+
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheRecords',
|
|
259
|
+
Target: `pivotCacheRecords${this._cachePartIndex}.xml`,
|
|
260
|
+
},
|
|
261
|
+
[],
|
|
262
|
+
),
|
|
263
|
+
],
|
|
264
|
+
);
|
|
285
265
|
|
|
286
|
-
|
|
287
|
-
return this;
|
|
266
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([relsRoot])}`;
|
|
288
267
|
}
|
|
289
268
|
|
|
290
269
|
/**
|
|
291
|
-
*
|
|
270
|
+
* @internal
|
|
292
271
|
*/
|
|
293
|
-
|
|
294
|
-
|
|
272
|
+
buildPivotPartsXml(): {
|
|
273
|
+
cacheDefinitionXml: string;
|
|
274
|
+
cacheRecordsXml: string;
|
|
275
|
+
cacheRelsXml: string;
|
|
276
|
+
pivotTableXml: string;
|
|
277
|
+
} {
|
|
278
|
+
const cacheData = this._buildPivotCacheData();
|
|
279
|
+
return {
|
|
280
|
+
cacheDefinitionXml: this._buildPivotCacheDefinitionXml(cacheData),
|
|
281
|
+
cacheRecordsXml: this._buildPivotCacheRecordsXml(cacheData),
|
|
282
|
+
cacheRelsXml: this.toPivotCacheDefinitionRelsXml(),
|
|
283
|
+
pivotTableXml: this._buildPivotTableDefinitionXml(cacheData),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
toPivotTableDefinitionXml(): string {
|
|
288
|
+
const cacheData = this._buildPivotCacheData();
|
|
289
|
+
return this._buildPivotTableDefinitionXml(cacheData);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private _buildPivotCacheDefinitionXml(cacheData: PivotCacheData): string {
|
|
293
|
+
const cacheFieldNodes = this._fields.map((field, index) => this._buildCacheFieldNode(field, index, cacheData));
|
|
294
|
+
|
|
295
|
+
const attrs: Record<string, string> = {
|
|
296
|
+
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
297
|
+
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
298
|
+
'xmlns:mc': 'http://schemas.openxmlformats.org/markup-compatibility/2006',
|
|
299
|
+
'mc:Ignorable': 'xr',
|
|
300
|
+
'xmlns:xr': 'http://schemas.microsoft.com/office/spreadsheetml/2014/revision',
|
|
301
|
+
'r:id': 'rId1',
|
|
302
|
+
createdVersion: '8',
|
|
303
|
+
minRefreshableVersion: '3',
|
|
304
|
+
refreshedVersion: '8',
|
|
305
|
+
refreshOnLoad: this._refreshOnLoad ? '1' : '0',
|
|
306
|
+
recordCount: String(cacheData.rowCount),
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const cacheSourceNode = createElement('cacheSource', { type: 'worksheet' }, [
|
|
310
|
+
createElement('worksheetSource', { sheet: this._sourceSheetName, ref: toRange(this._sourceRange) }, []),
|
|
311
|
+
]);
|
|
295
312
|
|
|
296
|
-
|
|
297
|
-
const locationRef = this._calculateLocationRef();
|
|
313
|
+
const cacheFieldsNode = createElement('cacheFields', { count: String(cacheFieldNodes.length) }, cacheFieldNodes);
|
|
298
314
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
315
|
+
const extLstNode = createElement('extLst', {}, [
|
|
316
|
+
createElement(
|
|
317
|
+
'ext',
|
|
318
|
+
{
|
|
319
|
+
uri: '{725AE2AE-9491-48be-B2B4-4EB974FC3084}',
|
|
320
|
+
'xmlns:x14': 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/main',
|
|
321
|
+
},
|
|
322
|
+
[createElement('x14:pivotCacheDefinition', {}, [])],
|
|
323
|
+
),
|
|
324
|
+
]);
|
|
325
|
+
|
|
326
|
+
const root = createElement('pivotCacheDefinition', attrs, [cacheSourceNode, cacheFieldsNode, extLstNode]);
|
|
327
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([root])}`;
|
|
328
|
+
}
|
|
307
329
|
|
|
308
|
-
|
|
309
|
-
|
|
330
|
+
private _buildPivotCacheRecordsXml(cacheData: PivotCacheData): string {
|
|
331
|
+
const recordNodes = cacheData.recordNodes;
|
|
332
|
+
const root = createElement(
|
|
333
|
+
'pivotCacheRecords',
|
|
310
334
|
{
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
335
|
+
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
336
|
+
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
337
|
+
'xmlns:mc': 'http://schemas.openxmlformats.org/markup-compatibility/2006',
|
|
338
|
+
'mc:Ignorable': 'xr',
|
|
339
|
+
'xmlns:xr': 'http://schemas.microsoft.com/office/spreadsheetml/2014/revision',
|
|
340
|
+
count: String(recordNodes.length),
|
|
315
341
|
},
|
|
316
|
-
|
|
342
|
+
recordNodes,
|
|
343
|
+
);
|
|
344
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([root])}`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private _buildPivotTableDefinitionXml(cacheData: PivotCacheData): string {
|
|
348
|
+
const effectiveValueFields = this._valueFields.length > 0 ? [this._valueFields[0]] : [];
|
|
349
|
+
const sourceFieldCount = this._fields.length;
|
|
350
|
+
const pivotFields: XmlNode[] = [];
|
|
351
|
+
const effectiveRowFieldName = this._rowFields[0];
|
|
352
|
+
const rowFieldIndexes = effectiveRowFieldName ? [this._fieldIndex(effectiveRowFieldName)] : [];
|
|
353
|
+
const colFieldIndexes = this._columnFields.length > 0 ? [this._fieldIndex(this._columnFields[0])] : [];
|
|
354
|
+
const valueFieldIndexes = new Set<number>(
|
|
355
|
+
effectiveValueFields.map((valueField) => this._fieldIndex(valueField.field)),
|
|
317
356
|
);
|
|
318
|
-
children.push(locationNode);
|
|
319
357
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
358
|
+
for (let index = 0; index < this._fields.length; index++) {
|
|
359
|
+
const field = this._fields[index];
|
|
360
|
+
const attrs: Record<string, string> = { showAll: '0' };
|
|
361
|
+
|
|
362
|
+
if (rowFieldIndexes.includes(index)) {
|
|
363
|
+
attrs.axis = 'axisRow';
|
|
364
|
+
} else if (colFieldIndexes.includes(index)) {
|
|
365
|
+
attrs.axis = 'axisCol';
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (valueFieldIndexes.has(index)) {
|
|
369
|
+
attrs.dataField = '1';
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const sortOrder = this._sortOrders.get(field.name);
|
|
373
|
+
if (sortOrder) {
|
|
374
|
+
attrs.sortType = SORT_TO_XML[sortOrder];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const children: XmlNode[] = [];
|
|
378
|
+
if (rowFieldIndexes.includes(index) || colFieldIndexes.includes(index)) {
|
|
379
|
+
const distinctItems = cacheData.distinctItemsByField[index] ?? [];
|
|
380
|
+
const itemNodes: XmlNode[] = distinctItems.map((_item, itemIndex) =>
|
|
381
|
+
createElement('item', { x: String(itemIndex) }, []),
|
|
382
|
+
);
|
|
383
|
+
itemNodes.push(createElement('item', { t: 'default' }, []));
|
|
384
|
+
children.push(createElement('items', { count: String(itemNodes.length) }, itemNodes));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
pivotFields.push(createElement('pivotField', attrs, children));
|
|
325
388
|
}
|
|
326
|
-
children.push(createElement('pivotFields', { count: String(pivotFieldNodes.length) }, pivotFieldNodes));
|
|
327
389
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
390
|
+
const children: XmlNode[] = [];
|
|
391
|
+
|
|
392
|
+
const locationRef = this._buildTargetAreaRef(cacheData);
|
|
393
|
+
children.push(
|
|
394
|
+
createElement(
|
|
395
|
+
'location',
|
|
396
|
+
{
|
|
397
|
+
ref: locationRef,
|
|
398
|
+
firstHeaderRow: '1',
|
|
399
|
+
firstDataRow: '1',
|
|
400
|
+
firstDataCol: String(Math.max(1, this._rowFields.length + 1)),
|
|
401
|
+
},
|
|
402
|
+
[],
|
|
403
|
+
),
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
children.push(createElement('pivotFields', { count: String(sourceFieldCount) }, pivotFields));
|
|
332
407
|
|
|
333
|
-
|
|
334
|
-
|
|
408
|
+
if (rowFieldIndexes.length > 0) {
|
|
409
|
+
children.push(
|
|
410
|
+
createElement(
|
|
411
|
+
'rowFields',
|
|
412
|
+
{ count: String(rowFieldIndexes.length) },
|
|
413
|
+
rowFieldIndexes.map((fieldIndex) => createElement('field', { x: String(fieldIndex) }, [])),
|
|
414
|
+
),
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const distinctRowItems = cacheData.distinctItemsByField[rowFieldIndexes[0]] ?? [];
|
|
418
|
+
const rowItemNodes: XmlNode[] = [];
|
|
419
|
+
if (distinctRowItems.length > 0) {
|
|
420
|
+
rowItemNodes.push(createElement('i', {}, [createElement('x', {}, [])]));
|
|
421
|
+
for (let itemIndex = 1; itemIndex < distinctRowItems.length; itemIndex++) {
|
|
422
|
+
rowItemNodes.push(createElement('i', {}, [createElement('x', { v: String(itemIndex) }, [])]));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
rowItemNodes.push(createElement('i', { t: 'grand' }, [createElement('x', {}, [])]));
|
|
335
426
|
children.push(createElement('rowItems', { count: String(rowItemNodes.length) }, rowItemNodes));
|
|
336
427
|
}
|
|
337
428
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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', {}, [])]));
|
|
429
|
+
if (colFieldIndexes.length > 0) {
|
|
430
|
+
children.push(
|
|
431
|
+
createElement(
|
|
432
|
+
'colFields',
|
|
433
|
+
{ count: String(colFieldIndexes.length) },
|
|
434
|
+
colFieldIndexes.map((fieldIndex) => createElement('field', { x: String(fieldIndex) }, [])),
|
|
435
|
+
),
|
|
436
|
+
);
|
|
363
437
|
}
|
|
364
438
|
|
|
365
|
-
//
|
|
439
|
+
// Excel expects colItems even when no explicit column fields are configured.
|
|
440
|
+
children.push(createElement('colItems', { count: '1' }, [createElement('i', {}, [])]));
|
|
441
|
+
|
|
366
442
|
if (this._filterFields.length > 0) {
|
|
367
|
-
|
|
368
|
-
createElement(
|
|
443
|
+
children.push(
|
|
444
|
+
createElement(
|
|
445
|
+
'pageFields',
|
|
446
|
+
{ count: String(this._filterFields.length) },
|
|
447
|
+
this._filterFields.map((field, index) =>
|
|
448
|
+
createElement('pageField', { fld: String(this._fieldIndex(field)), hier: '-1', item: String(index) }, []),
|
|
449
|
+
),
|
|
450
|
+
),
|
|
369
451
|
);
|
|
370
|
-
|
|
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
|
-
}
|
|
452
|
+
}
|
|
387
453
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
454
|
+
if (effectiveValueFields.length > 0) {
|
|
455
|
+
children.push(
|
|
456
|
+
createElement(
|
|
457
|
+
'dataFields',
|
|
458
|
+
{ count: String(effectiveValueFields.length) },
|
|
459
|
+
effectiveValueFields.map((valueField) => {
|
|
460
|
+
const attrs: Record<string, string> = {
|
|
461
|
+
name: valueField.name,
|
|
462
|
+
fld: String(this._fieldIndex(valueField.field)),
|
|
463
|
+
baseField: '0',
|
|
464
|
+
baseItem: '0',
|
|
465
|
+
subtotal: AGGREGATION_TO_XML[valueField.aggregation],
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
if (valueField.numberFormat) {
|
|
469
|
+
attrs.numFmtId = String(this._workbook.styles.getOrCreateNumFmtId(valueField.numberFormat));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return createElement('dataField', attrs, []);
|
|
473
|
+
}),
|
|
474
|
+
),
|
|
475
|
+
);
|
|
391
476
|
}
|
|
392
477
|
|
|
393
|
-
// Pivot table style
|
|
394
478
|
children.push(
|
|
395
479
|
createElement(
|
|
396
480
|
'pivotTableStyleInfo',
|
|
@@ -406,275 +490,286 @@ export class PivotTable {
|
|
|
406
490
|
),
|
|
407
491
|
);
|
|
408
492
|
|
|
409
|
-
const
|
|
410
|
-
'
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
multipleFieldFilters: '0',
|
|
434
|
-
},
|
|
435
|
-
children,
|
|
436
|
-
);
|
|
493
|
+
const attrs: Record<string, string> = {
|
|
494
|
+
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
495
|
+
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
496
|
+
name: this._name,
|
|
497
|
+
cacheId: String(this._cacheId),
|
|
498
|
+
dataCaption: 'Values',
|
|
499
|
+
applyNumberFormats: '1',
|
|
500
|
+
applyBorderFormats: '0',
|
|
501
|
+
applyFontFormats: '0',
|
|
502
|
+
applyPatternFormats: '0',
|
|
503
|
+
applyAlignmentFormats: '0',
|
|
504
|
+
applyWidthHeightFormats: '1',
|
|
505
|
+
updatedVersion: '8',
|
|
506
|
+
minRefreshableVersion: '3',
|
|
507
|
+
createdVersion: '8',
|
|
508
|
+
useAutoFormatting: '1',
|
|
509
|
+
rowGrandTotals: '1',
|
|
510
|
+
colGrandTotals: '1',
|
|
511
|
+
itemPrintTitles: '1',
|
|
512
|
+
indent: '0',
|
|
513
|
+
multipleFieldFilters: this._filters.size > 0 ? '1' : '0',
|
|
514
|
+
outline: '1',
|
|
515
|
+
outlineData: '1',
|
|
516
|
+
};
|
|
437
517
|
|
|
438
|
-
|
|
518
|
+
const root = createElement('pivotTableDefinition', attrs, children);
|
|
519
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([root])}`;
|
|
439
520
|
}
|
|
440
521
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const
|
|
446
|
-
|
|
522
|
+
private _buildCacheFieldNode(field: PivotFieldMeta, fieldIndex: number, cacheData: PivotCacheData): XmlNode {
|
|
523
|
+
const info = cacheData.numericInfoByField[fieldIndex];
|
|
524
|
+
const isAxisField = cacheData.isAxisFieldByIndex[fieldIndex];
|
|
525
|
+
const isValueField = cacheData.isValueFieldByIndex[fieldIndex];
|
|
526
|
+
const allNonNullAreNumbers = info.nonNullCount > 0 && info.numericCount === info.nonNullCount;
|
|
527
|
+
|
|
528
|
+
if (isValueField || (!isAxisField && allNonNullAreNumbers)) {
|
|
529
|
+
const minValue = info.hasNumeric ? info.min : 0;
|
|
530
|
+
const maxValue = info.hasNumeric ? info.max : 0;
|
|
531
|
+
const hasInteger = info.hasNumeric ? info.allIntegers : true;
|
|
532
|
+
|
|
533
|
+
const attrs: Record<string, string> = {
|
|
534
|
+
containsSemiMixedTypes: '0',
|
|
535
|
+
containsString: '0',
|
|
536
|
+
containsNumber: '1',
|
|
537
|
+
minValue: String(minValue),
|
|
538
|
+
maxValue: String(maxValue),
|
|
539
|
+
};
|
|
540
|
+
if (hasInteger) {
|
|
541
|
+
attrs.containsInteger = '1';
|
|
542
|
+
}
|
|
447
543
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
const valueField = this._valueFields.find((f) => f.fieldIndex === fieldIndex);
|
|
544
|
+
return createElement('cacheField', { name: field.name, numFmtId: '0' }, [
|
|
545
|
+
createElement('sharedItems', attrs, []),
|
|
546
|
+
]);
|
|
547
|
+
}
|
|
453
548
|
|
|
454
|
-
|
|
455
|
-
|
|
549
|
+
if (!isAxisField) {
|
|
550
|
+
return createElement('cacheField', { name: field.name, numFmtId: '0' }, [createElement('sharedItems', {}, [])]);
|
|
551
|
+
}
|
|
456
552
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
553
|
+
const sharedItems = cacheData.sharedItemsByField[fieldIndex] ?? [];
|
|
554
|
+
return createElement('cacheField', { name: field.name, numFmtId: '0' }, [
|
|
555
|
+
createElement('sharedItems', { count: String(sharedItems.length) }, sharedItems),
|
|
556
|
+
]);
|
|
557
|
+
}
|
|
460
558
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
559
|
+
private _buildTargetAreaRef(cacheData: PivotCacheData): string {
|
|
560
|
+
const start = this._targetCell;
|
|
561
|
+
const estimatedRows = Math.max(3, this._estimateOutputRows(cacheData));
|
|
562
|
+
const estimatedCols = Math.max(1, this._rowFields.length + Math.max(1, this._valueFields.length));
|
|
465
563
|
|
|
466
|
-
|
|
467
|
-
|
|
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';
|
|
564
|
+
const endRow = start.row + estimatedRows - 1;
|
|
565
|
+
const endCol = start.col + estimatedCols - 1;
|
|
475
566
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
attrs.sortType = colField.sortOrder === 'asc' ? 'ascending' : 'descending';
|
|
479
|
-
}
|
|
567
|
+
return `${toAddress(start.row, start.col)}:${toAddress(endRow, endCol)}`;
|
|
568
|
+
}
|
|
480
569
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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';
|
|
570
|
+
private _estimateOutputRows(cacheData: PivotCacheData): number {
|
|
571
|
+
if (this._rowFields.length === 0) {
|
|
572
|
+
return 3;
|
|
499
573
|
}
|
|
500
574
|
|
|
501
|
-
|
|
575
|
+
const rowFieldIndex = this._fieldIndex(this._rowFields[0]);
|
|
576
|
+
const distinctItems = cacheData.distinctItemsByField[rowFieldIndex] ?? [];
|
|
577
|
+
return Math.max(3, distinctItems.length + 2);
|
|
502
578
|
}
|
|
503
579
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
580
|
+
private _buildPivotCacheData(): PivotCacheData {
|
|
581
|
+
const rowCount = Math.max(0, this._sourceRange.end.row - this._sourceRange.start.row);
|
|
582
|
+
const fieldCount = this._fields.length;
|
|
583
|
+
const recordNodes: XmlNode[] = new Array(rowCount);
|
|
584
|
+
const sharedItemIndexByField: Array<Map<string, number> | null> = new Array(fieldCount).fill(null);
|
|
585
|
+
const sharedItemsByField: Array<XmlNode[] | null> = new Array(fieldCount).fill(null);
|
|
586
|
+
const distinctItemsByField: Array<Exclude<CellValue, null>[] | null> = new Array(fieldCount).fill(null);
|
|
587
|
+
const numericInfoByField: PivotNumericInfo[] = new Array(fieldCount);
|
|
588
|
+
const isAxisFieldByIndex: boolean[] = new Array(fieldCount);
|
|
589
|
+
const isValueFieldByIndex: boolean[] = new Array(fieldCount);
|
|
590
|
+
|
|
591
|
+
const effectiveRowField = this._rowFields[0] ?? null;
|
|
592
|
+
const effectiveColumnField = this._columnFields[0] ?? null;
|
|
593
|
+
const filterFields = new Set(this._filterFields);
|
|
594
|
+
const valueFields = new Set(this._valueFields.map((valueField) => valueField.field));
|
|
595
|
+
|
|
596
|
+
for (let fieldIndex = 0; fieldIndex < fieldCount; fieldIndex++) {
|
|
597
|
+
const fieldName = this._fields[fieldIndex].name;
|
|
598
|
+
const isAxisField =
|
|
599
|
+
fieldName === effectiveRowField || fieldName === effectiveColumnField || filterFields.has(fieldName);
|
|
600
|
+
const isValueField = valueFields.has(fieldName);
|
|
601
|
+
|
|
602
|
+
isAxisFieldByIndex[fieldIndex] = isAxisField;
|
|
603
|
+
isValueFieldByIndex[fieldIndex] = isValueField;
|
|
604
|
+
|
|
605
|
+
if (isAxisField) {
|
|
606
|
+
sharedItemIndexByField[fieldIndex] = new Map<string, number>();
|
|
607
|
+
sharedItemsByField[fieldIndex] = [];
|
|
608
|
+
distinctItemsByField[fieldIndex] = [];
|
|
525
609
|
}
|
|
526
610
|
|
|
527
|
-
|
|
611
|
+
numericInfoByField[fieldIndex] = {
|
|
612
|
+
nonNullCount: 0,
|
|
613
|
+
numericCount: 0,
|
|
614
|
+
min: 0,
|
|
615
|
+
max: 0,
|
|
616
|
+
hasNumeric: false,
|
|
617
|
+
allIntegers: true,
|
|
618
|
+
};
|
|
528
619
|
}
|
|
529
620
|
|
|
530
|
-
|
|
531
|
-
|
|
621
|
+
for (let rowOffset = 0; rowOffset < rowCount; rowOffset++) {
|
|
622
|
+
const row = this._sourceRange.start.row + 1 + rowOffset;
|
|
623
|
+
const valueNodes: XmlNode[] = [];
|
|
624
|
+
|
|
625
|
+
for (let fieldIndex = 0; fieldIndex < fieldCount; fieldIndex++) {
|
|
626
|
+
const field = this._fields[fieldIndex];
|
|
627
|
+
const cellValue = this._sourceSheet.getCellIfExists(row, field.sourceCol)?.value ?? null;
|
|
628
|
+
|
|
629
|
+
if (cellValue !== null) {
|
|
630
|
+
const numericInfo = numericInfoByField[fieldIndex];
|
|
631
|
+
numericInfo.nonNullCount++;
|
|
632
|
+
|
|
633
|
+
if (typeof cellValue === 'number' && Number.isFinite(cellValue)) {
|
|
634
|
+
numericInfo.numericCount++;
|
|
635
|
+
if (!numericInfo.hasNumeric) {
|
|
636
|
+
numericInfo.min = cellValue;
|
|
637
|
+
numericInfo.max = cellValue;
|
|
638
|
+
numericInfo.hasNumeric = true;
|
|
639
|
+
} else {
|
|
640
|
+
if (cellValue < numericInfo.min) numericInfo.min = cellValue;
|
|
641
|
+
if (cellValue > numericInfo.max) numericInfo.max = cellValue;
|
|
642
|
+
}
|
|
643
|
+
if (!Number.isInteger(cellValue)) {
|
|
644
|
+
numericInfo.allIntegers = false;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
532
647
|
|
|
533
|
-
|
|
534
|
-
|
|
648
|
+
if (isAxisFieldByIndex[fieldIndex]) {
|
|
649
|
+
const distinctMap = sharedItemIndexByField[fieldIndex]!;
|
|
650
|
+
const key = this._distinctKey(cellValue as Exclude<CellValue, null>);
|
|
651
|
+
let index = distinctMap.get(key);
|
|
652
|
+
if (index === undefined) {
|
|
653
|
+
index = distinctMap.size;
|
|
654
|
+
distinctMap.set(key, index);
|
|
655
|
+
distinctItemsByField[fieldIndex]!.push(cellValue as Exclude<CellValue, null>);
|
|
656
|
+
const sharedNode = this._buildSharedItemNode(cellValue as Exclude<CellValue, null>);
|
|
657
|
+
if (sharedNode) {
|
|
658
|
+
sharedItemsByField[fieldIndex]!.push(sharedNode);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
valueNodes.push(createElement('x', { v: String(index) }, []));
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
535
665
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
*/
|
|
539
|
-
private _buildRowItems(): XmlNode[] {
|
|
540
|
-
const items: XmlNode[] = [];
|
|
666
|
+
valueNodes.push(this._buildRawCacheValueNode(cellValue));
|
|
667
|
+
}
|
|
541
668
|
|
|
542
|
-
|
|
669
|
+
recordNodes[rowOffset] = createElement('r', {}, valueNodes);
|
|
670
|
+
}
|
|
543
671
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
672
|
+
return {
|
|
673
|
+
rowCount,
|
|
674
|
+
recordNodes,
|
|
675
|
+
sharedItemIndexByField,
|
|
676
|
+
sharedItemsByField,
|
|
677
|
+
distinctItemsByField,
|
|
678
|
+
numericInfoByField,
|
|
679
|
+
isAxisFieldByIndex,
|
|
680
|
+
isValueFieldByIndex,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
547
683
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
}
|
|
684
|
+
private _buildSharedItemNode(value: Exclude<CellValue, null>): XmlNode | null {
|
|
685
|
+
if (typeof value === 'string') {
|
|
686
|
+
return { s: [], ':@': { '@_v': value } } as XmlNode;
|
|
552
687
|
}
|
|
553
688
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
return items;
|
|
558
|
-
}
|
|
689
|
+
if (typeof value === 'number') {
|
|
690
|
+
return createElement('n', { v: String(value) }, []);
|
|
691
|
+
}
|
|
559
692
|
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
}
|
|
693
|
+
if (typeof value === 'boolean') {
|
|
694
|
+
return createElement('b', { v: value ? '1' : '0' }, []);
|
|
590
695
|
}
|
|
591
696
|
|
|
592
|
-
|
|
593
|
-
|
|
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', {}, [])]));
|
|
697
|
+
if (value instanceof Date) {
|
|
698
|
+
return createElement('d', { v: value.toISOString() }, []);
|
|
604
699
|
}
|
|
605
700
|
|
|
606
|
-
return
|
|
701
|
+
return null;
|
|
607
702
|
}
|
|
608
703
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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;
|
|
704
|
+
private _buildRawCacheValueNode(value: CellValue): XmlNode {
|
|
705
|
+
if (value === null) {
|
|
706
|
+
return createElement('m', {}, []);
|
|
707
|
+
}
|
|
621
708
|
|
|
622
|
-
|
|
623
|
-
|
|
709
|
+
if (typeof value === 'string') {
|
|
710
|
+
return { s: [], ':@': { '@_v': value } } as XmlNode;
|
|
711
|
+
}
|
|
624
712
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
private _estimateRowCount(): number {
|
|
629
|
-
let count = 1; // Header row
|
|
713
|
+
if (typeof value === 'number') {
|
|
714
|
+
return createElement('n', { v: String(value) }, []);
|
|
715
|
+
}
|
|
630
716
|
|
|
631
|
-
|
|
632
|
-
|
|
717
|
+
if (typeof value === 'boolean') {
|
|
718
|
+
return createElement('b', { v: value ? '1' : '0' }, []);
|
|
719
|
+
}
|
|
633
720
|
|
|
634
|
-
|
|
635
|
-
|
|
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
|
|
721
|
+
if (value instanceof Date) {
|
|
722
|
+
return createElement('d', { v: value.toISOString() }, []);
|
|
641
723
|
}
|
|
642
724
|
|
|
643
|
-
return
|
|
725
|
+
return createElement('m', {}, []);
|
|
644
726
|
}
|
|
645
727
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
// Row label columns
|
|
653
|
-
count += Math.max(this._rowFields.length, 1);
|
|
728
|
+
private _assertFieldExists(fieldName: string): void {
|
|
729
|
+
if (!this._fields.some((field) => field.name === fieldName)) {
|
|
730
|
+
throw new Error(`Pivot field not found: ${fieldName}`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
654
733
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
count += (cacheField?.sharedItems.length || 1) + 1; // +1 for grand total
|
|
660
|
-
} else {
|
|
661
|
-
// Value columns
|
|
662
|
-
count += Math.max(this._valueFields.length, 1);
|
|
734
|
+
private _fieldIndex(fieldName: string): number {
|
|
735
|
+
const index = this._fields.findIndex((field) => field.name === fieldName);
|
|
736
|
+
if (index < 0) {
|
|
737
|
+
throw new Error(`Pivot field not found: ${fieldName}`);
|
|
663
738
|
}
|
|
739
|
+
return index;
|
|
740
|
+
}
|
|
664
741
|
|
|
665
|
-
|
|
742
|
+
private _aggregationLabel(aggregation: PivotAggregationType): string {
|
|
743
|
+
switch (aggregation) {
|
|
744
|
+
case 'sum':
|
|
745
|
+
return 'Sum';
|
|
746
|
+
case 'count':
|
|
747
|
+
return 'Count';
|
|
748
|
+
case 'average':
|
|
749
|
+
return 'Average';
|
|
750
|
+
case 'min':
|
|
751
|
+
return 'Min';
|
|
752
|
+
case 'max':
|
|
753
|
+
return 'Max';
|
|
754
|
+
}
|
|
666
755
|
}
|
|
667
756
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
}
|
|
678
|
-
|
|
757
|
+
private _distinctKey(value: Exclude<CellValue, null>): string {
|
|
758
|
+
if (value instanceof Date) {
|
|
759
|
+
return `d:${value.toISOString()}`;
|
|
760
|
+
}
|
|
761
|
+
if (typeof value === 'string') {
|
|
762
|
+
return `s:${value}`;
|
|
763
|
+
}
|
|
764
|
+
if (typeof value === 'number') {
|
|
765
|
+
return `n:${value}`;
|
|
766
|
+
}
|
|
767
|
+
if (typeof value === 'boolean') {
|
|
768
|
+
return `b:${value ? 1 : 0}`;
|
|
769
|
+
}
|
|
770
|
+
if (typeof value === 'object' && value && 'error' in value) {
|
|
771
|
+
return `e:${value.error}`;
|
|
772
|
+
}
|
|
773
|
+
return 'u:';
|
|
679
774
|
}
|
|
680
775
|
}
|