@cj-tech-master/excelts 2.0.0 → 2.0.1

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.
@@ -7,9 +7,6 @@
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.DateFormatter = exports.DateParser = void 0;
10
- exports.mightBeDate = mightBeDate;
11
- exports.parseDate = parseDate;
12
- exports.formatDate = formatDate;
13
10
  exports.getSupportedFormats = getSupportedFormats;
14
11
  // ============================================================================
15
12
  // Constants - Pre-computed lookup tables for zero-allocation operations
@@ -18,7 +15,6 @@ exports.getSupportedFormats = getSupportedFormats;
18
15
  const PAD2 = Array.from({ length: 60 }, (_, i) => (i < 10 ? `0${i}` : `${i}`));
19
16
  // Character codes for fast comparison
20
17
  const C_0 = 48;
21
- const C_9 = 57;
22
18
  const C_DASH = 45;
23
19
  const C_SLASH = 47;
24
20
  const C_COLON = 58;
@@ -194,158 +190,6 @@ const AUTO_DETECT = [
194
190
  [29, [parseISOMsOffset]]
195
191
  ];
196
192
  // ============================================================================
197
- // Public API
198
- // ============================================================================
199
- /**
200
- * Quick pre-filter to reject non-date strings
201
- */
202
- function mightBeDate(s) {
203
- const len = s.length;
204
- if (len < 10 || len > 30) {
205
- return false;
206
- }
207
- const c0 = s.charCodeAt(0);
208
- const c1 = s.charCodeAt(1);
209
- // Must start with digits
210
- if (c0 < C_0 || c0 > C_9 || c1 < C_0 || c1 > C_9) {
211
- return false;
212
- }
213
- const c2 = s.charCodeAt(2);
214
- // Either YYYY format (4 digits) or MM/DD format (separator at pos 2)
215
- return (c2 >= C_0 && c2 <= C_9) || c2 === C_DASH || c2 === C_SLASH;
216
- }
217
- /**
218
- * Parse a date string
219
- *
220
- * @param value - String to parse
221
- * @param formats - Specific formats to try (optional, auto-detects ISO if omitted)
222
- * @returns Date object or null
223
- *
224
- * @example
225
- * parseDate("2024-12-26") // auto-detect ISO
226
- * parseDate("12-26-2024", ["MM-DD-YYYY"]) // explicit US format
227
- */
228
- function parseDate(value, formats) {
229
- if (!value || typeof value !== "string") {
230
- return null;
231
- }
232
- const s = value.trim();
233
- if (!s) {
234
- return null;
235
- }
236
- // Explicit formats
237
- if (formats?.length) {
238
- for (const fmt of formats) {
239
- const result = PARSERS[fmt]?.(s);
240
- if (result) {
241
- return result;
242
- }
243
- }
244
- return null;
245
- }
246
- // Auto-detect by length
247
- const len = s.length;
248
- for (const [l, parsers] of AUTO_DETECT) {
249
- if (len === l) {
250
- for (const p of parsers) {
251
- const result = p(s);
252
- if (result) {
253
- return result;
254
- }
255
- }
256
- }
257
- }
258
- return null;
259
- }
260
- /**
261
- * Format a Date to string
262
- *
263
- * @param date - Date to format
264
- * @param format - Format template (optional, defaults to ISO)
265
- * @param utc - Use UTC time (default: false)
266
- * @returns Formatted string or empty string if invalid
267
- *
268
- * @example
269
- * formatDate(date) // "2024-12-26T10:30:00.000+08:00"
270
- * formatDate(date, "YYYY-MM-DD", true) // "2024-12-26"
271
- */
272
- function formatDate(date, format, utc = false) {
273
- if (!(date instanceof Date)) {
274
- return "";
275
- }
276
- const t = date.getTime();
277
- if (t !== t) {
278
- return "";
279
- } // NaN check (faster than isNaN)
280
- // Default ISO format - direct string building (faster than toISOString)
281
- if (!format) {
282
- if (utc) {
283
- const y = date.getUTCFullYear();
284
- const M = date.getUTCMonth() + 1;
285
- const D = date.getUTCDate();
286
- const H = date.getUTCHours();
287
- const m = date.getUTCMinutes();
288
- const s = date.getUTCSeconds();
289
- const ms = date.getUTCMilliseconds();
290
- return `${y}-${PAD2[M]}-${PAD2[D]}T${PAD2[H]}:${PAD2[m]}:${PAD2[s]}.${ms < 10 ? "00" + ms : ms < 100 ? "0" + ms : ms}Z`;
291
- }
292
- const y = date.getFullYear();
293
- const M = date.getMonth() + 1;
294
- const D = date.getDate();
295
- const H = date.getHours();
296
- const m = date.getMinutes();
297
- const s = date.getSeconds();
298
- const ms = date.getMilliseconds();
299
- return `${y}-${PAD2[M]}-${PAD2[D]}T${PAD2[H]}:${PAD2[m]}:${PAD2[s]}.${ms < 10 ? "00" + ms : ms < 100 ? "0" + ms : ms}${tzOffset(date)}`;
300
- }
301
- // Fast paths for common formats
302
- if (format === "YYYY-MM-DD") {
303
- return utc
304
- ? `${date.getUTCFullYear()}-${PAD2[date.getUTCMonth() + 1]}-${PAD2[date.getUTCDate()]}`
305
- : `${date.getFullYear()}-${PAD2[date.getMonth() + 1]}-${PAD2[date.getDate()]}`;
306
- }
307
- return renderFormat(date, format, utc);
308
- }
309
- function tzOffset(d) {
310
- const off = -d.getTimezoneOffset();
311
- const sign = off >= 0 ? "+" : "-";
312
- const h = (Math.abs(off) / 60) | 0; // Bitwise OR faster than Math.floor
313
- const m = Math.abs(off) % 60;
314
- return `${sign}${PAD2[h]}:${PAD2[m]}`;
315
- }
316
- function renderFormat(d, fmt, utc) {
317
- // Handle escaped sections [...]
318
- const esc = [];
319
- let out = fmt.replace(/\[([^\]]*)\]/g, (_, c) => {
320
- esc.push(c);
321
- return `\x00${esc.length - 1}\x00`;
322
- });
323
- // Get components once
324
- const y = utc ? d.getUTCFullYear() : d.getFullYear();
325
- const M = utc ? d.getUTCMonth() + 1 : d.getMonth() + 1;
326
- const D = utc ? d.getUTCDate() : d.getDate();
327
- const H = utc ? d.getUTCHours() : d.getHours();
328
- const m = utc ? d.getUTCMinutes() : d.getMinutes();
329
- const s = utc ? d.getUTCSeconds() : d.getSeconds();
330
- const ms = utc ? d.getUTCMilliseconds() : d.getMilliseconds();
331
- // Replace tokens
332
- out = out
333
- .replace(/YYYY/g, String(y))
334
- .replace(/SSS/g, ms < 10 ? `00${ms}` : ms < 100 ? `0${ms}` : String(ms))
335
- .replace(/MM/g, PAD2[M])
336
- .replace(/DD/g, PAD2[D])
337
- .replace(/HH/g, PAD2[H])
338
- .replace(/mm/g, PAD2[m])
339
- .replace(/ss/g, PAD2[s])
340
- .replace(/Z/g, utc ? "Z" : tzOffset(d));
341
- // Restore escaped
342
- if (esc.length) {
343
- // oxlint-disable-next-line no-control-regex
344
- out = out.replace(/\x00(\d+)\x00/g, (_, i) => esc[+i]);
345
- }
346
- return out;
347
- }
348
- // ============================================================================
349
193
  // High-performance batch processors (class-based for state encapsulation)
350
194
  // ============================================================================
351
195
  /**
@@ -419,6 +263,13 @@ class DateParser {
419
263
  }
420
264
  }
421
265
  exports.DateParser = DateParser;
266
+ function tzOffset(d) {
267
+ const off = -d.getTimezoneOffset();
268
+ const sign = off >= 0 ? "+" : "-";
269
+ const h = (Math.abs(off) / 60) | 0; // Bitwise OR faster than Math.floor
270
+ const m = Math.abs(off) % 60;
271
+ return `${sign}${PAD2[h]}:${PAD2[m]}`;
272
+ }
422
273
  /**
423
274
  * Optimized date formatter for batch processing
424
275
  *
@@ -46,6 +46,8 @@ class PivotCacheDefinitionXform extends base_xform_1.BaseXform {
46
46
  */
47
47
  renderNew(xmlStream, model) {
48
48
  const { source, cacheFields } = model;
49
+ // Record count = number of data rows (excluding header row)
50
+ const recordCount = source.getSheetValues().slice(2).length;
49
51
  xmlStream.openXml(xml_stream_1.XmlStream.StdDocAttributes);
50
52
  xmlStream.openNode(this.tag, {
51
53
  ...PivotCacheDefinitionXform.PIVOT_CACHE_DEFINITION_ATTRIBUTES,
@@ -56,7 +58,7 @@ class PivotCacheDefinitionXform extends base_xform_1.BaseXform {
56
58
  createdVersion: "8",
57
59
  refreshedVersion: "8",
58
60
  minRefreshableVersion: "3",
59
- recordCount: cacheFields.length + 1
61
+ recordCount
60
62
  });
61
63
  xmlStream.openNode("cacheSource", { type: "worksheet" });
62
64
  xmlStream.leafNode("worksheetSource", {
@@ -60,6 +60,22 @@ class PivotTableXform extends base_xform_1.BaseXform {
60
60
  const { rows, columns, values, cacheFields, cacheId, applyWidthHeightFormats } = model;
61
61
  // Generate unique UID for each pivot table to prevent Excel treating them as identical
62
62
  const uniqueUid = `{${crypto.randomUUID().toUpperCase()}}`;
63
+ // Build rowItems - need one <i> for each unique value in row fields, plus grand total
64
+ const rowItems = buildRowItems(rows, cacheFields);
65
+ // Build colItems - need one <i> for each unique value in col fields, plus grand total
66
+ const colItems = buildColItems(columns, cacheFields, values.length);
67
+ // Calculate pivot table dimensions
68
+ const rowFieldItemCount = rows.length > 0 ? cacheFields[rows[0]]?.sharedItems?.length || 0 : 0;
69
+ const colFieldItemCount = columns.length > 0 ? cacheFields[columns[0]]?.sharedItems?.length || 0 : 0;
70
+ // Location: A3 is where pivot table starts
71
+ // - firstHeaderRow: 1 (column headers are in first row of pivot table)
72
+ // - firstDataRow: 2 (data starts in second row)
73
+ // - 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
76
+ const endCol = 1 + colFieldItemCount + 1; // start col + data cols + grand total
77
+ const endColLetter = String.fromCharCode(64 + endCol);
78
+ const locationRef = `A3:${endColLetter}${endRow}`;
63
79
  xmlStream.openXml(xml_stream_1.XmlStream.StdDocAttributes);
64
80
  xmlStream.openNode(this.tag, {
65
81
  ...PivotTableXform.PIVOT_TABLE_ATTRIBUTES,
@@ -84,23 +100,23 @@ class PivotTableXform extends base_xform_1.BaseXform {
84
100
  multipleFieldFilters: "0"
85
101
  });
86
102
  xmlStream.writeXml(`
87
- <location ref="A3:E15" firstHeaderRow="1" firstDataRow="2" firstDataCol="1" />
103
+ <location ref="${locationRef}" firstHeaderRow="1" firstDataRow="2" firstDataCol="1" />
88
104
  <pivotFields count="${cacheFields.length}">
89
105
  ${renderPivotFields(model)}
90
106
  </pivotFields>
91
107
  <rowFields count="${rows.length}">
92
108
  ${rows.map(rowIndex => `<field x="${rowIndex}" />`).join("\n ")}
93
109
  </rowFields>
94
- <rowItems count="1">
95
- <i t="grand"><x /></i>
110
+ <rowItems count="${rowItems.count}">
111
+ ${rowItems.xml}
96
112
  </rowItems>
97
113
  <colFields count="${columns.length === 0 ? 1 : columns.length}">
98
114
  ${columns.length === 0
99
115
  ? '<field x="-2" />'
100
116
  : columns.map(columnIndex => `<field x="${columnIndex}" />`).join("\n ")}
101
117
  </colFields>
102
- <colItems count="1">
103
- <i t="grand"><x /></i>
118
+ <colItems count="${colItems.count}">
119
+ ${colItems.xml}
104
120
  </colItems>
105
121
  <dataFields count="${values.length}">
106
122
  ${buildDataFields(cacheFields, values, model.metric)}
@@ -464,6 +480,70 @@ PivotTableXform.PIVOT_TABLE_ATTRIBUTES = {
464
480
  "xmlns:xr": "http://schemas.microsoft.com/office/spreadsheetml/2014/revision"
465
481
  };
466
482
  // Helpers
483
+ /**
484
+ * Build rowItems XML - one item for each unique value in row fields, plus grand total.
485
+ * Each <i> represents a row in the pivot table.
486
+ * - Regular items: <i><x v="index"/></i> where index is the position in sharedItems
487
+ * - Grand total: <i t="grand"><x/></i>
488
+ */
489
+ function buildRowItems(rows, cacheFields) {
490
+ if (rows.length === 0) {
491
+ // No row fields - just grand total
492
+ return { count: 1, xml: '<i t="grand"><x /></i>' };
493
+ }
494
+ // Get unique values count from the first row field
495
+ const rowFieldIndex = rows[0];
496
+ const sharedItems = cacheFields[rowFieldIndex]?.sharedItems || [];
497
+ const itemCount = sharedItems.length;
498
+ // Build items: one for each unique value + grand total
499
+ const items = [];
500
+ // Regular items - reference each unique value by index
501
+ for (let i = 0; i < itemCount; i++) {
502
+ items.push(`<i><x v="${i}" /></i>`);
503
+ }
504
+ // Grand total row
505
+ items.push('<i t="grand"><x /></i>');
506
+ return {
507
+ count: items.length,
508
+ xml: items.join("\n ")
509
+ };
510
+ }
511
+ /**
512
+ * Build colItems XML - one item for each unique value in column fields, plus grand total.
513
+ * When there are multiple data fields (values), each column value may have sub-columns.
514
+ */
515
+ function buildColItems(columns, cacheFields, valueCount) {
516
+ if (columns.length === 0) {
517
+ // No column fields - columns are based on data fields (values)
518
+ if (valueCount > 1) {
519
+ // Multiple values: one column per value + grand total
520
+ const items = [];
521
+ for (let i = 0; i < valueCount; i++) {
522
+ items.push(`<i><x v="${i}" /></i>`);
523
+ }
524
+ items.push('<i t="grand"><x /></i>');
525
+ return { count: items.length, xml: items.join("\n ") };
526
+ }
527
+ // Single value: just grand total
528
+ return { count: 1, xml: '<i t="grand"><x /></i>' };
529
+ }
530
+ // Get unique values count from the first column field
531
+ const colFieldIndex = columns[0];
532
+ const sharedItems = cacheFields[colFieldIndex]?.sharedItems || [];
533
+ const itemCount = sharedItems.length;
534
+ // Build items: one for each unique value + grand total
535
+ const items = [];
536
+ // Regular items - reference each unique value by index
537
+ for (let i = 0; i < itemCount; i++) {
538
+ items.push(`<i><x v="${i}" /></i>`);
539
+ }
540
+ // Grand total column
541
+ items.push('<i t="grand"><x /></i>');
542
+ return {
543
+ count: items.length,
544
+ xml: items.join("\n ")
545
+ };
546
+ }
467
547
  /**
468
548
  * Build dataField XML elements for all values in the pivot table.
469
549
  * Supports multiple values when columns is empty.
@@ -504,10 +584,15 @@ function renderPivotField(fieldType, sharedItems) {
504
584
  const defaultAttributes = 'compact="0" outline="0" showAll="0" defaultSubtotal="0"';
505
585
  if (fieldType === "row" || fieldType === "column") {
506
586
  const axis = fieldType === "row" ? "axisRow" : "axisCol";
587
+ // items = one for each shared item + one default item
588
+ const itemsXml = [
589
+ ...sharedItems.map((_item, index) => `<item x="${index}" />`),
590
+ '<item t="default" />' // Required default item for subtotals/grand totals
591
+ ].join("\n ");
507
592
  return `
508
593
  <pivotField axis="${axis}" ${defaultAttributes}>
509
594
  <items count="${sharedItems.length + 1}">
510
- ${sharedItems.map((_item, index) => `<item x="${index}" />`).join("\n ")}
595
+ ${itemsXml}
511
596
  </items>
512
597
  </pivotField>
513
598
  `;
@@ -11,7 +11,6 @@
11
11
  const PAD2 = Array.from({ length: 60 }, (_, i) => (i < 10 ? `0${i}` : `${i}`));
12
12
  // Character codes for fast comparison
13
13
  const C_0 = 48;
14
- const C_9 = 57;
15
14
  const C_DASH = 45;
16
15
  const C_SLASH = 47;
17
16
  const C_COLON = 58;
@@ -187,158 +186,6 @@ const AUTO_DETECT = [
187
186
  [29, [parseISOMsOffset]]
188
187
  ];
189
188
  // ============================================================================
190
- // Public API
191
- // ============================================================================
192
- /**
193
- * Quick pre-filter to reject non-date strings
194
- */
195
- export function mightBeDate(s) {
196
- const len = s.length;
197
- if (len < 10 || len > 30) {
198
- return false;
199
- }
200
- const c0 = s.charCodeAt(0);
201
- const c1 = s.charCodeAt(1);
202
- // Must start with digits
203
- if (c0 < C_0 || c0 > C_9 || c1 < C_0 || c1 > C_9) {
204
- return false;
205
- }
206
- const c2 = s.charCodeAt(2);
207
- // Either YYYY format (4 digits) or MM/DD format (separator at pos 2)
208
- return (c2 >= C_0 && c2 <= C_9) || c2 === C_DASH || c2 === C_SLASH;
209
- }
210
- /**
211
- * Parse a date string
212
- *
213
- * @param value - String to parse
214
- * @param formats - Specific formats to try (optional, auto-detects ISO if omitted)
215
- * @returns Date object or null
216
- *
217
- * @example
218
- * parseDate("2024-12-26") // auto-detect ISO
219
- * parseDate("12-26-2024", ["MM-DD-YYYY"]) // explicit US format
220
- */
221
- export function parseDate(value, formats) {
222
- if (!value || typeof value !== "string") {
223
- return null;
224
- }
225
- const s = value.trim();
226
- if (!s) {
227
- return null;
228
- }
229
- // Explicit formats
230
- if (formats?.length) {
231
- for (const fmt of formats) {
232
- const result = PARSERS[fmt]?.(s);
233
- if (result) {
234
- return result;
235
- }
236
- }
237
- return null;
238
- }
239
- // Auto-detect by length
240
- const len = s.length;
241
- for (const [l, parsers] of AUTO_DETECT) {
242
- if (len === l) {
243
- for (const p of parsers) {
244
- const result = p(s);
245
- if (result) {
246
- return result;
247
- }
248
- }
249
- }
250
- }
251
- return null;
252
- }
253
- /**
254
- * Format a Date to string
255
- *
256
- * @param date - Date to format
257
- * @param format - Format template (optional, defaults to ISO)
258
- * @param utc - Use UTC time (default: false)
259
- * @returns Formatted string or empty string if invalid
260
- *
261
- * @example
262
- * formatDate(date) // "2024-12-26T10:30:00.000+08:00"
263
- * formatDate(date, "YYYY-MM-DD", true) // "2024-12-26"
264
- */
265
- export function formatDate(date, format, utc = false) {
266
- if (!(date instanceof Date)) {
267
- return "";
268
- }
269
- const t = date.getTime();
270
- if (t !== t) {
271
- return "";
272
- } // NaN check (faster than isNaN)
273
- // Default ISO format - direct string building (faster than toISOString)
274
- if (!format) {
275
- if (utc) {
276
- const y = date.getUTCFullYear();
277
- const M = date.getUTCMonth() + 1;
278
- const D = date.getUTCDate();
279
- const H = date.getUTCHours();
280
- const m = date.getUTCMinutes();
281
- const s = date.getUTCSeconds();
282
- const ms = date.getUTCMilliseconds();
283
- return `${y}-${PAD2[M]}-${PAD2[D]}T${PAD2[H]}:${PAD2[m]}:${PAD2[s]}.${ms < 10 ? "00" + ms : ms < 100 ? "0" + ms : ms}Z`;
284
- }
285
- const y = date.getFullYear();
286
- const M = date.getMonth() + 1;
287
- const D = date.getDate();
288
- const H = date.getHours();
289
- const m = date.getMinutes();
290
- const s = date.getSeconds();
291
- const ms = date.getMilliseconds();
292
- return `${y}-${PAD2[M]}-${PAD2[D]}T${PAD2[H]}:${PAD2[m]}:${PAD2[s]}.${ms < 10 ? "00" + ms : ms < 100 ? "0" + ms : ms}${tzOffset(date)}`;
293
- }
294
- // Fast paths for common formats
295
- if (format === "YYYY-MM-DD") {
296
- return utc
297
- ? `${date.getUTCFullYear()}-${PAD2[date.getUTCMonth() + 1]}-${PAD2[date.getUTCDate()]}`
298
- : `${date.getFullYear()}-${PAD2[date.getMonth() + 1]}-${PAD2[date.getDate()]}`;
299
- }
300
- return renderFormat(date, format, utc);
301
- }
302
- function tzOffset(d) {
303
- const off = -d.getTimezoneOffset();
304
- const sign = off >= 0 ? "+" : "-";
305
- const h = (Math.abs(off) / 60) | 0; // Bitwise OR faster than Math.floor
306
- const m = Math.abs(off) % 60;
307
- return `${sign}${PAD2[h]}:${PAD2[m]}`;
308
- }
309
- function renderFormat(d, fmt, utc) {
310
- // Handle escaped sections [...]
311
- const esc = [];
312
- let out = fmt.replace(/\[([^\]]*)\]/g, (_, c) => {
313
- esc.push(c);
314
- return `\x00${esc.length - 1}\x00`;
315
- });
316
- // Get components once
317
- const y = utc ? d.getUTCFullYear() : d.getFullYear();
318
- const M = utc ? d.getUTCMonth() + 1 : d.getMonth() + 1;
319
- const D = utc ? d.getUTCDate() : d.getDate();
320
- const H = utc ? d.getUTCHours() : d.getHours();
321
- const m = utc ? d.getUTCMinutes() : d.getMinutes();
322
- const s = utc ? d.getUTCSeconds() : d.getSeconds();
323
- const ms = utc ? d.getUTCMilliseconds() : d.getMilliseconds();
324
- // Replace tokens
325
- out = out
326
- .replace(/YYYY/g, String(y))
327
- .replace(/SSS/g, ms < 10 ? `00${ms}` : ms < 100 ? `0${ms}` : String(ms))
328
- .replace(/MM/g, PAD2[M])
329
- .replace(/DD/g, PAD2[D])
330
- .replace(/HH/g, PAD2[H])
331
- .replace(/mm/g, PAD2[m])
332
- .replace(/ss/g, PAD2[s])
333
- .replace(/Z/g, utc ? "Z" : tzOffset(d));
334
- // Restore escaped
335
- if (esc.length) {
336
- // oxlint-disable-next-line no-control-regex
337
- out = out.replace(/\x00(\d+)\x00/g, (_, i) => esc[+i]);
338
- }
339
- return out;
340
- }
341
- // ============================================================================
342
189
  // High-performance batch processors (class-based for state encapsulation)
343
190
  // ============================================================================
344
191
  /**
@@ -411,6 +258,13 @@ export class DateParser {
411
258
  return out;
412
259
  }
413
260
  }
261
+ function tzOffset(d) {
262
+ const off = -d.getTimezoneOffset();
263
+ const sign = off >= 0 ? "+" : "-";
264
+ const h = (Math.abs(off) / 60) | 0; // Bitwise OR faster than Math.floor
265
+ const m = Math.abs(off) % 60;
266
+ return `${sign}${PAD2[h]}:${PAD2[m]}`;
267
+ }
414
268
  /**
415
269
  * Optimized date formatter for batch processing
416
270
  *
@@ -43,6 +43,8 @@ class PivotCacheDefinitionXform extends BaseXform {
43
43
  */
44
44
  renderNew(xmlStream, model) {
45
45
  const { source, cacheFields } = model;
46
+ // Record count = number of data rows (excluding header row)
47
+ const recordCount = source.getSheetValues().slice(2).length;
46
48
  xmlStream.openXml(XmlStream.StdDocAttributes);
47
49
  xmlStream.openNode(this.tag, {
48
50
  ...PivotCacheDefinitionXform.PIVOT_CACHE_DEFINITION_ATTRIBUTES,
@@ -53,7 +55,7 @@ class PivotCacheDefinitionXform extends BaseXform {
53
55
  createdVersion: "8",
54
56
  refreshedVersion: "8",
55
57
  minRefreshableVersion: "3",
56
- recordCount: cacheFields.length + 1
58
+ recordCount
57
59
  });
58
60
  xmlStream.openNode("cacheSource", { type: "worksheet" });
59
61
  xmlStream.leafNode("worksheetSource", {
@@ -57,6 +57,22 @@ class PivotTableXform extends BaseXform {
57
57
  const { rows, columns, values, cacheFields, cacheId, applyWidthHeightFormats } = model;
58
58
  // Generate unique UID for each pivot table to prevent Excel treating them as identical
59
59
  const uniqueUid = `{${crypto.randomUUID().toUpperCase()}}`;
60
+ // Build rowItems - need one <i> for each unique value in row fields, plus grand total
61
+ const rowItems = buildRowItems(rows, cacheFields);
62
+ // Build colItems - need one <i> for each unique value in col fields, plus grand total
63
+ const colItems = buildColItems(columns, cacheFields, values.length);
64
+ // Calculate pivot table dimensions
65
+ const rowFieldItemCount = rows.length > 0 ? cacheFields[rows[0]]?.sharedItems?.length || 0 : 0;
66
+ const colFieldItemCount = columns.length > 0 ? cacheFields[columns[0]]?.sharedItems?.length || 0 : 0;
67
+ // Location: A3 is where pivot table starts
68
+ // - firstHeaderRow: 1 (column headers are in first row of pivot table)
69
+ // - firstDataRow: 2 (data starts in second row)
70
+ // - firstDataCol: 1 (data starts in second column, after row labels)
71
+ // Calculate ref based on actual data size
72
+ const endRow = 3 + rowFieldItemCount + 1; // start row + data rows + grand total
73
+ const endCol = 1 + colFieldItemCount + 1; // start col + data cols + grand total
74
+ const endColLetter = String.fromCharCode(64 + endCol);
75
+ const locationRef = `A3:${endColLetter}${endRow}`;
60
76
  xmlStream.openXml(XmlStream.StdDocAttributes);
61
77
  xmlStream.openNode(this.tag, {
62
78
  ...PivotTableXform.PIVOT_TABLE_ATTRIBUTES,
@@ -81,23 +97,23 @@ class PivotTableXform extends BaseXform {
81
97
  multipleFieldFilters: "0"
82
98
  });
83
99
  xmlStream.writeXml(`
84
- <location ref="A3:E15" firstHeaderRow="1" firstDataRow="2" firstDataCol="1" />
100
+ <location ref="${locationRef}" firstHeaderRow="1" firstDataRow="2" firstDataCol="1" />
85
101
  <pivotFields count="${cacheFields.length}">
86
102
  ${renderPivotFields(model)}
87
103
  </pivotFields>
88
104
  <rowFields count="${rows.length}">
89
105
  ${rows.map(rowIndex => `<field x="${rowIndex}" />`).join("\n ")}
90
106
  </rowFields>
91
- <rowItems count="1">
92
- <i t="grand"><x /></i>
107
+ <rowItems count="${rowItems.count}">
108
+ ${rowItems.xml}
93
109
  </rowItems>
94
110
  <colFields count="${columns.length === 0 ? 1 : columns.length}">
95
111
  ${columns.length === 0
96
112
  ? '<field x="-2" />'
97
113
  : columns.map(columnIndex => `<field x="${columnIndex}" />`).join("\n ")}
98
114
  </colFields>
99
- <colItems count="1">
100
- <i t="grand"><x /></i>
115
+ <colItems count="${colItems.count}">
116
+ ${colItems.xml}
101
117
  </colItems>
102
118
  <dataFields count="${values.length}">
103
119
  ${buildDataFields(cacheFields, values, model.metric)}
@@ -460,6 +476,70 @@ PivotTableXform.PIVOT_TABLE_ATTRIBUTES = {
460
476
  "xmlns:xr": "http://schemas.microsoft.com/office/spreadsheetml/2014/revision"
461
477
  };
462
478
  // Helpers
479
+ /**
480
+ * Build rowItems XML - one item for each unique value in row fields, plus grand total.
481
+ * Each <i> represents a row in the pivot table.
482
+ * - Regular items: <i><x v="index"/></i> where index is the position in sharedItems
483
+ * - Grand total: <i t="grand"><x/></i>
484
+ */
485
+ function buildRowItems(rows, cacheFields) {
486
+ if (rows.length === 0) {
487
+ // No row fields - just grand total
488
+ return { count: 1, xml: '<i t="grand"><x /></i>' };
489
+ }
490
+ // Get unique values count from the first row field
491
+ const rowFieldIndex = rows[0];
492
+ const sharedItems = cacheFields[rowFieldIndex]?.sharedItems || [];
493
+ const itemCount = sharedItems.length;
494
+ // Build items: one for each unique value + grand total
495
+ const items = [];
496
+ // Regular items - reference each unique value by index
497
+ for (let i = 0; i < itemCount; i++) {
498
+ items.push(`<i><x v="${i}" /></i>`);
499
+ }
500
+ // Grand total row
501
+ items.push('<i t="grand"><x /></i>');
502
+ return {
503
+ count: items.length,
504
+ xml: items.join("\n ")
505
+ };
506
+ }
507
+ /**
508
+ * Build colItems XML - one item for each unique value in column fields, plus grand total.
509
+ * When there are multiple data fields (values), each column value may have sub-columns.
510
+ */
511
+ function buildColItems(columns, cacheFields, valueCount) {
512
+ if (columns.length === 0) {
513
+ // No column fields - columns are based on data fields (values)
514
+ if (valueCount > 1) {
515
+ // Multiple values: one column per value + grand total
516
+ const items = [];
517
+ for (let i = 0; i < valueCount; i++) {
518
+ items.push(`<i><x v="${i}" /></i>`);
519
+ }
520
+ items.push('<i t="grand"><x /></i>');
521
+ return { count: items.length, xml: items.join("\n ") };
522
+ }
523
+ // Single value: just grand total
524
+ return { count: 1, xml: '<i t="grand"><x /></i>' };
525
+ }
526
+ // Get unique values count from the first column field
527
+ const colFieldIndex = columns[0];
528
+ const sharedItems = cacheFields[colFieldIndex]?.sharedItems || [];
529
+ const itemCount = sharedItems.length;
530
+ // Build items: one for each unique value + grand total
531
+ const items = [];
532
+ // Regular items - reference each unique value by index
533
+ for (let i = 0; i < itemCount; i++) {
534
+ items.push(`<i><x v="${i}" /></i>`);
535
+ }
536
+ // Grand total column
537
+ items.push('<i t="grand"><x /></i>');
538
+ return {
539
+ count: items.length,
540
+ xml: items.join("\n ")
541
+ };
542
+ }
463
543
  /**
464
544
  * Build dataField XML elements for all values in the pivot table.
465
545
  * Supports multiple values when columns is empty.
@@ -500,10 +580,15 @@ function renderPivotField(fieldType, sharedItems) {
500
580
  const defaultAttributes = 'compact="0" outline="0" showAll="0" defaultSubtotal="0"';
501
581
  if (fieldType === "row" || fieldType === "column") {
502
582
  const axis = fieldType === "row" ? "axisRow" : "axisCol";
583
+ // items = one for each shared item + one default item
584
+ const itemsXml = [
585
+ ...sharedItems.map((_item, index) => `<item x="${index}" />`),
586
+ '<item t="default" />' // Required default item for subtotals/grand totals
587
+ ].join("\n ");
503
588
  return `
504
589
  <pivotField axis="${axis}" ${defaultAttributes}>
505
590
  <items count="${sharedItems.length + 1}">
506
- ${sharedItems.map((_item, index) => `<item x="${index}" />`).join("\n ")}
591
+ ${itemsXml}
507
592
  </items>
508
593
  </pivotField>
509
594
  `;