@cj-tech-master/excelts 2.0.1 → 3.0.0-canary.20251228183403.d3eb98d
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/excelts.esm.js +181 -133
- package/dist/browser/excelts.esm.js.map +1 -1
- package/dist/browser/excelts.esm.min.js +30 -24
- package/dist/browser/excelts.iife.js +181 -133
- package/dist/browser/excelts.iife.js.map +1 -1
- package/dist/browser/excelts.iife.min.js +30 -24
- package/dist/cjs/doc/pivot-table.js +47 -6
- package/dist/cjs/doc/worksheet.js +0 -1
- package/dist/cjs/stream/xlsx/worksheet-writer.js +0 -1
- package/dist/cjs/xlsx/xform/book/sheet-xform.js +3 -2
- package/dist/cjs/xlsx/xform/book/workbook-properties-xform.js +1 -1
- package/dist/cjs/xlsx/xform/core/content-types-xform.js +12 -6
- package/dist/cjs/xlsx/xform/pivot-table/cache-field-xform.js +17 -21
- package/dist/cjs/xlsx/xform/pivot-table/cache-field.js +43 -7
- package/dist/cjs/xlsx/xform/pivot-table/pivot-cache-definition-xform.js +1 -10
- package/dist/cjs/xlsx/xform/pivot-table/pivot-cache-records-xform.js +1 -1
- package/dist/cjs/xlsx/xform/pivot-table/pivot-table-xform.js +51 -30
- package/dist/cjs/xlsx/xform/sheet/page-setup-xform.js +4 -3
- package/dist/cjs/xlsx/xform/sheet/row-xform.js +1 -1
- package/dist/cjs/xlsx/xform/sheet/sheet-format-properties-xform.js +6 -3
- package/dist/cjs/xlsx/xform/sheet/sheet-view-xform.js +8 -4
- package/dist/cjs/xlsx/xform/sheet/worksheet-xform.js +2 -2
- package/dist/cjs/xlsx/xform/style/font-xform.js +36 -23
- package/dist/cjs/xlsx/xform/table/auto-filter-xform.js +3 -1
- package/dist/cjs/xlsx/xform/table/table-column-xform.js +2 -1
- package/dist/cjs/xlsx/xform/table/table-xform.js +5 -9
- package/dist/esm/doc/pivot-table.js +47 -6
- package/dist/esm/doc/worksheet.js +0 -1
- package/dist/esm/stream/xlsx/worksheet-writer.js +0 -1
- package/dist/esm/xlsx/xform/book/sheet-xform.js +3 -2
- package/dist/esm/xlsx/xform/book/workbook-properties-xform.js +1 -1
- package/dist/esm/xlsx/xform/core/content-types-xform.js +12 -6
- package/dist/esm/xlsx/xform/pivot-table/cache-field-xform.js +17 -21
- package/dist/esm/xlsx/xform/pivot-table/cache-field.js +43 -7
- package/dist/esm/xlsx/xform/pivot-table/pivot-cache-definition-xform.js +1 -10
- package/dist/esm/xlsx/xform/pivot-table/pivot-cache-records-xform.js +1 -1
- package/dist/esm/xlsx/xform/pivot-table/pivot-table-xform.js +51 -30
- package/dist/esm/xlsx/xform/sheet/page-setup-xform.js +4 -3
- package/dist/esm/xlsx/xform/sheet/row-xform.js +1 -1
- package/dist/esm/xlsx/xform/sheet/sheet-format-properties-xform.js +6 -3
- package/dist/esm/xlsx/xform/sheet/sheet-view-xform.js +8 -4
- package/dist/esm/xlsx/xform/sheet/worksheet-xform.js +2 -2
- package/dist/esm/xlsx/xform/style/font-xform.js +36 -23
- package/dist/esm/xlsx/xform/table/auto-filter-xform.js +3 -1
- package/dist/esm/xlsx/xform/table/table-column-xform.js +2 -1
- package/dist/esm/xlsx/xform/table/table-xform.js +5 -9
- package/dist/types/doc/pivot-table.d.ts +5 -1
- package/dist/types/stream/xlsx/worksheet-writer.d.ts +1 -1
- package/dist/types/types.d.ts +1 -1
- package/dist/types/xlsx/xform/pivot-table/cache-field-xform.d.ts +1 -1
- package/dist/types/xlsx/xform/pivot-table/cache-field.d.ts +6 -2
- package/dist/types/xlsx/xform/pivot-table/pivot-cache-definition-xform.d.ts +0 -5
- package/dist/types/xlsx/xform/pivot-table/pivot-table-xform.d.ts +0 -3
- package/dist/types/xlsx/xform/sheet/sheet-format-properties-xform.d.ts +1 -1
- package/dist/types/xlsx/xform/sheet/worksheet-xform.d.ts +1 -1
- package/dist/types/xlsx/xform/style/font-xform.d.ts +1 -0
- package/dist/types/xlsx/xform/table/table-xform.d.ts +0 -4
- package/package.json +1 -1
|
@@ -37,8 +37,11 @@ function createTableSourceAdapter(table) {
|
|
|
37
37
|
const endRow = startRow + tableModel.rows.length; // header row + data rows
|
|
38
38
|
const endCol = startCol + columnNames.length - 1;
|
|
39
39
|
const shortRange = col_cache_1.colCache.encode(startRow, startCol, endRow, endCol);
|
|
40
|
+
// Use the worksheet name (not table name) for pivotCacheDefinition's worksheetSource
|
|
41
|
+
// The sheet attribute in worksheetSource must reference the actual worksheet name
|
|
42
|
+
const worksheetName = table.worksheet.name;
|
|
40
43
|
return {
|
|
41
|
-
name:
|
|
44
|
+
name: worksheetName,
|
|
42
45
|
getRow(rowNumber) {
|
|
43
46
|
if (rowNumber === 1) {
|
|
44
47
|
return { values: headerRow };
|
|
@@ -111,7 +114,7 @@ function makePivotTable(worksheet, model) {
|
|
|
111
114
|
validate(worksheet, model, source);
|
|
112
115
|
const { rows, values } = model;
|
|
113
116
|
const columns = model.columns ?? [];
|
|
114
|
-
const cacheFields = makeCacheFields(source, [...rows, ...columns]);
|
|
117
|
+
const cacheFields = makeCacheFields(source, [...rows, ...columns], values);
|
|
115
118
|
const nameToIndex = cacheFields.reduce((result, cacheField, index) => {
|
|
116
119
|
result[cacheField.name] = index;
|
|
117
120
|
return result;
|
|
@@ -168,13 +171,15 @@ function validate(_worksheet, model, source) {
|
|
|
168
171
|
throw new Error("It is currently not possible to have multiple values when columns are specified. Please either supply an empty array for columns or a single value.");
|
|
169
172
|
}
|
|
170
173
|
}
|
|
171
|
-
function makeCacheFields(source, fieldNamesWithSharedItems) {
|
|
174
|
+
function makeCacheFields(source, fieldNamesWithSharedItems, valueFieldNames) {
|
|
172
175
|
// Cache fields are used in pivot tables to reference source data.
|
|
173
176
|
// Fields in fieldNamesWithSharedItems get their unique values extracted as sharedItems.
|
|
174
|
-
//
|
|
177
|
+
// Fields in valueFieldNames (but not in fieldNamesWithSharedItems) get min/max calculated.
|
|
178
|
+
// Other fields are unused and get empty sharedItems.
|
|
175
179
|
const names = source.getRow(1).values;
|
|
176
180
|
// Use Set for O(1) lookup instead of object
|
|
177
181
|
const sharedItemsFields = new Set(fieldNamesWithSharedItems);
|
|
182
|
+
const valueFields = new Set(valueFieldNames);
|
|
178
183
|
const aggregate = (columnIndex) => {
|
|
179
184
|
const columnValues = source.getColumn(columnIndex).values;
|
|
180
185
|
// Build unique values set directly, skipping header (index 0,1) and null/undefined
|
|
@@ -187,12 +192,48 @@ function makeCacheFields(source, fieldNamesWithSharedItems) {
|
|
|
187
192
|
}
|
|
188
193
|
return (0, utils_1.toSortedArray)(uniqueValues);
|
|
189
194
|
};
|
|
195
|
+
// Calculate min/max for numeric fields
|
|
196
|
+
const getMinMax = (columnIndex) => {
|
|
197
|
+
const columnValues = source.getColumn(columnIndex).values;
|
|
198
|
+
let min = Infinity;
|
|
199
|
+
let max = -Infinity;
|
|
200
|
+
let hasNumeric = false;
|
|
201
|
+
for (let i = 2; i < columnValues.length; i++) {
|
|
202
|
+
const v = columnValues[i];
|
|
203
|
+
if (typeof v === "number" && !isNaN(v)) {
|
|
204
|
+
hasNumeric = true;
|
|
205
|
+
if (v < min) {
|
|
206
|
+
min = v;
|
|
207
|
+
}
|
|
208
|
+
if (v > max) {
|
|
209
|
+
max = v;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return hasNumeric ? { minValue: min, maxValue: max } : null;
|
|
214
|
+
};
|
|
190
215
|
// Build result array
|
|
191
216
|
const result = [];
|
|
192
217
|
for (const columnIndex of (0, utils_1.range)(1, names.length)) {
|
|
193
218
|
const name = names[columnIndex];
|
|
194
|
-
|
|
195
|
-
|
|
219
|
+
if (sharedItemsFields.has(name)) {
|
|
220
|
+
// Field used for rows/columns - extract unique values as sharedItems
|
|
221
|
+
result.push({ name, sharedItems: aggregate(columnIndex) });
|
|
222
|
+
}
|
|
223
|
+
else if (valueFields.has(name)) {
|
|
224
|
+
// Field used only for values (aggregation) - calculate min/max
|
|
225
|
+
const minMax = getMinMax(columnIndex);
|
|
226
|
+
result.push({
|
|
227
|
+
name,
|
|
228
|
+
sharedItems: null,
|
|
229
|
+
minValue: minMax?.minValue,
|
|
230
|
+
maxValue: minMax?.maxValue
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
// Unused field - just empty sharedItems (like Excel does)
|
|
235
|
+
result.push({ name, sharedItems: null });
|
|
236
|
+
}
|
|
196
237
|
}
|
|
197
238
|
return result;
|
|
198
239
|
}
|
|
@@ -6,9 +6,10 @@ const base_xform_1 = require("../base-xform");
|
|
|
6
6
|
class WorksheetXform extends base_xform_1.BaseXform {
|
|
7
7
|
render(xmlStream, model) {
|
|
8
8
|
xmlStream.leafNode("sheet", {
|
|
9
|
-
sheetId: model.id,
|
|
10
9
|
name: model.name,
|
|
11
|
-
|
|
10
|
+
sheetId: model.id,
|
|
11
|
+
// Excel doesn't output state when it's 'visible' (default)
|
|
12
|
+
state: model.state === "visible" ? undefined : model.state,
|
|
12
13
|
"r:id": model.rId
|
|
13
14
|
});
|
|
14
15
|
}
|
|
@@ -6,7 +6,7 @@ class WorkbookPropertiesXform extends base_xform_1.BaseXform {
|
|
|
6
6
|
render(xmlStream, model) {
|
|
7
7
|
xmlStream.leafNode("workbookPr", {
|
|
8
8
|
date1904: model.date1904 ? 1 : undefined,
|
|
9
|
-
defaultThemeVersion
|
|
9
|
+
// Excel doesn't output defaultThemeVersion
|
|
10
10
|
filterPrivacy: 1
|
|
11
11
|
});
|
|
12
12
|
}
|
|
@@ -41,7 +41,7 @@ class ContentTypesXform extends base_xform_1.BaseXform {
|
|
|
41
41
|
});
|
|
42
42
|
});
|
|
43
43
|
if ((model.pivotTables || []).length) {
|
|
44
|
-
// Add content types for
|
|
44
|
+
// Add content types for pivot cache (definition and records)
|
|
45
45
|
(model.pivotTables || []).forEach((pivotTable) => {
|
|
46
46
|
const n = pivotTable.tableNumber;
|
|
47
47
|
xmlStream.leafNode("Override", {
|
|
@@ -52,10 +52,6 @@ class ContentTypesXform extends base_xform_1.BaseXform {
|
|
|
52
52
|
PartName: `/xl/pivotCache/pivotCacheRecords${n}.xml`,
|
|
53
53
|
ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml"
|
|
54
54
|
});
|
|
55
|
-
xmlStream.leafNode("Override", {
|
|
56
|
-
PartName: `/xl/pivotTables/pivotTable${n}.xml`,
|
|
57
|
-
ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"
|
|
58
|
-
});
|
|
59
55
|
});
|
|
60
56
|
}
|
|
61
57
|
xmlStream.leafNode("Override", {
|
|
@@ -81,6 +77,16 @@ class ContentTypesXform extends base_xform_1.BaseXform {
|
|
|
81
77
|
});
|
|
82
78
|
});
|
|
83
79
|
}
|
|
80
|
+
// Add pivot table overrides after tables (matches Excel order)
|
|
81
|
+
if ((model.pivotTables || []).length) {
|
|
82
|
+
(model.pivotTables || []).forEach((pivotTable) => {
|
|
83
|
+
const n = pivotTable.tableNumber;
|
|
84
|
+
xmlStream.leafNode("Override", {
|
|
85
|
+
PartName: `/xl/pivotTables/pivotTable${n}.xml`,
|
|
86
|
+
ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
84
90
|
if (model.drawings) {
|
|
85
91
|
model.drawings.forEach((drawing) => {
|
|
86
92
|
xmlStream.leafNode("Override", {
|
|
@@ -89,7 +95,7 @@ class ContentTypesXform extends base_xform_1.BaseXform {
|
|
|
89
95
|
});
|
|
90
96
|
});
|
|
91
97
|
}
|
|
92
|
-
if (model.commentRefs) {
|
|
98
|
+
if (model.commentRefs && model.commentRefs.length) {
|
|
93
99
|
xmlStream.leafNode("Default", {
|
|
94
100
|
Extension: "vml",
|
|
95
101
|
ContentType: "application/vnd.openxmlformats-officedocument.vmlDrawing"
|
|
@@ -47,28 +47,24 @@ class CacheFieldXform extends base_xform_1.BaseXform {
|
|
|
47
47
|
break;
|
|
48
48
|
case "sharedItems":
|
|
49
49
|
this.inSharedItems = true;
|
|
50
|
-
//
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
this.model.minValue = parseFloat(attributes.minValue);
|
|
57
|
-
}
|
|
58
|
-
if (attributes.maxValue !== undefined) {
|
|
59
|
-
this.model.maxValue = parseFloat(attributes.maxValue);
|
|
60
|
-
}
|
|
61
|
-
// Numeric fields have sharedItems = null
|
|
62
|
-
this.model.sharedItems = null;
|
|
50
|
+
// Store numeric field metadata
|
|
51
|
+
if (this.model) {
|
|
52
|
+
this.model.containsNumber = attributes.containsNumber === "1";
|
|
53
|
+
this.model.containsInteger = attributes.containsInteger === "1";
|
|
54
|
+
if (attributes.minValue !== undefined) {
|
|
55
|
+
this.model.minValue = parseFloat(attributes.minValue);
|
|
63
56
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
57
|
+
if (attributes.maxValue !== undefined) {
|
|
58
|
+
this.model.maxValue = parseFloat(attributes.maxValue);
|
|
59
|
+
}
|
|
60
|
+
// Initialize sharedItems array if count > 0 (for both string and numeric fields)
|
|
61
|
+
const count = parseInt(attributes.count || "0", 10);
|
|
62
|
+
if (count > 0) {
|
|
63
|
+
this.model.sharedItems = [];
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// No count means no individual items (pure numeric field)
|
|
67
|
+
this.model.sharedItems = null;
|
|
72
68
|
}
|
|
73
69
|
}
|
|
74
70
|
break;
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.CacheField = void 0;
|
|
4
4
|
const utils_1 = require("../../../utils/utils");
|
|
5
5
|
class CacheField {
|
|
6
|
-
constructor({ name, sharedItems }) {
|
|
6
|
+
constructor({ name, sharedItems, minValue, maxValue }) {
|
|
7
7
|
// string type
|
|
8
8
|
//
|
|
9
9
|
// {
|
|
@@ -13,28 +13,64 @@ class CacheField {
|
|
|
13
13
|
//
|
|
14
14
|
// or
|
|
15
15
|
//
|
|
16
|
-
// integer type
|
|
16
|
+
// integer type (no sharedItems)
|
|
17
17
|
//
|
|
18
18
|
// {
|
|
19
19
|
// 'name': 'D',
|
|
20
|
-
// 'sharedItems': null
|
|
20
|
+
// 'sharedItems': null,
|
|
21
|
+
// 'minValue': 5,
|
|
22
|
+
// 'maxValue': 45
|
|
23
|
+
// }
|
|
24
|
+
//
|
|
25
|
+
// or
|
|
26
|
+
//
|
|
27
|
+
// numeric type with shared items (used as both row/column and value field)
|
|
28
|
+
//
|
|
29
|
+
// {
|
|
30
|
+
// 'name': 'C',
|
|
31
|
+
// 'sharedItems': [5, 24, 35, 45]
|
|
21
32
|
// }
|
|
22
33
|
this.name = name;
|
|
23
34
|
this.sharedItems = sharedItems;
|
|
35
|
+
this.minValue = minValue;
|
|
36
|
+
this.maxValue = maxValue;
|
|
24
37
|
}
|
|
25
38
|
render() {
|
|
26
39
|
// PivotCache Field: http://www.datypic.com/sc/ooxml/e-ssml_cacheField-1.html
|
|
27
40
|
// Shared Items: http://www.datypic.com/sc/ooxml/e-ssml_sharedItems-1.html
|
|
28
41
|
// Escape XML special characters in name attribute
|
|
29
42
|
const escapedName = (0, utils_1.xmlEncode)(this.name);
|
|
30
|
-
//
|
|
43
|
+
// No shared items - field not used for rows/columns
|
|
31
44
|
if (this.sharedItems === null) {
|
|
32
|
-
//
|
|
45
|
+
// If no minValue/maxValue, this is an unused field - use empty sharedItems like Excel does
|
|
46
|
+
if (this.minValue === undefined || this.maxValue === undefined) {
|
|
47
|
+
return `<cacheField name="${escapedName}" numFmtId="0">
|
|
48
|
+
<sharedItems />
|
|
49
|
+
</cacheField>`;
|
|
50
|
+
}
|
|
51
|
+
// Numeric field used only for values (not rows/columns) - include min/max
|
|
33
52
|
return `<cacheField name="${escapedName}" numFmtId="0">
|
|
34
|
-
<sharedItems containsSemiMixedTypes="0" containsString="0" containsNumber="1" containsInteger="1" />
|
|
53
|
+
<sharedItems containsSemiMixedTypes="0" containsString="0" containsNumber="1" containsInteger="1" minValue="${this.minValue}" maxValue="${this.maxValue}" />
|
|
54
|
+
</cacheField>`;
|
|
55
|
+
}
|
|
56
|
+
// Shared items exist - check if all values are numeric
|
|
57
|
+
// Note: empty array returns true for every(), so check length first
|
|
58
|
+
const allNumeric = this.sharedItems.length > 0 &&
|
|
59
|
+
this.sharedItems.every(item => typeof item === "number" && Number.isFinite(item));
|
|
60
|
+
const allInteger = allNumeric && this.sharedItems.every(item => Number.isInteger(item));
|
|
61
|
+
if (allNumeric) {
|
|
62
|
+
// Numeric shared items - used when field is both a row/column field AND a value field
|
|
63
|
+
// This allows Excel to both group by unique values AND perform aggregation
|
|
64
|
+
const minValue = Math.min(...this.sharedItems);
|
|
65
|
+
const maxValue = Math.max(...this.sharedItems);
|
|
66
|
+
const integerAttr = allInteger ? ' containsInteger="1"' : "";
|
|
67
|
+
return `<cacheField name="${escapedName}" numFmtId="0">
|
|
68
|
+
<sharedItems containsSemiMixedTypes="0" containsString="0" containsNumber="1"${integerAttr} minValue="${minValue}" maxValue="${maxValue}" count="${this.sharedItems.length}">
|
|
69
|
+
${this.sharedItems.map(item => `<n v="${item}" />`).join("")}
|
|
70
|
+
</sharedItems>
|
|
35
71
|
</cacheField>`;
|
|
36
72
|
}
|
|
37
|
-
//
|
|
73
|
+
// String shared items - escape XML special characters in each value
|
|
38
74
|
return `<cacheField name="${escapedName}" numFmtId="0">
|
|
39
75
|
<sharedItems count="${this.sharedItems.length}">
|
|
40
76
|
${this.sharedItems.map(item => `<s v="${(0, utils_1.xmlEncode)(String(item))}" />`).join("")}
|
|
@@ -53,8 +53,6 @@ class PivotCacheDefinitionXform extends base_xform_1.BaseXform {
|
|
|
53
53
|
...PivotCacheDefinitionXform.PIVOT_CACHE_DEFINITION_ATTRIBUTES,
|
|
54
54
|
"r:id": "rId1",
|
|
55
55
|
refreshOnLoad: "1", // important for our implementation to work
|
|
56
|
-
refreshedBy: "Author",
|
|
57
|
-
refreshedDate: "45125.026046874998",
|
|
58
56
|
createdVersion: "8",
|
|
59
57
|
refreshedVersion: "8",
|
|
60
58
|
minRefreshableVersion: "3",
|
|
@@ -82,8 +80,6 @@ class PivotCacheDefinitionXform extends base_xform_1.BaseXform {
|
|
|
82
80
|
...PivotCacheDefinitionXform.PIVOT_CACHE_DEFINITION_ATTRIBUTES,
|
|
83
81
|
"r:id": model.rId || "rId1",
|
|
84
82
|
refreshOnLoad: model.refreshOnLoad || "1",
|
|
85
|
-
refreshedBy: model.refreshedBy || "Author",
|
|
86
|
-
refreshedDate: model.refreshedDate || "45125.026046874998",
|
|
87
83
|
createdVersion: model.createdVersion || "8",
|
|
88
84
|
refreshedVersion: model.refreshedVersion || "8",
|
|
89
85
|
minRefreshableVersion: model.minRefreshableVersion || "3",
|
|
@@ -117,8 +113,6 @@ class PivotCacheDefinitionXform extends base_xform_1.BaseXform {
|
|
|
117
113
|
cacheFields: [],
|
|
118
114
|
rId: attributes["r:id"],
|
|
119
115
|
refreshOnLoad: attributes.refreshOnLoad,
|
|
120
|
-
refreshedBy: attributes.refreshedBy,
|
|
121
|
-
refreshedDate: attributes.refreshedDate,
|
|
122
116
|
createdVersion: attributes.createdVersion,
|
|
123
117
|
refreshedVersion: attributes.refreshedVersion,
|
|
124
118
|
minRefreshableVersion: attributes.minRefreshableVersion,
|
|
@@ -184,8 +178,5 @@ class PivotCacheDefinitionXform extends base_xform_1.BaseXform {
|
|
|
184
178
|
exports.PivotCacheDefinitionXform = PivotCacheDefinitionXform;
|
|
185
179
|
PivotCacheDefinitionXform.PIVOT_CACHE_DEFINITION_ATTRIBUTES = {
|
|
186
180
|
xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
|
|
187
|
-
"xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
188
|
-
"xmlns:mc": "http://schemas.openxmlformats.org/markup-compatibility/2006",
|
|
189
|
-
"mc:Ignorable": "xr",
|
|
190
|
-
"xmlns:xr": "http://schemas.microsoft.com/office/spreadsheetml/2014/revision"
|
|
181
|
+
"xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
191
182
|
};
|
|
@@ -119,7 +119,7 @@ class PivotCacheRecordsXform extends base_xform_1.BaseXform {
|
|
|
119
119
|
}
|
|
120
120
|
return `<s v="${(0, utils_1.xmlEncode)(String(value))}" />`;
|
|
121
121
|
}
|
|
122
|
-
// shared items
|
|
122
|
+
// shared items - use indexOf for value lookup (works for both string and numeric)
|
|
123
123
|
const sharedItemsIndex = sharedItems.indexOf(value);
|
|
124
124
|
if (sharedItemsIndex < 0) {
|
|
125
125
|
throw new Error(`${JSON.stringify(value)} not in sharedItems ${JSON.stringify(sharedItems)}`);
|
|
@@ -58,8 +58,6 @@ class PivotTableXform extends base_xform_1.BaseXform {
|
|
|
58
58
|
*/
|
|
59
59
|
renderNew(xmlStream, model) {
|
|
60
60
|
const { rows, columns, values, cacheFields, cacheId, applyWidthHeightFormats } = model;
|
|
61
|
-
// Generate unique UID for each pivot table to prevent Excel treating them as identical
|
|
62
|
-
const uniqueUid = `{${crypto.randomUUID().toUpperCase()}}`;
|
|
63
61
|
// Build rowItems - need one <i> for each unique value in row fields, plus grand total
|
|
64
62
|
const rowItems = buildRowItems(rows, cacheFields);
|
|
65
63
|
// Build colItems - need one <i> for each unique value in col fields, plus grand total
|
|
@@ -71,15 +69,20 @@ class PivotTableXform extends base_xform_1.BaseXform {
|
|
|
71
69
|
// - firstHeaderRow: 1 (column headers are in first row of pivot table)
|
|
72
70
|
// - firstDataRow: 2 (data starts in second row)
|
|
73
71
|
// - firstDataCol: 1 (data starts in second column, after row labels)
|
|
74
|
-
// Calculate ref based on actual data size
|
|
75
|
-
|
|
72
|
+
// Calculate ref based on actual data size:
|
|
73
|
+
// - Start row: 3
|
|
74
|
+
// - Header rows: 2 (column label row + subheader row)
|
|
75
|
+
// - Data rows: rowFieldItemCount
|
|
76
|
+
// - Grand total row: 1
|
|
77
|
+
// endRow = 3 + 2 + rowFieldItemCount + 1 - 1 = 5 + rowFieldItemCount
|
|
78
|
+
// Or simplified: startRow (3) + 1 (column labels) + rowFieldItemCount (data) + 1 (grand total)
|
|
79
|
+
const endRow = 3 + 1 + rowFieldItemCount + 1; // = 5 + rowFieldItemCount
|
|
76
80
|
const endCol = 1 + colFieldItemCount + 1; // start col + data cols + grand total
|
|
77
81
|
const endColLetter = String.fromCharCode(64 + endCol);
|
|
78
82
|
const locationRef = `A3:${endColLetter}${endRow}`;
|
|
79
83
|
xmlStream.openXml(xml_stream_1.XmlStream.StdDocAttributes);
|
|
80
84
|
xmlStream.openNode(this.tag, {
|
|
81
85
|
...PivotTableXform.PIVOT_TABLE_ATTRIBUTES,
|
|
82
|
-
"xr:uid": uniqueUid,
|
|
83
86
|
name: "PivotTable2",
|
|
84
87
|
cacheId,
|
|
85
88
|
applyNumberFormats: "0",
|
|
@@ -156,11 +159,9 @@ class PivotTableXform extends base_xform_1.BaseXform {
|
|
|
156
159
|
* Render loaded pivot table (preserving original structure)
|
|
157
160
|
*/
|
|
158
161
|
renderLoaded(xmlStream, model) {
|
|
159
|
-
const uniqueUid = model.uid || `{${crypto.randomUUID().toUpperCase()}}`;
|
|
160
162
|
xmlStream.openXml(xml_stream_1.XmlStream.StdDocAttributes);
|
|
161
163
|
xmlStream.openNode(this.tag, {
|
|
162
164
|
...PivotTableXform.PIVOT_TABLE_ATTRIBUTES,
|
|
163
|
-
"xr:uid": uniqueUid,
|
|
164
165
|
name: model.name || "PivotTable1",
|
|
165
166
|
cacheId: model.cacheId,
|
|
166
167
|
applyNumberFormats: model.applyNumberFormats || "0",
|
|
@@ -474,17 +475,15 @@ class PivotTableXform extends base_xform_1.BaseXform {
|
|
|
474
475
|
}
|
|
475
476
|
exports.PivotTableXform = PivotTableXform;
|
|
476
477
|
PivotTableXform.PIVOT_TABLE_ATTRIBUTES = {
|
|
477
|
-
xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
|
478
|
-
"xmlns:mc": "http://schemas.openxmlformats.org/markup-compatibility/2006",
|
|
479
|
-
"mc:Ignorable": "xr",
|
|
480
|
-
"xmlns:xr": "http://schemas.microsoft.com/office/spreadsheetml/2014/revision"
|
|
478
|
+
xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
|
481
479
|
};
|
|
482
480
|
// Helpers
|
|
483
481
|
/**
|
|
484
482
|
* Build rowItems XML - one item for each unique value in row fields, plus grand total.
|
|
485
483
|
* Each <i> represents a row in the pivot table.
|
|
486
|
-
* - Regular items: <i><x v="index"/></i>
|
|
484
|
+
* - Regular items: <i><x/></i> for index 0, <i><x v="index"/></i> for index > 0
|
|
487
485
|
* - Grand total: <i t="grand"><x/></i>
|
|
486
|
+
* Note: When v=0, the v attribute should be omitted (Excel convention)
|
|
488
487
|
*/
|
|
489
488
|
function buildRowItems(rows, cacheFields) {
|
|
490
489
|
if (rows.length === 0) {
|
|
@@ -498,8 +497,14 @@ function buildRowItems(rows, cacheFields) {
|
|
|
498
497
|
// Build items: one for each unique value + grand total
|
|
499
498
|
const items = [];
|
|
500
499
|
// Regular items - reference each unique value by index
|
|
500
|
+
// Note: v="0" should be omitted (Excel uses <x/> instead of <x v="0"/>)
|
|
501
501
|
for (let i = 0; i < itemCount; i++) {
|
|
502
|
-
|
|
502
|
+
if (i === 0) {
|
|
503
|
+
items.push("<i><x /></i>");
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
items.push(`<i><x v="${i}" /></i>`);
|
|
507
|
+
}
|
|
503
508
|
}
|
|
504
509
|
// Grand total row
|
|
505
510
|
items.push('<i t="grand"><x /></i>');
|
|
@@ -511,6 +516,7 @@ function buildRowItems(rows, cacheFields) {
|
|
|
511
516
|
/**
|
|
512
517
|
* Build colItems XML - one item for each unique value in column fields, plus grand total.
|
|
513
518
|
* When there are multiple data fields (values), each column value may have sub-columns.
|
|
519
|
+
* Note: When v=0, the v attribute should be omitted (Excel convention)
|
|
514
520
|
*/
|
|
515
521
|
function buildColItems(columns, cacheFields, valueCount) {
|
|
516
522
|
if (columns.length === 0) {
|
|
@@ -519,7 +525,12 @@ function buildColItems(columns, cacheFields, valueCount) {
|
|
|
519
525
|
// Multiple values: one column per value + grand total
|
|
520
526
|
const items = [];
|
|
521
527
|
for (let i = 0; i < valueCount; i++) {
|
|
522
|
-
|
|
528
|
+
if (i === 0) {
|
|
529
|
+
items.push("<i><x /></i>");
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
items.push(`<i><x v="${i}" /></i>`);
|
|
533
|
+
}
|
|
523
534
|
}
|
|
524
535
|
items.push('<i t="grand"><x /></i>');
|
|
525
536
|
return { count: items.length, xml: items.join("\n ") };
|
|
@@ -534,8 +545,14 @@ function buildColItems(columns, cacheFields, valueCount) {
|
|
|
534
545
|
// Build items: one for each unique value + grand total
|
|
535
546
|
const items = [];
|
|
536
547
|
// Regular items - reference each unique value by index
|
|
548
|
+
// Note: v="0" should be omitted (Excel uses <x/> instead of <x v="0"/>)
|
|
537
549
|
for (let i = 0; i < itemCount; i++) {
|
|
538
|
-
|
|
550
|
+
if (i === 0) {
|
|
551
|
+
items.push("<i><x /></i>");
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
items.push(`<i><x v="${i}" /></i>`);
|
|
555
|
+
}
|
|
539
556
|
}
|
|
540
557
|
// Grand total column
|
|
541
558
|
items.push('<i t="grand"><x /></i>');
|
|
@@ -568,38 +585,42 @@ function renderPivotFields(pivotTable) {
|
|
|
568
585
|
const valueSet = new Set(pivotTable.values);
|
|
569
586
|
return pivotTable.cacheFields
|
|
570
587
|
.map((cacheField, fieldIndex) => {
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
: valueSet.has(fieldIndex)
|
|
576
|
-
? "value"
|
|
577
|
-
: null;
|
|
578
|
-
return renderPivotField(fieldType, cacheField.sharedItems);
|
|
588
|
+
const isRow = rowSet.has(fieldIndex);
|
|
589
|
+
const isCol = colSet.has(fieldIndex);
|
|
590
|
+
const isValue = valueSet.has(fieldIndex);
|
|
591
|
+
return renderPivotField(isRow, isCol, isValue, cacheField.sharedItems);
|
|
579
592
|
})
|
|
580
593
|
.join("");
|
|
581
594
|
}
|
|
582
|
-
function renderPivotField(
|
|
583
|
-
//
|
|
584
|
-
|
|
585
|
-
if (
|
|
586
|
-
const axis =
|
|
595
|
+
function renderPivotField(isRow, isCol, isValue, sharedItems) {
|
|
596
|
+
// A field can be both a row/column field AND a value field (issue #15)
|
|
597
|
+
// In this case, it needs both axis attribute AND dataField="1"
|
|
598
|
+
if (isRow || isCol) {
|
|
599
|
+
const axis = isRow ? "axisRow" : "axisCol";
|
|
600
|
+
// Row and column fields should NOT have defaultSubtotal="0"
|
|
601
|
+
let axisAttributes = 'compact="0" outline="0" showAll="0"';
|
|
602
|
+
// If also a value field, add dataField="1"
|
|
603
|
+
if (isValue) {
|
|
604
|
+
axisAttributes = `dataField="1" ${axisAttributes}`;
|
|
605
|
+
}
|
|
587
606
|
// items = one for each shared item + one default item
|
|
588
607
|
const itemsXml = [
|
|
589
608
|
...sharedItems.map((_item, index) => `<item x="${index}" />`),
|
|
590
609
|
'<item t="default" />' // Required default item for subtotals/grand totals
|
|
591
610
|
].join("\n ");
|
|
592
611
|
return `
|
|
593
|
-
<pivotField axis="${axis}" ${
|
|
612
|
+
<pivotField axis="${axis}" ${axisAttributes}>
|
|
594
613
|
<items count="${sharedItems.length + 1}">
|
|
595
614
|
${itemsXml}
|
|
596
615
|
</items>
|
|
597
616
|
</pivotField>
|
|
598
617
|
`;
|
|
599
618
|
}
|
|
619
|
+
// Value fields and non-axis fields should have defaultSubtotal="0"
|
|
620
|
+
const defaultAttributes = 'compact="0" outline="0" showAll="0" defaultSubtotal="0"';
|
|
600
621
|
return `
|
|
601
622
|
<pivotField
|
|
602
|
-
${
|
|
623
|
+
${isValue ? 'dataField="1"' : ""}
|
|
603
624
|
${defaultAttributes}
|
|
604
625
|
/>
|
|
605
626
|
`;
|
|
@@ -51,9 +51,10 @@ class PageSetupXform extends base_xform_1.BaseXform {
|
|
|
51
51
|
draft: booleanToXml(model.draft),
|
|
52
52
|
cellComments: cellCommentsToXml(model.cellComments),
|
|
53
53
|
errors: errorsToXml(model.errors),
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
// Only output non-default values (matches Excel behavior)
|
|
55
|
+
scale: model.scale !== 100 ? model.scale : undefined,
|
|
56
|
+
fitToWidth: model.fitToWidth !== 1 ? model.fitToWidth : undefined,
|
|
57
|
+
fitToHeight: model.fitToHeight !== 1 ? model.fitToHeight : undefined,
|
|
57
58
|
firstPageNumber: model.firstPageNumber,
|
|
58
59
|
useFirstPageNumber: booleanToXml(!!model.firstPageNumber),
|
|
59
60
|
usePrinterDefaults: booleanToXml(model.usePrinterDefaults),
|
|
@@ -51,7 +51,7 @@ class RowXform extends base_xform_1.BaseXform {
|
|
|
51
51
|
xmlStream.addAttribute("s", model.styleId);
|
|
52
52
|
xmlStream.addAttribute("customFormat", "1");
|
|
53
53
|
}
|
|
54
|
-
|
|
54
|
+
// Note: dyDescent is MS extension, not output by default (Excel auto-calculates)
|
|
55
55
|
if (model.outlineLevel) {
|
|
56
56
|
xmlStream.addAttribute("outlineLevel", model.outlineLevel);
|
|
57
57
|
}
|
|
@@ -10,10 +10,13 @@ class SheetFormatPropertiesXform extends base_xform_1.BaseXform {
|
|
|
10
10
|
if (model) {
|
|
11
11
|
const attributes = {
|
|
12
12
|
defaultRowHeight: model.defaultRowHeight,
|
|
13
|
-
outlineLevelRow
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
// Only output outlineLevelRow/Col when non-zero (matches Excel behavior)
|
|
14
|
+
outlineLevelRow: model.outlineLevelRow || undefined,
|
|
15
|
+
outlineLevelCol: model.outlineLevelCol || undefined,
|
|
16
|
+
// Only output dyDescent if explicitly set (MS extension, not ECMA-376 standard)
|
|
17
|
+
"x14ac:dyDescent": model.dyDescent || undefined
|
|
16
18
|
};
|
|
19
|
+
// Only output defaultColWidth if explicitly set
|
|
17
20
|
if (model.defaultColWidth) {
|
|
18
21
|
attributes.defaultColWidth = model.defaultColWidth;
|
|
19
22
|
}
|
|
@@ -23,16 +23,20 @@ class SheetViewXform extends base_xform_1.BaseXform {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
render(xmlStream, model) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
// Build initial attributes with correct order to match Excel output
|
|
27
|
+
const initialAttrs = {};
|
|
28
|
+
if (model.tabSelected) {
|
|
29
|
+
initialAttrs.tabSelected = "1";
|
|
30
|
+
}
|
|
31
|
+
initialAttrs.workbookViewId = model.workbookViewId || 0;
|
|
32
|
+
xmlStream.openNode("sheetView", initialAttrs);
|
|
29
33
|
const add = function (name, value, included) {
|
|
30
34
|
if (included) {
|
|
31
35
|
xmlStream.addAttribute(name, value);
|
|
32
36
|
}
|
|
33
37
|
};
|
|
34
38
|
add("rightToLeft", "1", model.rightToLeft === true);
|
|
35
|
-
|
|
39
|
+
// tabSelected is now in initialAttrs
|
|
36
40
|
add("showRuler", "0", model.showRuler === false);
|
|
37
41
|
add("showRowColHeaders", "0", model.showRowColHeaders === false);
|
|
38
42
|
add("showGridLines", "0", model.showGridLines === false);
|
|
@@ -515,6 +515,6 @@ WorkSheetXform.WORKSHEET_ATTRIBUTES = {
|
|
|
515
515
|
xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
|
|
516
516
|
"xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
|
|
517
517
|
"xmlns:mc": "http://schemas.openxmlformats.org/markup-compatibility/2006",
|
|
518
|
-
"
|
|
519
|
-
"
|
|
518
|
+
"xmlns:x14ac": "http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac",
|
|
519
|
+
"mc:Ignorable": "x14ac"
|
|
520
520
|
};
|