Skip to content

Commit 521c379

Browse files
protobi-pieterclaudedsilva01
committed
Update #12 Adopt upstream PR exceljs#2885: Add 'count' metric for pivot tables
Adds support for metric: 'count' alongside existing metric: 'sum' in pivot tables. This enables count-based aggregations, a widely used feature in Excel pivot tables. Changes: - lib/doc/pivot-table.js: Accept metric from model, update validation - lib/xlsx/xform/pivot-table/pivot-table-xform.js: Generate XML with subtotal='count' - spec/integration/workbook/pivot-tables-with-count.spec.js: Integration tests - test/test-pivot-table-with-count.js: Manual test script Upstream PR: exceljs#2885 Original Author: Desiderio Silva (@dsilva01) Tests: All existing + new pivot table count tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> Co-Authored-By: Desiderio Silva <[email protected]>
1 parent e064c9e commit 521c379

File tree

4 files changed

+139
-5
lines changed

4 files changed

+139
-5
lines changed

lib/doc/pivot-table.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@ function makePivotTable(worksheet, model) {
1414
// rows: ['A', 'B'],
1515
// columns: ['C'],
1616
// values: ['E'], // only 1 item possible for now
17-
// metric: 'sum', // only 'sum' possible for now
17+
// metric: 'sum', 'count' // only 'sum' and 'count' are possible for now
1818
// }
1919

2020
validate(worksheet, model);
2121

2222
const {sourceSheet} = model;
2323
let {rows, columns, values} = model;
24+
const {metric} = model;
2425

2526
// Generate sharedItems for ALL fields in the source, not just the ones used by this pivot table
2627
// This ensures Excel can properly display any field configuration
@@ -49,7 +50,7 @@ function makePivotTable(worksheet, model) {
4950
rows,
5051
columns,
5152
values,
52-
metric: 'sum',
53+
metric,
5354
cacheFields,
5455
// defined in <pivotTableDefinition> of xl/pivotTables/pivotTableN.xml;
5556
// also used in xl/workbook.xml
@@ -64,8 +65,8 @@ function makePivotTable(worksheet, model) {
6465
function validate(worksheet, model) {
6566
// Note: Multiple pivot tables are now supported
6667

67-
if (model.metric && model.metric !== 'sum') {
68-
throw new Error('Only the "sum" metric is supported at this time.');
68+
if (model.metric && model.metric !== 'sum' && model.metric !== 'count') {
69+
throw new Error('Only the "sum" and "count" metric is supported at this time.');
6970
}
7071

7172
const headerNames = model.sourceSheet.getRow(1).values.slice(1);

lib/xlsx/xform/pivot-table/pivot-table-xform.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,11 @@ class PivotTableXform extends BaseXform {
8585
</colItems>
8686
<dataFields count="${values.length}">
8787
<dataField
88-
name="Sum of ${cacheFields[values[0]].name}"
88+
name="${metric === 'count' ? 'Count' : 'Sum'} of ${cacheFields[values[0]].name}"
8989
fld="${values[0]}"
9090
baseField="0"
9191
baseItem="0"
92+
${metric === 'count' ? 'subtotal="count"' : ''}
9293
/>
9394
</dataFields>
9495
<pivotTableStyleInfo
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// *Note*: `fs.promises` not supported before Node.js 11.14.0;
2+
// ExcelJS version range '>=8.3.0' (as of 2023-10-08).
3+
const fs = require('fs');
4+
const {promisify} = require('util');
5+
6+
const fsReadFileAsync = promisify(fs.readFile);
7+
8+
const JSZip = require('jszip');
9+
10+
const ExcelJS = verquire('exceljs');
11+
12+
const PIVOT_TABLE_FILEPATHS = [
13+
'xl/pivotCache/pivotCacheRecords1.xml',
14+
'xl/pivotCache/pivotCacheDefinition1.xml',
15+
'xl/pivotCache/_rels/pivotCacheDefinition1.xml.rels',
16+
'xl/pivotTables/pivotTable1.xml',
17+
'xl/pivotTables/_rels/pivotTable1.xml.rels',
18+
];
19+
20+
const TEST_XLSX_FILEPATH = './spec/out/wb.test.xlsx';
21+
22+
const TEST_DATA = [
23+
['A', 'B', 'C', 'D', 'E'],
24+
['a1', 'b1', 'c1', 4, 5],
25+
['a1', 'b2', 'c1', 4, 5],
26+
['a2', 'b1', 'c2', 14, 24],
27+
['a2', 'b2', 'c2', 24, 35],
28+
['a3', 'b1', 'c3', 34, 45],
29+
['a3', 'b2', 'c3', 44, 45],
30+
];
31+
32+
// =============================================================================
33+
// Tests
34+
35+
describe('Workbook', () => {
36+
describe('Pivot Tables with count', () => {
37+
it('if pivot table added, then certain xml and rels files are added', async () => {
38+
const workbook = new ExcelJS.Workbook();
39+
40+
const worksheet1 = workbook.addWorksheet('Sheet1');
41+
worksheet1.addRows(TEST_DATA);
42+
43+
const worksheet2 = workbook.addWorksheet('Sheet2');
44+
worksheet2.addPivotTable({
45+
sourceSheet: worksheet1,
46+
rows: ['A', 'B'],
47+
columns: ['C'],
48+
values: ['E'],
49+
metric: 'count',
50+
});
51+
52+
return workbook.xlsx.writeFile(TEST_XLSX_FILEPATH).then(async () => {
53+
const buffer = await fsReadFileAsync(TEST_XLSX_FILEPATH);
54+
const zip = await JSZip.loadAsync(buffer);
55+
for (const filepath of PIVOT_TABLE_FILEPATHS) {
56+
expect(zip.files[filepath]).to.not.be.undefined();
57+
}
58+
});
59+
});
60+
61+
it('if pivot table NOT added, then certain xml and rels files are not added', () => {
62+
const workbook = new ExcelJS.Workbook();
63+
64+
const worksheet1 = workbook.addWorksheet('Sheet1');
65+
worksheet1.addRows(TEST_DATA);
66+
67+
workbook.addWorksheet('Sheet2');
68+
69+
return workbook.xlsx.writeFile(TEST_XLSX_FILEPATH).then(async () => {
70+
const buffer = await fsReadFileAsync(TEST_XLSX_FILEPATH);
71+
const zip = await JSZip.loadAsync(buffer);
72+
for (const filepath of PIVOT_TABLE_FILEPATHS) {
73+
expect(zip.files[filepath]).to.be.undefined();
74+
}
75+
});
76+
});
77+
});
78+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// --------------------------------------------------
2+
// This enables the generation of a XLSX pivot table
3+
// with several restrictions
4+
//
5+
// Last updated: 2023-10-19
6+
// --------------------------------------------------
7+
/* eslint-disable */
8+
9+
function main(filepath) {
10+
const Excel = require('../lib/exceljs.nodejs.js');
11+
12+
const workbook = new Excel.Workbook();
13+
14+
const worksheet1 = workbook.addWorksheet('Sheet1');
15+
worksheet1.addRows([
16+
['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'],
17+
['a1', 'b1', 'c1', 'd1', 'e1', 'f1', 4, 5],
18+
['a1', 'b2', 'c1', 'd2', 'e1', 'f1', 4, 5],
19+
['a2', 'b1', 'c2', 'd1', 'e2', 'f1', 14, 24],
20+
['a2', 'b2', 'c2', 'd2', 'e2', 'f2', 24, 35],
21+
['a3', 'b1', 'c3', 'd1', 'e3', 'f2', 34, 45],
22+
['a3', 'b2', 'c3', 'd2', 'e3', 'f2', 44, 45],
23+
]);
24+
25+
const worksheet2 = workbook.addWorksheet('Sheet2');
26+
worksheet2.addPivotTable({
27+
// Source of data: the entire sheet range is taken;
28+
// akin to `worksheet1.getSheetValues()`.
29+
sourceSheet: worksheet1,
30+
// Pivot table fields: values indicate field names;
31+
// they come from the first row in `worksheet1`.
32+
rows: ['A', 'B', 'E'],
33+
columns: ['C', 'D'],
34+
values: ['H'], // only 1 item possible for now
35+
metric: 'count', // only 'sum' and 'count' are possible for now
36+
});
37+
38+
save(workbook, filepath);
39+
}
40+
41+
function save(workbook, filepath) {
42+
const HrStopwatch = require('./utils/hr-stopwatch.js');
43+
const stopwatch = new HrStopwatch();
44+
stopwatch.start();
45+
46+
workbook.xlsx.writeFile(filepath).then(() => {
47+
const microseconds = stopwatch.microseconds;
48+
console.log('Done.');
49+
console.log('Time taken:', microseconds);
50+
});
51+
}
52+
53+
const [, , filepath] = process.argv;
54+
main(filepath);

0 commit comments

Comments
 (0)