@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.
Files changed (58) hide show
  1. package/dist/browser/excelts.esm.js +181 -133
  2. package/dist/browser/excelts.esm.js.map +1 -1
  3. package/dist/browser/excelts.esm.min.js +30 -24
  4. package/dist/browser/excelts.iife.js +181 -133
  5. package/dist/browser/excelts.iife.js.map +1 -1
  6. package/dist/browser/excelts.iife.min.js +30 -24
  7. package/dist/cjs/doc/pivot-table.js +47 -6
  8. package/dist/cjs/doc/worksheet.js +0 -1
  9. package/dist/cjs/stream/xlsx/worksheet-writer.js +0 -1
  10. package/dist/cjs/xlsx/xform/book/sheet-xform.js +3 -2
  11. package/dist/cjs/xlsx/xform/book/workbook-properties-xform.js +1 -1
  12. package/dist/cjs/xlsx/xform/core/content-types-xform.js +12 -6
  13. package/dist/cjs/xlsx/xform/pivot-table/cache-field-xform.js +17 -21
  14. package/dist/cjs/xlsx/xform/pivot-table/cache-field.js +43 -7
  15. package/dist/cjs/xlsx/xform/pivot-table/pivot-cache-definition-xform.js +1 -10
  16. package/dist/cjs/xlsx/xform/pivot-table/pivot-cache-records-xform.js +1 -1
  17. package/dist/cjs/xlsx/xform/pivot-table/pivot-table-xform.js +51 -30
  18. package/dist/cjs/xlsx/xform/sheet/page-setup-xform.js +4 -3
  19. package/dist/cjs/xlsx/xform/sheet/row-xform.js +1 -1
  20. package/dist/cjs/xlsx/xform/sheet/sheet-format-properties-xform.js +6 -3
  21. package/dist/cjs/xlsx/xform/sheet/sheet-view-xform.js +8 -4
  22. package/dist/cjs/xlsx/xform/sheet/worksheet-xform.js +2 -2
  23. package/dist/cjs/xlsx/xform/style/font-xform.js +36 -23
  24. package/dist/cjs/xlsx/xform/table/auto-filter-xform.js +3 -1
  25. package/dist/cjs/xlsx/xform/table/table-column-xform.js +2 -1
  26. package/dist/cjs/xlsx/xform/table/table-xform.js +5 -9
  27. package/dist/esm/doc/pivot-table.js +47 -6
  28. package/dist/esm/doc/worksheet.js +0 -1
  29. package/dist/esm/stream/xlsx/worksheet-writer.js +0 -1
  30. package/dist/esm/xlsx/xform/book/sheet-xform.js +3 -2
  31. package/dist/esm/xlsx/xform/book/workbook-properties-xform.js +1 -1
  32. package/dist/esm/xlsx/xform/core/content-types-xform.js +12 -6
  33. package/dist/esm/xlsx/xform/pivot-table/cache-field-xform.js +17 -21
  34. package/dist/esm/xlsx/xform/pivot-table/cache-field.js +43 -7
  35. package/dist/esm/xlsx/xform/pivot-table/pivot-cache-definition-xform.js +1 -10
  36. package/dist/esm/xlsx/xform/pivot-table/pivot-cache-records-xform.js +1 -1
  37. package/dist/esm/xlsx/xform/pivot-table/pivot-table-xform.js +51 -30
  38. package/dist/esm/xlsx/xform/sheet/page-setup-xform.js +4 -3
  39. package/dist/esm/xlsx/xform/sheet/row-xform.js +1 -1
  40. package/dist/esm/xlsx/xform/sheet/sheet-format-properties-xform.js +6 -3
  41. package/dist/esm/xlsx/xform/sheet/sheet-view-xform.js +8 -4
  42. package/dist/esm/xlsx/xform/sheet/worksheet-xform.js +2 -2
  43. package/dist/esm/xlsx/xform/style/font-xform.js +36 -23
  44. package/dist/esm/xlsx/xform/table/auto-filter-xform.js +3 -1
  45. package/dist/esm/xlsx/xform/table/table-column-xform.js +2 -1
  46. package/dist/esm/xlsx/xform/table/table-xform.js +5 -9
  47. package/dist/types/doc/pivot-table.d.ts +5 -1
  48. package/dist/types/stream/xlsx/worksheet-writer.d.ts +1 -1
  49. package/dist/types/types.d.ts +1 -1
  50. package/dist/types/xlsx/xform/pivot-table/cache-field-xform.d.ts +1 -1
  51. package/dist/types/xlsx/xform/pivot-table/cache-field.d.ts +6 -2
  52. package/dist/types/xlsx/xform/pivot-table/pivot-cache-definition-xform.d.ts +0 -5
  53. package/dist/types/xlsx/xform/pivot-table/pivot-table-xform.d.ts +0 -3
  54. package/dist/types/xlsx/xform/sheet/sheet-format-properties-xform.d.ts +1 -1
  55. package/dist/types/xlsx/xform/sheet/worksheet-xform.d.ts +1 -1
  56. package/dist/types/xlsx/xform/style/font-xform.d.ts +1 -0
  57. package/dist/types/xlsx/xform/table/table-xform.d.ts +0 -4
  58. 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: tableModel.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
- // Other fields (typically numeric) have sharedItems = null.
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
- const sharedItems = sharedItemsFields.has(name) ? aggregate(columnIndex) : null;
195
- result.push({ name, sharedItems });
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
  }
@@ -43,7 +43,6 @@ class Worksheet {
43
43
  // for tabColor, default row height, outline levels, etc
44
44
  this.properties = Object.assign({}, {
45
45
  defaultRowHeight: 15,
46
- dyDescent: 55,
47
46
  outlineLevelCol: 0,
48
47
  outlineLevelRow: 0
49
48
  }, options.properties);
@@ -98,7 +98,6 @@ class WorksheetWriter {
98
98
  // for default row height, outline levels, etc
99
99
  this.properties = Object.assign({}, {
100
100
  defaultRowHeight: 15,
101
- dyDescent: 55,
102
101
  outlineLevelCol: 0,
103
102
  outlineLevelRow: 0
104
103
  }, options.properties);
@@ -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
- state: model.state,
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: 164011,
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 each pivot table
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
- // Check if this is a numeric field (no string items)
51
- if (attributes.containsNumber === "1" || attributes.containsInteger === "1") {
52
- if (this.model) {
53
- this.model.containsNumber = attributes.containsNumber === "1";
54
- this.model.containsInteger = attributes.containsInteger === "1";
55
- if (attributes.minValue !== undefined) {
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
- else {
66
- // String field - initialize sharedItems array if count > 0
67
- if (this.model) {
68
- const count = parseInt(attributes.count || "0", 10);
69
- if (count > 0) {
70
- this.model.sharedItems = [];
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
- // integer types
43
+ // No shared items - field not used for rows/columns
31
44
  if (this.sharedItems === null) {
32
- // TK(2023-07-18): left out attributes... minValue="5" maxValue="45"
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
- // string types - escape XML special characters in each shared item value
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
- const endRow = 3 + rowFieldItemCount + 1; // start row + data rows + grand total
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> where index is the position in sharedItems
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
- items.push(`<i><x v="${i}" /></i>`);
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
- items.push(`<i><x v="${i}" /></i>`);
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
- items.push(`<i><x v="${i}" /></i>`);
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 fieldType = rowSet.has(fieldIndex)
572
- ? "row"
573
- : colSet.has(fieldIndex)
574
- ? "column"
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(fieldType, sharedItems) {
583
- // fieldType: 'row', 'column', 'value', null
584
- const defaultAttributes = 'compact="0" outline="0" showAll="0" defaultSubtotal="0"';
585
- if (fieldType === "row" || fieldType === "column") {
586
- const axis = fieldType === "row" ? "axisRow" : "axisCol";
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}" ${defaultAttributes}>
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
- ${fieldType === "value" ? 'dataField="1"' : ""}
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
- scale: model.scale,
55
- fitToWidth: model.fitToWidth,
56
- fitToHeight: model.fitToHeight,
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
- xmlStream.addAttribute("x14ac:dyDescent", "0.25");
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: model.outlineLevelRow,
14
- outlineLevelCol: model.outlineLevelCol,
15
- "x14ac:dyDescent": model.dyDescent
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
- xmlStream.openNode("sheetView", {
27
- workbookViewId: model.workbookViewId || 0
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
- add("tabSelected", "1", model.tabSelected);
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
- "mc:Ignorable": "x14ac",
519
- "xmlns:x14ac": "http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac"
518
+ "xmlns:x14ac": "http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac",
519
+ "mc:Ignorable": "x14ac"
520
520
  };