Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit a3d9929

Browse files
committed
Add pivot table with limitations
```js worksheet.addPivotTable(configuration); ``` **Note:** Pivot table support is in its early stages with certain limitations, including: - No support for reading xlsx documents with existing pivot tables (writing is supported). - Pivot table configurations must consist of 2 rows, 1 column, 1 value, and use the sum metric. The source data can have any number of columns and rows. - Only one pivot table can be added for the entire document.
1 parent 3178efd commit a3d9929

18 files changed

+941
-22
lines changed

.prettier

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
"bracketSpacing": false,
33
"printWidth": 100,
44
"trailingComma": "all",
5-
"bracketSpacing": false,
6-
"arrowParens": "avoid"
5+
"arrowParens": "avoid",
6+
"singleQuote": true,
77
}

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ npm install exceljs
1919
# New Features!
2020

2121
<ul>
22+
<li>
23+
Merged <a href="https://github.com/exceljs/exceljs/pull/2551">Add pivot table with limitations #2551</a>.
24+
Many thanks to Protobi and <a href="https://github.com/mikez">Michael</a> for this contribution.
25+
</li>
2226
<li>
2327
Merged <a href="https://github.com/exceljs/exceljs/pull/1656">Add TS declarations of Workbook properties #1656</a>.
2428
Many thanks to <a href="https://github.com/kaoths">Tanawit Kritwongwiman</a> for this contribution.
@@ -163,6 +167,7 @@ To be clear, all contributions added to this library will be included in the lib
163167
</li>
164168
</ul>
165169
</li>
170+
<li><a href="#pivot-tables">Pivot Tables</a></li>
166171
</ul>
167172
</li>
168173
<li><a href="#browser">Browser</a></li>
@@ -2543,6 +2548,48 @@ workbookReader.on('error', (err) => {
25432548
});
25442549
```
25452550

2551+
## Pivot Tables[](#contents)<!-- Link generated with jump2header -->
2552+
2553+
Add a pivot table to a Workbook without existing pivot tables.
2554+
2555+
```javascript
2556+
worksheet.addPivotTable(configuration);
2557+
```
2558+
2559+
**Note:** Pivot table support is in its early stages with certain limitations, including:
2560+
2561+
- No support for reading xlsx documents with existing pivot tables (writing is supported).
2562+
- Pivot table configurations must consist of 2 rows, 1 column, 1 value, and use the sum metric. The source data can have any number of columns and rows.
2563+
- Only one pivot table can be added for the entire document.
2564+
2565+
### Add pivot table to worksheet[](#contents)<!-- Link generated with jump2header -->
2566+
2567+
```javascript
2568+
const workbook = new Excel.Workbook();
2569+
2570+
const worksheet1 = workbook.addWorksheet('Sheet1');
2571+
worksheet1.addRows([
2572+
['A', 'B', 'C', 'D', 'E'],
2573+
['a1', 'b1', 'c1', 4, 5],
2574+
['a1', 'b2', 'c1', 4, 5],
2575+
['a2', 'b1', 'c2', 14, 24],
2576+
['a2', 'b2', 'c2', 24, 35],
2577+
['a3', 'b1', 'c3', 34, 45],
2578+
['a3', 'b2', 'c3', 44, 45],
2579+
]);
2580+
2581+
const worksheet2 = workbook.addWorksheet('Sheet2');
2582+
worksheet2.addPivotTable({
2583+
// Source data: entire sheet range
2584+
sourceSheet: worksheet1,
2585+
// Pivot table fields: via header row in `worksheet1`
2586+
rows: ['A', 'B'], // Exactly 2 fields
2587+
columns: ['C'], // Exactly 1 field
2588+
values: ['E'], // Exactly 1 field
2589+
metric: 'sum', // Metric: 'sum' only
2590+
});
2591+
```
2592+
25462593
# Browser[](#contents)<!-- Link generated with jump2header -->
25472594

25482595
A portion of this library has been isolated and tested for use within a browser environment.

lib/doc/pivot-table.js

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
const {objectFromProps, range, toSortedArray} = require('../utils/utils');
2+
3+
// TK(2023-10-10): turn this into a class constructor.
4+
5+
function makePivotTable(worksheet, model) {
6+
// Example `model`:
7+
// {
8+
// // Source of data: the entire sheet range is taken,
9+
// // akin to `worksheet1.getSheetValues()`.
10+
// sourceSheet: worksheet1,
11+
//
12+
// // Pivot table fields: values indicate field names;
13+
// // they come from the first row in `worksheet1`.
14+
// rows: ['A', 'B'], // only 2 items possible for now
15+
// columns: ['C'], // only 1 item possible for now
16+
// values: ['E'], // only 1 item possible for now
17+
// metric: 'sum', // only 'sum' possible for now
18+
// }
19+
20+
validate(worksheet, model);
21+
22+
const {sourceSheet} = model;
23+
let {rows, columns, values} = model;
24+
25+
const cacheFields = makeCacheFields(sourceSheet, [...rows, ...columns]);
26+
27+
// let {rows, columns, values} use indices instead of names;
28+
// names can then be accessed via `pivotTable.cacheFields[index].name`.
29+
// *Note*: Using `reduce` as `Object.fromEntries` requires Node 12+;
30+
// ExcelJS is >=8.3.0 (as of 2023-10-08).
31+
const nameToIndex = cacheFields.reduce((result, cacheField, index) => {
32+
result[cacheField.name] = index;
33+
return result;
34+
}, {});
35+
rows = rows.map(row => nameToIndex[row]);
36+
columns = columns.map(column => nameToIndex[column]);
37+
values = values.map(value => nameToIndex[value]);
38+
39+
// form pivot table object
40+
return {
41+
sourceSheet,
42+
rows,
43+
columns,
44+
values,
45+
metric: 'sum',
46+
cacheFields,
47+
// defined in <pivotTableDefinition> of xl/pivotTables/pivotTable1.xml;
48+
// also used in xl/workbook.xml
49+
cacheId: '10',
50+
};
51+
}
52+
53+
function validate(worksheet, model) {
54+
if (worksheet.workbook.pivotTables.length === 1) {
55+
throw new Error(
56+
'A pivot table was already added. At this time, ExcelJS supports at most one pivot table per file.'
57+
);
58+
}
59+
60+
if (model.metric && model.metric !== 'sum') {
61+
throw new Error('Only the "sum" metric is supported at this time.');
62+
}
63+
64+
const headerNames = model.sourceSheet.getRow(1).values.slice(1);
65+
const isInHeaderNames = objectFromProps(headerNames, true);
66+
for (const name of [...model.rows, ...model.columns, ...model.values]) {
67+
if (!isInHeaderNames[name]) {
68+
throw new Error(`The header name "${name}" was not found in ${model.sourceSheet.name}.`);
69+
}
70+
}
71+
72+
if (model.rows.length !== 2) {
73+
throw new Error('Exactly 2 rows need to be specified at this time.');
74+
}
75+
76+
if (model.columns.length !== 1) {
77+
throw new Error('Exactly 1 column needs to be specified at this time.');
78+
}
79+
80+
if (model.values.length !== 1) {
81+
throw new Error('Exactly 1 value needs to be specified at this time.');
82+
}
83+
}
84+
85+
function makeCacheFields(worksheet, fieldNamesWithSharedItems) {
86+
// Cache fields are used in pivot tables to reference source data.
87+
//
88+
// Example
89+
// -------
90+
// Turn
91+
//
92+
// `worksheet` sheet values [
93+
// ['A', 'B', 'C', 'D', 'E'],
94+
// ['a1', 'b1', 'c1', 4, 5],
95+
// ['a1', 'b2', 'c1', 4, 5],
96+
// ['a2', 'b1', 'c2', 14, 24],
97+
// ['a2', 'b2', 'c2', 24, 35],
98+
// ['a3', 'b1', 'c3', 34, 45],
99+
// ['a3', 'b2', 'c3', 44, 45]
100+
// ];
101+
// fieldNamesWithSharedItems = ['A', 'B', 'C'];
102+
//
103+
// into
104+
//
105+
// [
106+
// { name: 'A', sharedItems: ['a1', 'a2', 'a3'] },
107+
// { name: 'B', sharedItems: ['b1', 'b2'] },
108+
// { name: 'C', sharedItems: ['c1', 'c2', 'c3'] },
109+
// { name: 'D', sharedItems: null },
110+
// { name: 'E', sharedItems: null }
111+
// ]
112+
113+
const names = worksheet.getRow(1).values;
114+
const nameToHasSharedItems = objectFromProps(fieldNamesWithSharedItems, true);
115+
116+
const aggregate = columnIndex => {
117+
const columnValues = worksheet.getColumn(columnIndex).values.splice(2);
118+
const columnValuesAsSet = new Set(columnValues);
119+
return toSortedArray(columnValuesAsSet);
120+
};
121+
122+
// make result
123+
const result = [];
124+
for (const columnIndex of range(1, names.length)) {
125+
const name = names[columnIndex];
126+
const sharedItems = nameToHasSharedItems[name] ? aggregate(columnIndex) : null;
127+
result.push({name, sharedItems});
128+
}
129+
return result;
130+
}
131+
132+
module.exports = {makePivotTable};

lib/doc/workbook.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class Workbook {
2727
this.title = '';
2828
this.views = [];
2929
this.media = [];
30+
this.pivotTables = [];
3031
this._definedNames = new DefinedNames();
3132
}
3233

@@ -174,6 +175,7 @@ class Workbook {
174175
contentStatus: this.contentStatus,
175176
themes: this._themes,
176177
media: this.media,
178+
pivotTables: this.pivotTables,
177179
calcProperties: this.calcProperties,
178180
};
179181
}
@@ -215,6 +217,7 @@ class Workbook {
215217
this.views = value.views;
216218
this._themes = value.themes;
217219
this.media = value.media || [];
220+
this.pivotTables = value.pivotTables || [];
218221
}
219222
}
220223

lib/doc/worksheet.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const Enums = require('./enums');
88
const Image = require('./image');
99
const Table = require('./table');
1010
const DataValidations = require('./data-validations');
11+
const {makePivotTable} = require('./pivot-table');
1112
const Encryptor = require('../utils/encryptor');
1213
const {copyStyle} = require('../utils/copy-style');
1314

@@ -124,6 +125,8 @@ class Worksheet {
124125
// for tables
125126
this.tables = {};
126127

128+
this.pivotTables = [];
129+
127130
this.conditionalFormattings = [];
128131
}
129132

@@ -806,6 +809,17 @@ class Worksheet {
806809
return Object.values(this.tables);
807810
}
808811

812+
// =========================================================================
813+
// Pivot Tables
814+
addPivotTable(model) {
815+
const pivotTable = makePivotTable(this, model);
816+
817+
this.pivotTables.push(pivotTable);
818+
this.workbook.pivotTables.push(pivotTable);
819+
820+
return pivotTable;
821+
}
822+
809823
// ===========================================================================
810824
// Conditional Formatting
811825
addConditionalFormatting(cf) {
@@ -854,6 +868,7 @@ class Worksheet {
854868
media: this._media.map(medium => medium.model),
855869
sheetProtection: this.sheetProtection,
856870
tables: Object.values(this.tables).map(table => table.model),
871+
pivotTables: this.pivotTables,
857872
conditionalFormattings: this.conditionalFormattings,
858873
};
859874

@@ -920,6 +935,7 @@ class Worksheet {
920935
tables[table.name] = t;
921936
return tables;
922937
}, {});
938+
this.pivotTables = value.pivotTables;
923939
this.conditionalFormattings = value.conditionalFormattings;
924940
}
925941
}

lib/utils/utils.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,11 @@ const utils = {
5353
},
5454
inherits,
5555
dateToExcel(d, date1904) {
56-
return 25569 + ( d.getTime() / (24 * 3600 * 1000) ) - (date1904 ? 1462 : 0);
56+
// eslint-disable-next-line no-mixed-operators
57+
return 25569 + d.getTime() / (24 * 3600 * 1000) - (date1904 ? 1462 : 0);
5758
},
5859
excelToDate(v, date1904) {
60+
// eslint-disable-next-line no-mixed-operators
5961
const millisecondSinceEpoch = Math.round((v - 25569 + (date1904 ? 1462 : 0)) * 24 * 3600 * 1000);
6062
return new Date(millisecondSinceEpoch);
6163
},
@@ -167,6 +169,37 @@ const utils = {
167169
parseBoolean(value) {
168170
return value === true || value === 'true' || value === 1 || value === '1';
169171
},
172+
173+
*range(start, stop, step = 1) {
174+
const compareOrder = step > 0 ? (a, b) => a < b : (a, b) => a > b;
175+
for (let value = start; compareOrder(value, stop); value += step) {
176+
yield value;
177+
}
178+
},
179+
180+
toSortedArray(values) {
181+
const result = Array.from(values);
182+
183+
// Note: per default, `Array.prototype.sort()` converts values
184+
// to strings when comparing. Here, if we have numbers, we use
185+
// numeric sort.
186+
if (result.every(item => Number.isFinite(item))) {
187+
const compareNumbers = (a, b) => a - b;
188+
return result.sort(compareNumbers);
189+
}
190+
191+
return result.sort();
192+
},
193+
194+
objectFromProps(props, value = null) {
195+
// *Note*: Using `reduce` as `Object.fromEntries` requires Node 12+;
196+
// ExcelJs is >=8.3.0 (as of 2023-10-08).
197+
// return Object.fromEntries(props.map(property => [property, value]));
198+
return props.reduce((result, property) => {
199+
result[property] = value;
200+
return result;
201+
}, {});
202+
},
170203
};
171204

172205
module.exports = utils;

lib/xlsx/rel-type.js

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
'use strict';
22

33
module.exports = {
4-
OfficeDocument:
5-
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument',
4+
OfficeDocument: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument',
65
Worksheet: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet',
76
CalcChain: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/calcChain',
8-
SharedStrings:
9-
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings',
7+
SharedStrings: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings',
108
Styles: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles',
119
Theme: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme',
1210
Hyperlink: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink',
1311
Image: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image',
14-
CoreProperties:
15-
'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties',
16-
ExtenderProperties:
17-
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties',
12+
CoreProperties: 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties',
13+
ExtenderProperties: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties',
1814
Comments: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments',
1915
VmlDrawing: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing',
2016
Table: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table',
17+
PivotCacheDefinition: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',
18+
PivotCacheRecords: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheRecords',
19+
PivotTable: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable',
2120
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const BaseXform = require('../base-xform');
2+
3+
class WorkbookPivotCacheXform extends BaseXform {
4+
render(xmlStream, model) {
5+
xmlStream.leafNode('pivotCache', {
6+
cacheId: model.cacheId,
7+
'r:id': model.rId,
8+
});
9+
}
10+
11+
parseOpen(node) {
12+
if (node.name === 'pivotCache') {
13+
this.model = {
14+
cacheId: node.attributes.cacheId,
15+
rId: node.attributes['r:id'],
16+
};
17+
return true;
18+
}
19+
return false;
20+
}
21+
22+
parseText() {}
23+
24+
parseClose() {
25+
return false;
26+
}
27+
}
28+
29+
module.exports = WorkbookPivotCacheXform;

0 commit comments

Comments
 (0)