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'],
15+ // columns: ['C'],
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 ) {
73+ throw new Error ( 'No pivot table rows specified.' ) ;
74+ }
75+
76+ if ( model . values . length < 1 ) {
77+ throw new Error ( 'Must have at least one value.' ) ;
78+ }
79+
80+ if ( model . values . length > 1 && model . columns . length > 0 ) {
81+ throw new Error (
82+ 'It is currently not possible to have multiple values when columns are specified. Please either supply an empty array for columns or a single value.'
83+ ) ;
84+ }
85+ }
86+
87+ function makeCacheFields ( worksheet , fieldNamesWithSharedItems ) {
88+ // Cache fields are used in pivot tables to reference source data.
89+ //
90+ // Example
91+ // -------
92+ // Turn
93+ //
94+ // `worksheet` sheet values [
95+ // ['A', 'B', 'C', 'D', 'E'],
96+ // ['a1', 'b1', 'c1', 4, 5],
97+ // ['a1', 'b2', 'c1', 4, 5],
98+ // ['a2', 'b1', 'c2', 14, 24],
99+ // ['a2', 'b2', 'c2', 24, 35],
100+ // ['a3', 'b1', 'c3', 34, 45],
101+ // ['a3', 'b2', 'c3', 44, 45]
102+ // ];
103+ // fieldNamesWithSharedItems = ['A', 'B', 'C'];
104+ //
105+ // into
106+ //
107+ // [
108+ // { name: 'A', sharedItems: ['a1', 'a2', 'a3'] },
109+ // { name: 'B', sharedItems: ['b1', 'b2'] },
110+ // { name: 'C', sharedItems: ['c1', 'c2', 'c3'] },
111+ // { name: 'D', sharedItems: null },
112+ // { name: 'E', sharedItems: null }
113+ // ]
114+
115+ const names = worksheet . getRow ( 1 ) . values ;
116+ const nameToHasSharedItems = objectFromProps ( fieldNamesWithSharedItems , true ) ;
117+
118+ const aggregate = columnIndex => {
119+ const columnValues = worksheet . getColumn ( columnIndex ) . values . splice ( 2 ) ;
120+ const columnValuesAsSet = new Set ( columnValues ) ;
121+ return toSortedArray ( columnValuesAsSet ) ;
122+ } ;
123+
124+ // make result
125+ const result = [ ] ;
126+ for ( const columnIndex of range ( 1 , names . length ) ) {
127+ const name = names [ columnIndex ] ;
128+ const sharedItems = nameToHasSharedItems [ name ] ? aggregate ( columnIndex ) : null ;
129+ result . push ( { name, sharedItems} ) ;
130+ }
131+ return result ;
132+ }
133+
134+ module . exports = { makePivotTable} ;
0 commit comments