@@ -64,6 +64,7 @@ interface CollapsiblePivotTableColumn extends PivotTableColumn {
6464export class SpreadsheetPivotTable {
6565 readonly columns : CollapsiblePivotTableColumn [ ] [ ] ;
6666 rows : PivotTableRow [ ] ;
67+ readonly numberOfRowGroupings : number ;
6768 readonly measures : string [ ] ;
6869 readonly fieldsType : Record < string , string | undefined > ;
6970 readonly maxIndent : number ;
@@ -87,9 +88,7 @@ export class SpreadsheetPivotTable {
8788 columns = this . removeCollapsedColumns ( columns , measures , collapsedDomains . COL ) ;
8889 }
8990 this . columns = columns . map ( ( cols ) => {
90- // offset in the pivot table
91- // starts at 1 because the first column is the row title
92- let offset = 1 ;
91+ let offset = 0 ;
9392 return cols . map ( ( col ) => {
9493 col = { ...col , offset } ;
9594 offset += col . width ;
@@ -98,6 +97,7 @@ export class SpreadsheetPivotTable {
9897 } ) ;
9998
10099 this . rows = rows . filter ( ( row ) => ! this . isParentCollapsed ( collapsedDomains . ROW , row ) ) ;
100+ this . numberOfRowGroupings = Math . max ( ...rows . map ( ( row ) => row . fields . length ) ) ;
101101 this . maxIndent = Math . max ( ...this . rows . map ( ( row ) => row . indent ) ) ;
102102 this . rowTree = lazy ( ( ) => this . buildRowsTree ( ) ) ;
103103 this . colTree = lazy ( ( ) => this . buildColumnsTree ( ) ) ;
@@ -159,13 +159,23 @@ export class SpreadsheetPivotTable {
159159
160160 private getSkippedRows ( pivotStyle : Required < PivotStyle > ) {
161161 const skippedRows : Set < number > = new Set ( ) ;
162+ const colHeadersHeight = this . getColHeadersHeight ( ) ;
162163 if ( ! pivotStyle . displayColumnHeaders ) {
163- for ( let i = 0 ; i < this . columns . length - 1 ; i ++ ) {
164+ for ( let i = 0 ; i < colHeadersHeight - 1 ; i ++ ) {
164165 skippedRows . add ( i ) ;
165166 }
166167 }
167168 if ( ! pivotStyle . displayMeasuresRow ) {
168- skippedRows . add ( this . columns . length - 1 ) ;
169+ skippedRows . add ( colHeadersHeight - 1 ) ;
170+ }
171+ // Skip sub-total rows in tabular form
172+ if ( pivotStyle . tabularForm ) {
173+ for ( let i = 0 ; i < this . rows . length ; i ++ ) {
174+ const indent = this . rows [ i ] . indent ;
175+ if ( indent !== 0 && indent !== this . maxIndent ) {
176+ skippedRows . add ( i + colHeadersHeight ) ;
177+ }
178+ }
169179 }
170180 return skippedRows ;
171181 }
@@ -176,8 +186,8 @@ export class SpreadsheetPivotTable {
176186 const { displayTotals } = pivotStyle ;
177187 const numberOfDataRows = this . rows . length ;
178188 const numberOfDataColumns = this . getNumberOfDataColumns ( ) ;
179- let pivotHeight = this . columns . length + numberOfDataRows ;
180- let pivotWidth = 1 /*(row headers)*/ + numberOfDataColumns ;
189+ let pivotHeight = numberOfDataRows + this . getColHeadersHeight ( ) ;
190+ let pivotWidth = numberOfDataColumns + this . getRowHeadersWidth ( pivotStyle ) ;
181191 if ( ! displayTotals && numberOfDataRows !== 1 ) {
182192 pivotHeight -= 1 ;
183193 }
@@ -192,7 +202,10 @@ export class SpreadsheetPivotTable {
192202 if ( skippedRows . has ( row ) ) {
193203 continue ;
194204 }
195- domainArray [ col ] . push ( this . getPivotCell ( col , row , displayTotals ) ) ;
205+ const cell = pivotStyle . tabularForm
206+ ? this . getTabularFormPivotCell ( col , row , pivotStyle )
207+ : this . getPivotCell ( col , row , pivotStyle ) ;
208+ domainArray [ col ] . push ( cell ) ;
196209 }
197210 }
198211 this . pivotCells [ key ] = domainArray ;
@@ -212,38 +225,72 @@ export class SpreadsheetPivotTable {
212225 return this . rows [ index ] . indent !== this . maxIndent ;
213226 }
214227
215- private getPivotCell ( col : number , row : number , includeTotal = true ) : PivotTableCell {
216- const colHeadersHeight = this . columns . length ;
217- if ( col > 0 && row === colHeadersHeight - 1 ) {
218- const domain = this . getColHeaderDomain ( col , row ) ;
228+ private getPivotCell ( col : number , row : number , pivotStyle : Required < PivotStyle > ) : PivotTableCell {
229+ const colHeadersHeight = this . getColHeadersHeight ( ) ;
230+ const rowHeadersWidth = this . getRowHeadersWidth ( pivotStyle ) ;
231+
232+ const isColHeader = row < colHeadersHeight - 1 && col >= rowHeadersWidth ;
233+ const isMeasureHeader = row === colHeadersHeight - 1 && col >= rowHeadersWidth ;
234+ const isRowHeader = row > colHeadersHeight - 1 && col < rowHeadersWidth ;
235+ const isPivotValue = row > colHeadersHeight - 1 && col >= rowHeadersWidth ;
236+
237+ if ( isMeasureHeader ) {
238+ const colIndex = col - rowHeadersWidth ;
239+ const domain = this . getColHeaderDomain ( colIndex , row ) ;
219240 if ( ! domain ) {
220241 return EMPTY_PIVOT_CELL ;
221242 }
222243 const measure = domain . at ( - 1 ) ?. value ?. toString ( ) || "" ;
223244 return { type : "MEASURE_HEADER" , domain : domain . slice ( 0 , - 1 ) , measure } ;
224- } else if ( row <= colHeadersHeight - 1 ) {
225- const domain = this . getColHeaderDomain ( col , row ) ;
245+ } else if ( isColHeader ) {
246+ const colIndex = col - rowHeadersWidth ;
247+ const domain = this . getColHeaderDomain ( colIndex , row ) ;
226248 return domain ? { type : "HEADER" , domain, dimension : "COL" } : EMPTY_PIVOT_CELL ;
227- } else if ( col === 0 ) {
249+ } else if ( isRowHeader ) {
228250 const rowIndex = row - colHeadersHeight ;
229251 const domain = this . getDomain ( this . rows [ rowIndex ] ) ;
230252 return { type : "HEADER" , domain, dimension : "ROW" } ;
231- } else {
253+ } else if ( isPivotValue ) {
232254 const rowIndex = row - colHeadersHeight ;
233- if ( ! includeTotal && this . isTotalRow ( rowIndex ) ) {
255+ const colIndex = col - rowHeadersWidth ;
256+ if ( ! pivotStyle . displayTotals && this . isTotalRow ( rowIndex ) ) {
234257 return EMPTY_PIVOT_CELL ;
235258 }
236- const domain = [ ...this . getDomain ( this . rows [ rowIndex ] ) , ...this . getColDomain ( col ) ] ;
237- const measure = this . getColMeasure ( col ) ;
259+ const domain = [ ...this . getDomain ( this . rows [ rowIndex ] ) , ...this . getColDomain ( colIndex ) ] ;
260+ const measure = this . getColMeasure ( colIndex ) ;
238261 return { type : "VALUE" , domain, measure } ;
239262 }
263+
264+ return EMPTY_PIVOT_CELL ;
240265 }
241266
242- private getColHeaderDomain ( col : number , row : number ) {
243- if ( col === 0 ) {
244- return undefined ;
267+ private getTabularFormPivotCell (
268+ col : number ,
269+ row : number ,
270+ pivotStyle : Required < PivotStyle >
271+ ) : PivotTableCell {
272+ const colHeadersHeight = this . getColHeadersHeight ( ) ;
273+ const rowHeadersWidth = this . getRowHeadersWidth ( pivotStyle ) ;
274+
275+ const isRowHeader = row > colHeadersHeight - 1 && col < rowHeadersWidth ;
276+ const isRowGroupName = row === colHeadersHeight - 1 && col < rowHeadersWidth ;
277+
278+ if ( isRowHeader ) {
279+ const rowIndex = row - colHeadersHeight ;
280+ const domain = this . getDomain ( this . rows [ rowIndex ] ) . slice ( 0 , col + 1 ) ;
281+ if ( domain . length === 0 && col !== 0 ) {
282+ return EMPTY_PIVOT_CELL ;
283+ }
284+ return { type : "HEADER" , domain, dimension : "ROW" } ;
285+ } else if ( isRowGroupName ) {
286+ return { type : "ROW_GROUP_NAME" , groupByIndex : col } ;
245287 }
246- const pivotCol = this . columns [ row ] . find ( ( pivotCol ) => pivotCol . offset === col ) ;
288+
289+ return this . getPivotCell ( col , row , pivotStyle ) ;
290+ }
291+
292+ private getColHeaderDomain ( colIndex : number , row : number ) {
293+ const pivotCol = this . columns [ row ] . find ( ( pivotCol ) => pivotCol . offset === colIndex ) ;
247294 if ( ! pivotCol || pivotCol . collapsedHeader ) {
248295 return undefined ;
249296 }
@@ -273,20 +320,28 @@ export class SpreadsheetPivotTable {
273320 } ) ;
274321 }
275322
276- private getColDomain ( col : number ) {
277- const domain = this . getColHeaderDomain ( col , this . columns . length - 1 ) ;
323+ private getColDomain ( colIndex : number ) {
324+ const domain = this . getColHeaderDomain ( colIndex , this . getColHeadersHeight ( ) - 1 ) ;
278325 return domain ? domain . slice ( 0 , - 1 ) : [ ] ; // slice: remove measure and value
279326 }
280327
281- private getColMeasure ( col : number ) {
282- const domain = this . getColHeaderDomain ( col , this . columns . length - 1 ) ;
328+ private getColMeasure ( colIndex : number ) {
329+ const domain = this . getColHeaderDomain ( colIndex , this . getColHeadersHeight ( ) - 1 ) ;
283330 const measure = domain ?. at ( - 1 ) ?. value ;
284331 if ( measure === undefined || measure === null ) {
285332 throw new Error ( "Measure is missing" ) ;
286333 }
287334 return measure . toString ( ) ;
288335 }
289336
337+ getRowHeadersWidth ( pivotStyle : Required < PivotStyle > ) {
338+ return pivotStyle . tabularForm ? this . numberOfRowGroupings : 1 ;
339+ }
340+
341+ private getColHeadersHeight ( ) {
342+ return this . columns . length ;
343+ }
344+
290345 buildRowsTree ( ) : DimensionTree {
291346 const tree : DimensionTree = [ ] ;
292347 let depth = 0 ;
@@ -406,7 +461,7 @@ export class SpreadsheetPivotTable {
406461 }
407462
408463 getColumnDomainsAtDepth ( depth : number ) {
409- if ( depth < 0 || depth >= this . columns . length - 1 ) {
464+ if ( depth < 0 || depth >= this . getColHeadersHeight ( ) - 1 ) {
410465 return [ ] ;
411466 }
412467 return this . columns [ depth ] . map ( ( col ) => this . getDomain ( col ) ) . filter ( ( d ) => d . length ) ;
0 commit comments