Skip to content

feat: add 'count' metric for pivot table #2885

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions lib/doc/pivot-table.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ function makePivotTable(worksheet, model) {
// rows: ['A', 'B'],
// columns: ['C'],
// values: ['E'], // only 1 item possible for now
// metric: 'sum', // only 'sum' possible for now
// metric: 'sum', 'count' // only 'sum' and 'count' are possible for now
// }

validate(worksheet, model);

const {sourceSheet} = model;
let {rows, columns, values} = model;
const {metric} = model;

const cacheFields = makeCacheFields(sourceSheet, [...rows, ...columns]);

Expand All @@ -42,7 +43,7 @@ function makePivotTable(worksheet, model) {
rows,
columns,
values,
metric: 'sum',
metric,
cacheFields,
// defined in <pivotTableDefinition> of xl/pivotTables/pivotTable1.xml;
// also used in xl/workbook.xml
Expand All @@ -57,8 +58,8 @@ function validate(worksheet, model) {
);
}

if (model.metric && model.metric !== 'sum') {
throw new Error('Only the "sum" metric is supported at this time.');
if (model.metric && model.metric !== 'sum' && model.metric !== 'count') {
throw new Error('Only the "sum" and "count" metric is supported at this time.');
}

const headerNames = model.sourceSheet.getRow(1).values.slice(1);
Expand Down
3 changes: 2 additions & 1 deletion lib/xlsx/xform/pivot-table/pivot-table-xform.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,11 @@ class PivotTableXform extends BaseXform {
</colItems>
<dataFields count="${values.length}">
<dataField
name="Sum of ${cacheFields[values[0]].name}"
name="${metric === 'count' ? 'Count' : 'Sum'} of ${cacheFields[values[0]].name}"
fld="${values[0]}"
baseField="0"
baseItem="0"
${metric === 'count' ? 'subtotal="count"' : ''}
/>
</dataFields>
<pivotTableStyleInfo
Expand Down
78 changes: 78 additions & 0 deletions spec/integration/workbook/pivot-tables-with-count.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// *Note*: `fs.promises` not supported before Node.js 11.14.0;
// ExcelJS version range '>=8.3.0' (as of 2023-10-08).
const fs = require('fs');
const {promisify} = require('util');

const fsReadFileAsync = promisify(fs.readFile);

const JSZip = require('jszip');

const ExcelJS = verquire('exceljs');

const PIVOT_TABLE_FILEPATHS = [
'xl/pivotCache/pivotCacheRecords1.xml',
'xl/pivotCache/pivotCacheDefinition1.xml',
'xl/pivotCache/_rels/pivotCacheDefinition1.xml.rels',
'xl/pivotTables/pivotTable1.xml',
'xl/pivotTables/_rels/pivotTable1.xml.rels',
];

const TEST_XLSX_FILEPATH = './spec/out/wb.test.xlsx';

const TEST_DATA = [
['A', 'B', 'C', 'D', 'E'],
['a1', 'b1', 'c1', 4, 5],
['a1', 'b2', 'c1', 4, 5],
['a2', 'b1', 'c2', 14, 24],
['a2', 'b2', 'c2', 24, 35],
['a3', 'b1', 'c3', 34, 45],
['a3', 'b2', 'c3', 44, 45],
];

// =============================================================================
// Tests

describe('Workbook', () => {
describe('Pivot Tables with count', () => {
it('if pivot table added, then certain xml and rels files are added', async () => {
const workbook = new ExcelJS.Workbook();

const worksheet1 = workbook.addWorksheet('Sheet1');
worksheet1.addRows(TEST_DATA);

const worksheet2 = workbook.addWorksheet('Sheet2');
worksheet2.addPivotTable({
sourceSheet: worksheet1,
rows: ['A', 'B'],
columns: ['C'],
values: ['E'],
metric: 'count',
});

return workbook.xlsx.writeFile(TEST_XLSX_FILEPATH).then(async () => {
const buffer = await fsReadFileAsync(TEST_XLSX_FILEPATH);
const zip = await JSZip.loadAsync(buffer);
for (const filepath of PIVOT_TABLE_FILEPATHS) {
expect(zip.files[filepath]).to.not.be.undefined();
}
});
});

it('if pivot table NOT added, then certain xml and rels files are not added', () => {
const workbook = new ExcelJS.Workbook();

const worksheet1 = workbook.addWorksheet('Sheet1');
worksheet1.addRows(TEST_DATA);

workbook.addWorksheet('Sheet2');

return workbook.xlsx.writeFile(TEST_XLSX_FILEPATH).then(async () => {
const buffer = await fsReadFileAsync(TEST_XLSX_FILEPATH);
const zip = await JSZip.loadAsync(buffer);
for (const filepath of PIVOT_TABLE_FILEPATHS) {
expect(zip.files[filepath]).to.be.undefined();
}
});
});
});
});
54 changes: 54 additions & 0 deletions test/test-pivot-table-with-count.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// --------------------------------------------------
// This enables the generation of a XLSX pivot table
// with several restrictions
//
// Last updated: 2023-10-19
// --------------------------------------------------
/* eslint-disable */

function main(filepath) {
const Excel = require('../lib/exceljs.nodejs.js');

const workbook = new Excel.Workbook();

const worksheet1 = workbook.addWorksheet('Sheet1');
worksheet1.addRows([
['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'],
['a1', 'b1', 'c1', 'd1', 'e1', 'f1', 4, 5],
['a1', 'b2', 'c1', 'd2', 'e1', 'f1', 4, 5],
['a2', 'b1', 'c2', 'd1', 'e2', 'f1', 14, 24],
['a2', 'b2', 'c2', 'd2', 'e2', 'f2', 24, 35],
['a3', 'b1', 'c3', 'd1', 'e3', 'f2', 34, 45],
['a3', 'b2', 'c3', 'd2', 'e3', 'f2', 44, 45],
]);

const worksheet2 = workbook.addWorksheet('Sheet2');
worksheet2.addPivotTable({
// Source of data: the entire sheet range is taken;
// akin to `worksheet1.getSheetValues()`.
sourceSheet: worksheet1,
// Pivot table fields: values indicate field names;
// they come from the first row in `worksheet1`.
rows: ['A', 'B', 'E'],
columns: ['C', 'D'],
values: ['H'], // only 1 item possible for now
metric: 'count', // only 'sum' and 'count' are possible for now
});

save(workbook, filepath);
}

function save(workbook, filepath) {
const HrStopwatch = require('./utils/hr-stopwatch.js');
const stopwatch = new HrStopwatch();
stopwatch.start();

workbook.xlsx.writeFile(filepath).then(() => {
const microseconds = stopwatch.microseconds;
console.log('Done.');
console.log('Time taken:', microseconds);
});
}

const [, , filepath] = process.argv;
main(filepath);