@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
@@ -12,35 +12,48 @@ class FontXform extends base_xform_1.BaseXform {
12
12
  constructor(options) {
13
13
  super();
14
14
  this.options = options || FontXform.OPTIONS;
15
- this.map = {
16
- b: { prop: "bold", xform: new boolean_xform_1.BooleanXform({ tag: "b", attr: "val" }) },
17
- i: { prop: "italic", xform: new boolean_xform_1.BooleanXform({ tag: "i", attr: "val" }) },
18
- u: { prop: "underline", xform: new underline_xform_1.UnderlineXform() },
19
- charset: { prop: "charset", xform: new integer_xform_1.IntegerXform({ tag: "charset", attr: "val" }) },
20
- color: { prop: "color", xform: new color_xform_1.ColorXform() },
21
- condense: { prop: "condense", xform: new boolean_xform_1.BooleanXform({ tag: "condense", attr: "val" }) },
22
- extend: { prop: "extend", xform: new boolean_xform_1.BooleanXform({ tag: "extend", attr: "val" }) },
23
- family: { prop: "family", xform: new integer_xform_1.IntegerXform({ tag: "family", attr: "val" }) },
24
- outline: { prop: "outline", xform: new boolean_xform_1.BooleanXform({ tag: "outline", attr: "val" }) },
25
- vertAlign: { prop: "vertAlign", xform: new string_xform_1.StringXform({ tag: "vertAlign", attr: "val" }) },
26
- scheme: { prop: "scheme", xform: new string_xform_1.StringXform({ tag: "scheme", attr: "val" }) },
27
- shadow: { prop: "shadow", xform: new boolean_xform_1.BooleanXform({ tag: "shadow", attr: "val" }) },
28
- strike: { prop: "strike", xform: new boolean_xform_1.BooleanXform({ tag: "strike", attr: "val" }) },
29
- sz: { prop: "size", xform: new integer_xform_1.IntegerXform({ tag: "sz", attr: "val" }) }
30
- };
31
- this.map[this.options.fontNameTag] = {
32
- prop: "name",
33
- xform: new string_xform_1.StringXform({ tag: this.options.fontNameTag, attr: "val" })
34
- };
15
+ // Define properties in render order (Excel's expected order)
16
+ const fontProperties = [
17
+ { tag: "b", prop: "bold", xform: new boolean_xform_1.BooleanXform({ tag: "b", attr: "val" }) },
18
+ { tag: "i", prop: "italic", xform: new boolean_xform_1.BooleanXform({ tag: "i", attr: "val" }) },
19
+ { tag: "u", prop: "underline", xform: new underline_xform_1.UnderlineXform() },
20
+ { tag: "strike", prop: "strike", xform: new boolean_xform_1.BooleanXform({ tag: "strike", attr: "val" }) },
21
+ {
22
+ tag: "condense",
23
+ prop: "condense",
24
+ xform: new boolean_xform_1.BooleanXform({ tag: "condense", attr: "val" })
25
+ },
26
+ { tag: "extend", prop: "extend", xform: new boolean_xform_1.BooleanXform({ tag: "extend", attr: "val" }) },
27
+ { tag: "outline", prop: "outline", xform: new boolean_xform_1.BooleanXform({ tag: "outline", attr: "val" }) },
28
+ { tag: "shadow", prop: "shadow", xform: new boolean_xform_1.BooleanXform({ tag: "shadow", attr: "val" }) },
29
+ { tag: "sz", prop: "size", xform: new integer_xform_1.IntegerXform({ tag: "sz", attr: "val" }) },
30
+ { tag: "color", prop: "color", xform: new color_xform_1.ColorXform() },
31
+ {
32
+ tag: this.options.fontNameTag,
33
+ prop: "name",
34
+ xform: new string_xform_1.StringXform({ tag: this.options.fontNameTag, attr: "val" })
35
+ },
36
+ { tag: "family", prop: "family", xform: new integer_xform_1.IntegerXform({ tag: "family", attr: "val" }) },
37
+ { tag: "scheme", prop: "scheme", xform: new string_xform_1.StringXform({ tag: "scheme", attr: "val" }) },
38
+ { tag: "charset", prop: "charset", xform: new integer_xform_1.IntegerXform({ tag: "charset", attr: "val" }) },
39
+ {
40
+ tag: "vertAlign",
41
+ prop: "vertAlign",
42
+ xform: new string_xform_1.StringXform({ tag: "vertAlign", attr: "val" })
43
+ }
44
+ ];
45
+ // Build map and renderOrder from single source of truth
46
+ this.map = Object.fromEntries(fontProperties.map(p => [p.tag, { prop: p.prop, xform: p.xform }]));
47
+ this.renderOrder = fontProperties.map(p => p.tag);
35
48
  }
36
49
  get tag() {
37
50
  return this.options.tagName;
38
51
  }
39
52
  render(xmlStream, model) {
40
- const { map } = this;
53
+ const { map, renderOrder } = this;
41
54
  xmlStream.openNode(this.options.tagName);
42
- Object.entries(this.map).forEach(([tag, defn]) => {
43
- map[tag].xform.render(xmlStream, model[defn.prop]);
55
+ renderOrder.forEach(tag => {
56
+ map[tag].xform.render(xmlStream, model[map[tag].prop]);
44
57
  });
45
58
  xmlStream.closeNode();
46
59
  }
@@ -20,7 +20,9 @@ class AutoFilterXform extends base_xform_1.BaseXform {
20
20
  });
21
21
  }
22
22
  render(xmlStream, model) {
23
- xmlStream.openNode(this.tag, { ref: model.autoFilterRef });
23
+ xmlStream.openNode(this.tag, {
24
+ ref: model.autoFilterRef
25
+ });
24
26
  model.columns.forEach((column) => {
25
27
  this.map.filterColumn.render(xmlStream, column);
26
28
  });
@@ -18,7 +18,8 @@ class TableColumnXform extends base_xform_1.BaseXform {
18
18
  id: model.id.toString(),
19
19
  name: model.name,
20
20
  totalsRowLabel: model.totalsRowLabel,
21
- totalsRowFunction: model.totalsRowFunction,
21
+ // Excel doesn't output totalsRowFunction when value is 'none'
22
+ totalsRowFunction: model.totalsRowFunction === "none" ? undefined : model.totalsRowFunction,
22
23
  dxfId: model.dxfId
23
24
  });
24
25
  }
@@ -43,8 +43,8 @@ class TableXform extends base_xform_1.BaseXform {
43
43
  displayName: model.displayName || model.name,
44
44
  ref: model.tableRef,
45
45
  totalsRowCount: model.totalsRow ? "1" : undefined,
46
- totalsRowShown: model.totalsRow ? undefined : "1",
47
- headerRowCount: model.headerRow ? "1" : "0"
46
+ // Excel doesn't output headerRowCount when it's 1 (default) or when there's a header row
47
+ headerRowCount: model.headerRow ? undefined : "0"
48
48
  });
49
49
  this.map.autoFilter.render(xmlStream, model);
50
50
  this.map.tableColumns.render(xmlStream, model.columns);
@@ -65,7 +65,8 @@ class TableXform extends base_xform_1.BaseXform {
65
65
  displayName: attributes.displayName || attributes.name,
66
66
  tableRef: attributes.ref,
67
67
  totalsRow: attributes.totalsRowCount === "1",
68
- headerRow: attributes.headerRowCount === "1"
68
+ // ECMA-376: headerRowCount defaults to 1, so missing attribute means has header
69
+ headerRow: attributes.headerRowCount !== "0"
69
70
  };
70
71
  break;
71
72
  default:
@@ -124,10 +125,5 @@ class TableXform extends base_xform_1.BaseXform {
124
125
  }
125
126
  exports.TableXform = TableXform;
126
127
  TableXform.TABLE_ATTRIBUTES = {
127
- xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
128
- "xmlns:mc": "http://schemas.openxmlformats.org/markup-compatibility/2006",
129
- "mc:Ignorable": "xr xr3",
130
- "xmlns:xr": "http://schemas.microsoft.com/office/spreadsheetml/2014/revision",
131
- "xmlns:xr3": "http://schemas.microsoft.com/office/spreadsheetml/2016/revision3"
132
- // 'xr:uid': '{00000000-000C-0000-FFFF-FFFF00000000}',
128
+ xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
133
129
  };
@@ -34,8 +34,11 @@ function createTableSourceAdapter(table) {
34
34
  const endRow = startRow + tableModel.rows.length; // header row + data rows
35
35
  const endCol = startCol + columnNames.length - 1;
36
36
  const shortRange = colCache.encode(startRow, startCol, endRow, endCol);
37
+ // Use the worksheet name (not table name) for pivotCacheDefinition's worksheetSource
38
+ // The sheet attribute in worksheetSource must reference the actual worksheet name
39
+ const worksheetName = table.worksheet.name;
37
40
  return {
38
- name: tableModel.name,
41
+ name: worksheetName,
39
42
  getRow(rowNumber) {
40
43
  if (rowNumber === 1) {
41
44
  return { values: headerRow };
@@ -108,7 +111,7 @@ function makePivotTable(worksheet, model) {
108
111
  validate(worksheet, model, source);
109
112
  const { rows, values } = model;
110
113
  const columns = model.columns ?? [];
111
- const cacheFields = makeCacheFields(source, [...rows, ...columns]);
114
+ const cacheFields = makeCacheFields(source, [...rows, ...columns], values);
112
115
  const nameToIndex = cacheFields.reduce((result, cacheField, index) => {
113
116
  result[cacheField.name] = index;
114
117
  return result;
@@ -165,13 +168,15 @@ function validate(_worksheet, model, source) {
165
168
  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.");
166
169
  }
167
170
  }
168
- function makeCacheFields(source, fieldNamesWithSharedItems) {
171
+ function makeCacheFields(source, fieldNamesWithSharedItems, valueFieldNames) {
169
172
  // Cache fields are used in pivot tables to reference source data.
170
173
  // Fields in fieldNamesWithSharedItems get their unique values extracted as sharedItems.
171
- // Other fields (typically numeric) have sharedItems = null.
174
+ // Fields in valueFieldNames (but not in fieldNamesWithSharedItems) get min/max calculated.
175
+ // Other fields are unused and get empty sharedItems.
172
176
  const names = source.getRow(1).values;
173
177
  // Use Set for O(1) lookup instead of object
174
178
  const sharedItemsFields = new Set(fieldNamesWithSharedItems);
179
+ const valueFields = new Set(valueFieldNames);
175
180
  const aggregate = (columnIndex) => {
176
181
  const columnValues = source.getColumn(columnIndex).values;
177
182
  // Build unique values set directly, skipping header (index 0,1) and null/undefined
@@ -184,12 +189,48 @@ function makeCacheFields(source, fieldNamesWithSharedItems) {
184
189
  }
185
190
  return toSortedArray(uniqueValues);
186
191
  };
192
+ // Calculate min/max for numeric fields
193
+ const getMinMax = (columnIndex) => {
194
+ const columnValues = source.getColumn(columnIndex).values;
195
+ let min = Infinity;
196
+ let max = -Infinity;
197
+ let hasNumeric = false;
198
+ for (let i = 2; i < columnValues.length; i++) {
199
+ const v = columnValues[i];
200
+ if (typeof v === "number" && !isNaN(v)) {
201
+ hasNumeric = true;
202
+ if (v < min) {
203
+ min = v;
204
+ }
205
+ if (v > max) {
206
+ max = v;
207
+ }
208
+ }
209
+ }
210
+ return hasNumeric ? { minValue: min, maxValue: max } : null;
211
+ };
187
212
  // Build result array
188
213
  const result = [];
189
214
  for (const columnIndex of range(1, names.length)) {
190
215
  const name = names[columnIndex];
191
- const sharedItems = sharedItemsFields.has(name) ? aggregate(columnIndex) : null;
192
- result.push({ name, sharedItems });
216
+ if (sharedItemsFields.has(name)) {
217
+ // Field used for rows/columns - extract unique values as sharedItems
218
+ result.push({ name, sharedItems: aggregate(columnIndex) });
219
+ }
220
+ else if (valueFields.has(name)) {
221
+ // Field used only for values (aggregation) - calculate min/max
222
+ const minMax = getMinMax(columnIndex);
223
+ result.push({
224
+ name,
225
+ sharedItems: null,
226
+ minValue: minMax?.minValue,
227
+ maxValue: minMax?.maxValue
228
+ });
229
+ }
230
+ else {
231
+ // Unused field - just empty sharedItems (like Excel does)
232
+ result.push({ name, sharedItems: null });
233
+ }
193
234
  }
194
235
  return result;
195
236
  }
@@ -40,7 +40,6 @@ class Worksheet {
40
40
  // for tabColor, default row height, outline levels, etc
41
41
  this.properties = Object.assign({}, {
42
42
  defaultRowHeight: 15,
43
- dyDescent: 55,
44
43
  outlineLevelCol: 0,
45
44
  outlineLevelRow: 0
46
45
  }, options.properties);
@@ -95,7 +95,6 @@ class WorksheetWriter {
95
95
  // for default row height, outline levels, etc
96
96
  this.properties = Object.assign({}, {
97
97
  defaultRowHeight: 15,
98
- dyDescent: 55,
99
98
  outlineLevelCol: 0,
100
99
  outlineLevelRow: 0
101
100
  }, options.properties);
@@ -3,9 +3,10 @@ import { BaseXform } from "../base-xform.js";
3
3
  class WorksheetXform extends BaseXform {
4
4
  render(xmlStream, model) {
5
5
  xmlStream.leafNode("sheet", {
6
- sheetId: model.id,
7
6
  name: model.name,
8
- state: model.state,
7
+ sheetId: model.id,
8
+ // Excel doesn't output state when it's 'visible' (default)
9
+ state: model.state === "visible" ? undefined : model.state,
9
10
  "r:id": model.rId
10
11
  });
11
12
  }
@@ -3,7 +3,7 @@ class WorkbookPropertiesXform extends BaseXform {
3
3
  render(xmlStream, model) {
4
4
  xmlStream.leafNode("workbookPr", {
5
5
  date1904: model.date1904 ? 1 : undefined,
6
- defaultThemeVersion: 164011,
6
+ // Excel doesn't output defaultThemeVersion
7
7
  filterPrivacy: 1
8
8
  });
9
9
  }
@@ -38,7 +38,7 @@ class ContentTypesXform extends BaseXform {
38
38
  });
39
39
  });
40
40
  if ((model.pivotTables || []).length) {
41
- // Add content types for each pivot table
41
+ // Add content types for pivot cache (definition and records)
42
42
  (model.pivotTables || []).forEach((pivotTable) => {
43
43
  const n = pivotTable.tableNumber;
44
44
  xmlStream.leafNode("Override", {
@@ -49,10 +49,6 @@ class ContentTypesXform extends BaseXform {
49
49
  PartName: `/xl/pivotCache/pivotCacheRecords${n}.xml`,
50
50
  ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml"
51
51
  });
52
- xmlStream.leafNode("Override", {
53
- PartName: `/xl/pivotTables/pivotTable${n}.xml`,
54
- ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"
55
- });
56
52
  });
57
53
  }
58
54
  xmlStream.leafNode("Override", {
@@ -78,6 +74,16 @@ class ContentTypesXform extends BaseXform {
78
74
  });
79
75
  });
80
76
  }
77
+ // Add pivot table overrides after tables (matches Excel order)
78
+ if ((model.pivotTables || []).length) {
79
+ (model.pivotTables || []).forEach((pivotTable) => {
80
+ const n = pivotTable.tableNumber;
81
+ xmlStream.leafNode("Override", {
82
+ PartName: `/xl/pivotTables/pivotTable${n}.xml`,
83
+ ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"
84
+ });
85
+ });
86
+ }
81
87
  if (model.drawings) {
82
88
  model.drawings.forEach((drawing) => {
83
89
  xmlStream.leafNode("Override", {
@@ -86,7 +92,7 @@ class ContentTypesXform extends BaseXform {
86
92
  });
87
93
  });
88
94
  }
89
- if (model.commentRefs) {
95
+ if (model.commentRefs && model.commentRefs.length) {
90
96
  xmlStream.leafNode("Default", {
91
97
  Extension: "vml",
92
98
  ContentType: "application/vnd.openxmlformats-officedocument.vmlDrawing"
@@ -44,28 +44,24 @@ class CacheFieldXform extends BaseXform {
44
44
  break;
45
45
  case "sharedItems":
46
46
  this.inSharedItems = true;
47
- // Check if this is a numeric field (no string items)
48
- if (attributes.containsNumber === "1" || attributes.containsInteger === "1") {
49
- if (this.model) {
50
- this.model.containsNumber = attributes.containsNumber === "1";
51
- this.model.containsInteger = attributes.containsInteger === "1";
52
- if (attributes.minValue !== undefined) {
53
- this.model.minValue = parseFloat(attributes.minValue);
54
- }
55
- if (attributes.maxValue !== undefined) {
56
- this.model.maxValue = parseFloat(attributes.maxValue);
57
- }
58
- // Numeric fields have sharedItems = null
59
- this.model.sharedItems = null;
47
+ // Store numeric field metadata
48
+ if (this.model) {
49
+ this.model.containsNumber = attributes.containsNumber === "1";
50
+ this.model.containsInteger = attributes.containsInteger === "1";
51
+ if (attributes.minValue !== undefined) {
52
+ this.model.minValue = parseFloat(attributes.minValue);
60
53
  }
61
- }
62
- else {
63
- // String field - initialize sharedItems array if count > 0
64
- if (this.model) {
65
- const count = parseInt(attributes.count || "0", 10);
66
- if (count > 0) {
67
- this.model.sharedItems = [];
68
- }
54
+ if (attributes.maxValue !== undefined) {
55
+ this.model.maxValue = parseFloat(attributes.maxValue);
56
+ }
57
+ // Initialize sharedItems array if count > 0 (for both string and numeric fields)
58
+ const count = parseInt(attributes.count || "0", 10);
59
+ if (count > 0) {
60
+ this.model.sharedItems = [];
61
+ }
62
+ else {
63
+ // No count means no individual items (pure numeric field)
64
+ this.model.sharedItems = null;
69
65
  }
70
66
  }
71
67
  break;
@@ -1,6 +1,6 @@
1
1
  import { xmlEncode } from "../../../utils/utils.js";
2
2
  class CacheField {
3
- constructor({ name, sharedItems }) {
3
+ constructor({ name, sharedItems, minValue, maxValue }) {
4
4
  // string type
5
5
  //
6
6
  // {
@@ -10,28 +10,64 @@ class CacheField {
10
10
  //
11
11
  // or
12
12
  //
13
- // integer type
13
+ // integer type (no sharedItems)
14
14
  //
15
15
  // {
16
16
  // 'name': 'D',
17
- // 'sharedItems': null
17
+ // 'sharedItems': null,
18
+ // 'minValue': 5,
19
+ // 'maxValue': 45
20
+ // }
21
+ //
22
+ // or
23
+ //
24
+ // numeric type with shared items (used as both row/column and value field)
25
+ //
26
+ // {
27
+ // 'name': 'C',
28
+ // 'sharedItems': [5, 24, 35, 45]
18
29
  // }
19
30
  this.name = name;
20
31
  this.sharedItems = sharedItems;
32
+ this.minValue = minValue;
33
+ this.maxValue = maxValue;
21
34
  }
22
35
  render() {
23
36
  // PivotCache Field: http://www.datypic.com/sc/ooxml/e-ssml_cacheField-1.html
24
37
  // Shared Items: http://www.datypic.com/sc/ooxml/e-ssml_sharedItems-1.html
25
38
  // Escape XML special characters in name attribute
26
39
  const escapedName = xmlEncode(this.name);
27
- // integer types
40
+ // No shared items - field not used for rows/columns
28
41
  if (this.sharedItems === null) {
29
- // TK(2023-07-18): left out attributes... minValue="5" maxValue="45"
42
+ // If no minValue/maxValue, this is an unused field - use empty sharedItems like Excel does
43
+ if (this.minValue === undefined || this.maxValue === undefined) {
44
+ return `<cacheField name="${escapedName}" numFmtId="0">
45
+ <sharedItems />
46
+ </cacheField>`;
47
+ }
48
+ // Numeric field used only for values (not rows/columns) - include min/max
30
49
  return `<cacheField name="${escapedName}" numFmtId="0">
31
- <sharedItems containsSemiMixedTypes="0" containsString="0" containsNumber="1" containsInteger="1" />
50
+ <sharedItems containsSemiMixedTypes="0" containsString="0" containsNumber="1" containsInteger="1" minValue="${this.minValue}" maxValue="${this.maxValue}" />
51
+ </cacheField>`;
52
+ }
53
+ // Shared items exist - check if all values are numeric
54
+ // Note: empty array returns true for every(), so check length first
55
+ const allNumeric = this.sharedItems.length > 0 &&
56
+ this.sharedItems.every(item => typeof item === "number" && Number.isFinite(item));
57
+ const allInteger = allNumeric && this.sharedItems.every(item => Number.isInteger(item));
58
+ if (allNumeric) {
59
+ // Numeric shared items - used when field is both a row/column field AND a value field
60
+ // This allows Excel to both group by unique values AND perform aggregation
61
+ const minValue = Math.min(...this.sharedItems);
62
+ const maxValue = Math.max(...this.sharedItems);
63
+ const integerAttr = allInteger ? ' containsInteger="1"' : "";
64
+ return `<cacheField name="${escapedName}" numFmtId="0">
65
+ <sharedItems containsSemiMixedTypes="0" containsString="0" containsNumber="1"${integerAttr} minValue="${minValue}" maxValue="${maxValue}" count="${this.sharedItems.length}">
66
+ ${this.sharedItems.map(item => `<n v="${item}" />`).join("")}
67
+ </sharedItems>
32
68
  </cacheField>`;
33
69
  }
34
- // string types - escape XML special characters in each shared item value
70
+ // String shared items - escape XML special characters in each value
35
71
  return `<cacheField name="${escapedName}" numFmtId="0">
36
72
  <sharedItems count="${this.sharedItems.length}">
37
73
  ${this.sharedItems.map(item => `<s v="${xmlEncode(String(item))}" />`).join("")}
@@ -50,8 +50,6 @@ class PivotCacheDefinitionXform extends BaseXform {
50
50
  ...PivotCacheDefinitionXform.PIVOT_CACHE_DEFINITION_ATTRIBUTES,
51
51
  "r:id": "rId1",
52
52
  refreshOnLoad: "1", // important for our implementation to work
53
- refreshedBy: "Author",
54
- refreshedDate: "45125.026046874998",
55
53
  createdVersion: "8",
56
54
  refreshedVersion: "8",
57
55
  minRefreshableVersion: "3",
@@ -79,8 +77,6 @@ class PivotCacheDefinitionXform extends BaseXform {
79
77
  ...PivotCacheDefinitionXform.PIVOT_CACHE_DEFINITION_ATTRIBUTES,
80
78
  "r:id": model.rId || "rId1",
81
79
  refreshOnLoad: model.refreshOnLoad || "1",
82
- refreshedBy: model.refreshedBy || "Author",
83
- refreshedDate: model.refreshedDate || "45125.026046874998",
84
80
  createdVersion: model.createdVersion || "8",
85
81
  refreshedVersion: model.refreshedVersion || "8",
86
82
  minRefreshableVersion: model.minRefreshableVersion || "3",
@@ -114,8 +110,6 @@ class PivotCacheDefinitionXform extends BaseXform {
114
110
  cacheFields: [],
115
111
  rId: attributes["r:id"],
116
112
  refreshOnLoad: attributes.refreshOnLoad,
117
- refreshedBy: attributes.refreshedBy,
118
- refreshedDate: attributes.refreshedDate,
119
113
  createdVersion: attributes.createdVersion,
120
114
  refreshedVersion: attributes.refreshedVersion,
121
115
  minRefreshableVersion: attributes.minRefreshableVersion,
@@ -180,9 +174,6 @@ class PivotCacheDefinitionXform extends BaseXform {
180
174
  }
181
175
  PivotCacheDefinitionXform.PIVOT_CACHE_DEFINITION_ATTRIBUTES = {
182
176
  xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
183
- "xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
184
- "xmlns:mc": "http://schemas.openxmlformats.org/markup-compatibility/2006",
185
- "mc:Ignorable": "xr",
186
- "xmlns:xr": "http://schemas.microsoft.com/office/spreadsheetml/2014/revision"
177
+ "xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
187
178
  };
188
179
  export { PivotCacheDefinitionXform };
@@ -116,7 +116,7 @@ class PivotCacheRecordsXform extends BaseXform {
116
116
  }
117
117
  return `<s v="${xmlEncode(String(value))}" />`;
118
118
  }
119
- // shared items
119
+ // shared items - use indexOf for value lookup (works for both string and numeric)
120
120
  const sharedItemsIndex = sharedItems.indexOf(value);
121
121
  if (sharedItemsIndex < 0) {
122
122
  throw new Error(`${JSON.stringify(value)} not in sharedItems ${JSON.stringify(sharedItems)}`);