@jsreport/exceljs
Version:
Excel Workbook Manager - Read and Write xlsx and csv Files.
133 lines (114 loc) • 4.07 kB
JavaScript
const {objectFromProps, range, toSortedArray} = require('../utils/utils');
// TK(2023-10-10): turn this into a class constructor.
function makePivotTable(worksheet, model) {
// Example `model`:
// {
// // 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'],
// columns: ['C'],
// values: ['E'], // only 1 item possible for now
// metric: 'sum', // only 'sum' possible for now
// }
validate(worksheet, model);
const {sourceSheet} = model;
let {rows, columns, values} = model;
const cacheFields = makeCacheFields(sourceSheet, [...rows, ...columns]);
// let {rows, columns, values} use indices instead of names;
// names can then be accessed via `pivotTable.cacheFields[index].name`.
// *Note*: Using `reduce` as `Object.fromEntries` requires Node 12+;
// ExcelJS is >=8.3.0 (as of 2023-10-08).
const nameToIndex = cacheFields.reduce((result, cacheField, index) => {
result[cacheField.name] = index;
return result;
}, {});
rows = rows.map(row => nameToIndex[row]);
columns = columns.map(column => nameToIndex[column]);
values = values.map(value => nameToIndex[value]);
// form pivot table object
return {
sourceSheet,
rows,
columns,
values,
metric: 'sum',
cacheFields,
// defined in <pivotTableDefinition> of xl/pivotTables/pivotTable1.xml;
// also used in xl/workbook.xml
cacheId: '10',
};
}
function validate(worksheet, model) {
if (worksheet.workbook.pivotTables.length === 1) {
throw new Error(
'A pivot table was already added. At this time, ExcelJS supports at most one pivot table per file.'
);
}
if (model.metric && model.metric !== 'sum') {
throw new Error('Only the "sum" metric is supported at this time.');
}
const headerNames = model.sourceSheet.getRow(1).values.slice(1);
const isInHeaderNames = objectFromProps(headerNames, true);
for (const name of [...model.rows, ...model.columns, ...model.values]) {
if (!isInHeaderNames[name]) {
throw new Error(`The header name "${name}" was not found in ${model.sourceSheet.name}.`);
}
}
if (!model.rows.length) {
throw new Error('No pivot table rows specified.');
}
if (!model.columns.length) {
throw new Error('No pivot table columns specified.');
}
if (model.values.length !== 1) {
throw new Error('Exactly 1 value needs to be specified at this time.');
}
}
function makeCacheFields(worksheet, fieldNamesWithSharedItems) {
// Cache fields are used in pivot tables to reference source data.
//
// Example
// -------
// Turn
//
// `worksheet` sheet values [
// ['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]
// ];
// fieldNamesWithSharedItems = ['A', 'B', 'C'];
//
// into
//
// [
// { name: 'A', sharedItems: ['a1', 'a2', 'a3'] },
// { name: 'B', sharedItems: ['b1', 'b2'] },
// { name: 'C', sharedItems: ['c1', 'c2', 'c3'] },
// { name: 'D', sharedItems: null },
// { name: 'E', sharedItems: null }
// ]
const names = worksheet.getRow(1).values;
const nameToHasSharedItems = objectFromProps(fieldNamesWithSharedItems, true);
const aggregate = columnIndex => {
const columnValues = worksheet.getColumn(columnIndex).values.splice(2);
const columnValuesAsSet = new Set(columnValues);
return toSortedArray(columnValuesAsSet);
};
// make result
const result = [];
for (const columnIndex of range(1, names.length)) {
const name = names[columnIndex];
const sharedItems = nameToHasSharedItems[name] ? aggregate(columnIndex) : null;
result.push({name, sharedItems});
}
return result;
}
module.exports = {makePivotTable};