@protobi/exceljs
Version:
Excel Workbook Manager - Temporary fork with pivot table enhancements and bug fixes pending upstream merge
198 lines (184 loc) • 8.68 kB
JavaScript
;
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
var _require = require('../utils/utils'),
objectFromProps = _require.objectFromProps,
range = _require.range,
toSortedArray = _require.toSortedArray;
// TK(2023-10-10): turn this into a class constructor.
// IMPORTANT: Pivot tables are NOT supported with streaming API (WorkbookWriter)
//
// Pivot tables require reading ALL source data to generate the pivot cache,
// which conflicts with streaming's one-pass write model. Excel requires complete
// pivot cache data (all unique values and all data rows) at file creation time.
//
// For large datasets, use the standard (non-streaming) Workbook API with pivot tables.
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', 'count' // only 'sum' and 'count' are possible for now
// }
validate(worksheet, model);
var sourceSheet = model.sourceSheet;
var rows = model.rows,
columns = model.columns,
values = model.values;
var metric = model.metric;
// Generate sharedItems for ALL fields in the source, not just the ones used by this pivot table
// This ensures Excel can properly display any field configuration
var allHeaderNames = sourceSheet.getRow(1).values.slice(1);
var cacheFields = makeCacheFields(sourceSheet, allHeaderNames);
// 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).
var nameToIndex = cacheFields.reduce(function (result, cacheField, index) {
result[cacheField.name] = index;
return result;
}, {});
rows = rows.map(function (row) {
return nameToIndex[row];
});
columns = columns.map(function (column) {
return nameToIndex[column];
});
values = values.map(function (value) {
return nameToIndex[value];
});
// Generate unique cache ID based on the number of existing pivot tables
// Each pivot table gets its own cache ID (starting from 10)
var cacheId = String(10 + worksheet.workbook.pivotTables.length);
// form pivot table object
return {
sourceSheet: sourceSheet,
rows: rows,
columns: columns,
values: values,
metric: metric,
cacheFields: cacheFields,
// defined in <pivotTableDefinition> of xl/pivotTables/pivotTableN.xml;
// also used in xl/workbook.xml
cacheId: cacheId,
// Control whether pivot table style overrides worksheet column widths
// '0' = preserve worksheet column widths (useful for custom sizing)
// '1' = apply pivot table style width/height (default Excel behavior)
applyWidthHeightFormats: model.applyWidthHeightFormats !== undefined ? model.applyWidthHeightFormats : '1'
};
}
function validate(worksheet, model) {
// Note: Multiple pivot tables are now supported
if (model.metric && model.metric !== 'sum' && model.metric !== 'count') {
throw new Error('Only the "sum" and "count" metric is supported at this time.');
}
var headerNames = model.sourceSheet.getRow(1).values.slice(1);
var isInHeaderNames = objectFromProps(headerNames, true);
for (var _i = 0, _arr = [].concat(_toConsumableArray(model.rows), _toConsumableArray(model.columns), _toConsumableArray(model.values)); _i < _arr.length; _i++) {
var name = _arr[_i];
if (!isInHeaderNames[name]) {
throw new Error("The header name \"".concat(name, "\" was not found in ").concat(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 }
// ]
var names = worksheet.getRow(1).values;
var nameToHasSharedItems = objectFromProps(fieldNamesWithSharedItems, true);
var aggregate = function aggregate(columnIndex) {
var columnValues = worksheet.getColumn(columnIndex).values.slice(2);
// Deduplicate case-insensitively for Excel compatibility
// Excel treats pivot table values as case-insensitive, so "Apple" and "apple"
// are considered the same value. We keep the first occurrence of each case-insensitive variant.
var seen = new Map(); // lowercase -> first occurrence
var uniqueValues = [];
var _iterator = _createForOfIteratorHelper(columnValues),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var value = _step.value;
if (value === null || value === undefined) continue;
var key = typeof value === 'string' ? value.toLowerCase() : value;
if (!seen.has(key)) {
seen.set(key, value);
uniqueValues.push(value);
}
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
return toSortedArray(uniqueValues);
};
// make result
var result = [];
var _iterator2 = _createForOfIteratorHelper(range(1, names.length)),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var columnIndex = _step2.value;
var name = names[columnIndex];
var sharedItems = nameToHasSharedItems[name] ? aggregate(columnIndex) : null;
result.push({
name: name,
sharedItems: sharedItems
});
}
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
return result;
}
module.exports = {
makePivotTable: makePivotTable
};
//# sourceMappingURL=pivot-table.js.map