/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */ import React from 'react' ; import PropTypes from 'prop-types' ; import { PivotData , flatKey } from './utilities' ; import { Styles } from './Styles' ; const parseLabel = value => { if ( typeof value === 'number' || typeof value === 'string' ) { return value ; } return String ( value ) ; } ; function displayHeaderCell ( needToggle , ArrowIcon , onArrowClick , value , namesMapping ) { const name = namesMapping [ value ] || value ; return needToggle ? ( < span className = "toggle-wrapper" > < span role = "button" tabIndex = "0" className = "toggle" onClick = { onArrowClick } > { ArrowIcon } < /span> < span className = "toggle-val" > { parseLabel ( name ) } < /span> < /span>) : (parseLabel(name)); } export class TableRenderer extends React . Component { constructor ( props ) { super ( props ) ; this . state = { collapsedRows : { } , collapsedCols : { } } ; this . clickHeaderHandler = this . clickHeaderHandler . bind ( this ) ; this . clickHandler = this . clickHandler . bind ( this ) ; } getBasePivotSettings ( ) { const { props } = this ; const colAttrs = props . cols ; const rowAttrs = props . rows ; const tableOptions = { rowTotals : true , colTotals : true , ... props . tableOptions , } ; const rowTotals = tableOptions . rowTotals || colAttrs . length === 0 ; const colTotals = tableOptions . colTotals || rowAttrs . length === 0 ; const namesMapping = props . namesMapping || { } ; const subtotalOptions = { arrowCollapsed : '\u25B2' , arrowExpanded : '\u25BC' , ... props . subtotalOptions , } ; const colSubtotalDisplay = { displayOnTop : false , enabled : rowTotals , hideOnExpand : false , ... subtotalOptions . colSubtotalDisplay , } ; const rowSubtotalDisplay = { displayOnTop : false , enabled : colTotals , hideOnExpand : false , ... subtotalOptions . rowSubtotalDisplay , } ; const pivotData = new PivotData ( props , { rowEnabled : rowSubtotalDisplay . enabled , colEnabled : colSubtotalDisplay . enabled , rowPartialOnTop : rowSubtotalDisplay . displayOnTop , colPartialOnTop : colSubtotalDisplay . displayOnTop , } ) ; const rowKeys = pivotData . getRowKeys ( ) ; const colKeys = pivotData . getColKeys ( ) ; const cellCallbacks = { } ; const rowTotalCallbacks = { } ; const colTotalCallbacks = { } ; let grandTotalCallback = null ; if ( tableOptions . clickCallback ) { rowKeys . forEach ( rowKey => { const flatRowKey = flatKey ( rowKey ) ; if ( ! ( flatRowKey in cellCallbacks ) ) { cellCallbacks [ flatRowKey ] = { } ; } colKeys . forEach ( colKey => { cellCallbacks [ flatRowKey ] [ flatKey ( colKey ) ] = this . clickHandler ( pivotData , rowKey , colKey ) ; } ) ; } ) ; if ( rowTotals ) { rowKeys . forEach ( rowKey => { rowTotalCallbacks [ flatKey ( rowKey ) ] = this . clickHandler ( pivotData , rowKey , [ ] ) ; } ) ; } if ( colTotals ) { colKeys . forEach ( colKey => { colTotalCallbacks [ flatKey ( colKey ) ] = this . clickHandler ( pivotData , [ ] , colKey ) ; } ) ; } if ( rowTotals && colTotals ) { grandTotalCallback = this . clickHandler ( pivotData , [ ] , [ ] ) ; } } return { pivotData , colAttrs , rowAttrs , colKeys , rowKeys , rowTotals , colTotals , arrowCollapsed : subtotalOptions . arrowCollapsed , arrowExpanded : subtotalOptions . arrowExpanded , colSubtotalDisplay , rowSubtotalDisplay , cellCallbacks , rowTotalCallbacks , colTotalCallbacks , grandTotalCallback , namesMapping , } ; } clickHandler ( pivotData , rowValues , colValues ) { const colAttrs = this . props . cols ; const rowAttrs = this . props . rows ; const value = pivotData . getAggregator ( rowValues , colValues ) . value ( ) ; const filters = { } ; const colLimit = Math . min ( colAttrs . length , colValues . length ) ; for ( let i = 0 ; i < colLimit ; i += 1 ) { const attr = colAttrs [ i ] ; if ( colValues [ i ] !== null ) { filters [ attr ] = colValues [ i ] ; } } const rowLimit = Math . min ( rowAttrs . length , rowValues . length ) ; for ( let i = 0 ; i < rowLimit ; i += 1 ) { const attr = rowAttrs [ i ] ; if ( rowValues [ i ] !== null ) { filters [ attr ] = rowValues [ i ] ; } } return e => this . props . tableOptions . clickCallback ( e , value , filters , pivotData ) ; } clickHeaderHandler ( pivotData , values , attrs , attrIdx , callback , isSubtotal = false , isGrandTotal = false ) { const filters = { } ; for ( let i = 0 ; i <= attrIdx ; i += 1 ) { const attr = attrs [ i ] ; filters [ attr ] = values [ i ] ; } return e => callback ( e , values [ attrIdx ] , filters , pivotData , isSubtotal , isGrandTotal ) ; } collapseAttr ( rowOrCol , attrIdx , allKeys ) { return e => { e . stopPropagation ( ) ; const keyLen = attrIdx + 1 ; const collapsed = allKeys . filter ( k => k . length === keyLen ) . map ( flatKey ) ; const updates = { } ; collapsed . forEach ( k => { updates [ k ] = true ; } ) ; if ( rowOrCol ) { this . setState ( state => ( { collapsedRows : { ... state . collapsedRows , ... updates } , } ) ) ; } else { this . setState ( state => ( { collapsedCols : { ... state . collapsedCols , ... updates } , } ) ) ; } } ; } expandAttr ( rowOrCol , attrIdx , allKeys ) { return e => { e . stopPropagation ( ) ; const updates = { } ; allKeys . forEach ( k => { for ( let i = 0 ; i <= attrIdx ; i += 1 ) { updates [ flatKey ( k . slice ( 0 , i + 1 ) ) ] = false ; } } ) ; if ( rowOrCol ) { this . setState ( state => ( { collapsedRows : { ... state . collapsedRows , ... updates } , } ) ) ; } else { this . setState ( state => ( { collapsedCols : { ... state . collapsedCols , ... updates } , } ) ) ; } } ; } toggleRowKey ( flatRowKey ) { return e => { e . stopPropagation ( ) ; this . setState ( state => ( { collapsedRows : { ... state . collapsedRows , [ flatRowKey ] : ! state . collapsedRows [ flatRowKey ] , } , } ) ) ; } ; } toggleColKey ( flatColKey ) { return e => { e . stopPropagation ( ) ; this . setState ( state => ( { collapsedCols : { ... state . collapsedCols , [ flatColKey ] : ! state . collapsedCols [ flatColKey ] , } , } ) ) ; } ; } calcAttrSpans ( attrArr , numAttrs ) { const spans = [ ] ; const li = Array ( numAttrs ) . map ( ( ) => 0 ) ; let lv = Array ( numAttrs ) . map ( ( ) => null ) ; for ( let i = 0 ; i < attrArr . length ; i += 1 ) { const cv = attrArr [ i ] ; const ent = [ ] ; let depth = 0 ; const limit = Math . min ( lv . length , cv . length ) ; while ( depth < limit && lv [ depth ] === cv [ depth ] ) { ent . push ( - 1 ) ; spans [ li [ depth ] ] [ depth ] += 1 ; depth += 1 ; } while ( depth < cv . length ) { li [ depth ] = i ; ent . push ( 1 ) ; depth += 1 ; } spans . push ( ent ) ; lv = cv ; } return spans ; } renderColHeaderRow ( attrName , attrIdx , pivotSettings ) { const { rowAttrs , colAttrs , colKeys , visibleColKeys , colAttrSpans , rowTotals , arrowExpanded , arrowCollapsed , colSubtotalDisplay , maxColVisible , pivotData , namesMapping , } = pivotSettings ; const { highlightHeaderCellsOnHover , omittedHighlightHeaderGroups = [ ] , highlightedHeaderCells , dateFormatters , } = this . props . tableOptions ; const spaceCell = attrIdx === 0 && rowAttrs . length !== 0 ? ( < th key = "padding" colSpan = { rowAttrs . length } rowSpan = { colAttrs . length } aria - hidden = "true" / > ) : null ; const needToggle = colSubtotalDisplay . enabled && attrIdx !== colAttrs . length - 1 ; let arrowClickHandle = null ; let subArrow = null ; if ( needToggle ) { arrowClickHandle = attrIdx + 1 < maxColVisible ? this . collapseAttr ( false , attrIdx , colKeys ) : this . expandAttr ( false , attrIdx , colKeys ) ; subArrow = attrIdx + 1 < maxColVisible ? arrowExpanded : arrowCollapsed ; } const attrNameCell = ( < th key = "label" className = "pvtAxisLabel" > { displayHeaderCell ( needToggle , subArrow , arrowClickHandle , attrName , namesMapping ) } < /th>); const attrValueCells = [ ] ; const rowIncrSpan = rowAttrs . length !== 0 ? 1 : 0 ; let i = 0 ; while ( i < visibleColKeys . length ) { const colKey = visibleColKeys [ i ] ; const colSpan = attrIdx < colKey . length ? colAttrSpans [ i ] [ attrIdx ] : 1 ; let colLabelClass = 'pvtColLabel' ; if ( attrIdx < colKey . length ) { if ( highlightHeaderCellsOnHover && ! omittedHighlightHeaderGroups . includes ( colAttrs [ attrIdx ] ) ) { colLabelClass += ' hoverable' ; } if ( highlightedHeaderCells && Array . isArray ( highlightedHeaderCells [ colAttrs [ attrIdx ] ] ) && highlightedHeaderCells [ colAttrs [ attrIdx ] ] . includes ( colKey [ attrIdx ] ) ) { colLabelClass += ' active' ; } const rowSpan = 1 + ( attrIdx === colAttrs . length - 1 ? rowIncrSpan : 0 ) ; const flatColKey = flatKey ( colKey . slice ( 0 , attrIdx + 1 ) ) ; const onArrowClick = needToggle ? this . toggleColKey ( flatColKey ) : null ; const headerCellFormattedValue = dateFormatters && dateFormatters [ attrName ] && typeof dateFormatters [ attrName ] === 'function' ? dateFormatters [ attrName ] ( colKey [ attrIdx ] ) : colKey [ attrIdx ] ; attrValueCells . push ( < th className = { colLabelClass } key = { `colKey-${ flatColKey }` } colSpan = { colSpan } rowSpan = { rowSpan } onClick = { this . clickHeaderHandler ( pivotData , colKey , this . props . cols , attrIdx , this . props . tableOptions . clickColumnHeaderCallback ) } > { displayHeaderCell ( needToggle , this . state . collapsedCols [ flatColKey ] ? arrowCollapsed : arrowExpanded , onArrowClick , headerCellFormattedValue , namesMapping ) } < /th>); } else if ( attrIdx === colKey . length ) { const rowSpan = colAttrs . length - colKey . length + rowIncrSpan ; attrValueCells . push ( < th className = { `${ colLabelClass } pvtSubtotalLabel` } key = { `colKeyBuffer-${ flatKey ( colKey ) }` } colSpan = { colSpan } rowSpan = { rowSpan } onClick = { this . clickHeaderHandler ( pivotData , colKey , this . props . cols , attrIdx , this . props . tableOptions . clickColumnHeaderCallback , true ) } > Subtotal < /th>); } i += colSpan ; } const totalCell = attrIdx === 0 && rowTotals ? ( < th key = "total" className = "pvtTotalLabel" rowSpan = { colAttrs . length + Math . min ( rowAttrs . length , 1 ) } onClick = { this . clickHeaderHandler ( pivotData , [ ] , this . props . cols , attrIdx , this . props . tableOptions . clickColumnHeaderCallback , false , true ) } > { `Total (${ this . props . aggregatorName })` } < /th>) : null; const cells = [ spaceCell , attrNameCell , ... attrValueCells , totalCell ] ; return < tr key = { `colAttr-${ attrIdx }` } > { cells } < /tr>; } renderRowHeaderRow ( pivotSettings ) { const { rowAttrs , colAttrs , rowKeys , arrowCollapsed , arrowExpanded , rowSubtotalDisplay , maxRowVisible , pivotData , namesMapping , } = pivotSettings ; return ( < tr key = "rowHdr" > { rowAttrs . map ( ( r , i ) => { const needLabelToggle = rowSubtotalDisplay . enabled && i !== rowAttrs . length - 1 ; let arrowClickHandle = null ; let subArrow = null ; if ( needLabelToggle ) { arrowClickHandle = i + 1 < maxRowVisible ? this . collapseAttr ( true , i , rowKeys ) : this . expandAttr ( true , i , rowKeys ) ; subArrow = i + 1 < maxRowVisible ? arrowExpanded : arrowCollapsed ; } return ( < th className = "pvtAxisLabel" key = { `rowAttr-${ i }` } > { displayHeaderCell ( needLabelToggle , subArrow , arrowClickHandle , r , namesMapping ) } < /th>); } ) } < th className = "pvtTotalLabel" key = "padding" onClick = { this . clickHeaderHandler ( pivotData , [ ] , this . props . rows , 0 , this . props . tableOptions . clickRowHeaderCallback , false , true ) } > { colAttrs . length === 0 ? `Total (${ this . props . aggregatorName })` : null } < /th> < /tr>); } renderTableRow ( rowKey , rowIdx , pivotSettings ) { const { rowAttrs , colAttrs , rowAttrSpans , visibleColKeys , pivotData , rowTotals , rowSubtotalDisplay , arrowExpanded , arrowCollapsed , cellCallbacks , rowTotalCallbacks , namesMapping , } = pivotSettings ; const { highlightHeaderCellsOnHover , omittedHighlightHeaderGroups = [ ] , highlightedHeaderCells , cellColorFormatters , dateFormatters , } = this . props . tableOptions ; const flatRowKey = flatKey ( rowKey ) ; const colIncrSpan = colAttrs . length !== 0 ? 1 : 0 ; const attrValueCells = rowKey . map ( ( r , i ) => { let valueCellClassName = 'pvtRowLabel' ; if ( highlightHeaderCellsOnHover && ! omittedHighlightHeaderGroups . includes ( rowAttrs [ i ] ) ) { valueCellClassName += ' hoverable' ; } if ( highlightedHeaderCells && Array . isArray ( highlightedHeaderCells [ rowAttrs [ i ] ] ) && highlightedHeaderCells [ rowAttrs [ i ] ] . includes ( r ) ) { valueCellClassName += ' active' ; } const rowSpan = rowAttrSpans [ rowIdx ] [ i ] ; if ( rowSpan > 0 ) { const flatRowKey = flatKey ( rowKey . slice ( 0 , i + 1 ) ) ; const colSpan = 1 + ( i === rowAttrs . length - 1 ? colIncrSpan : 0 ) ; const needRowToggle = rowSubtotalDisplay . enabled && i !== rowAttrs . length - 1 ; const onArrowClick = needRowToggle ? this . toggleRowKey ( flatRowKey ) : null ; const headerCellFormattedValue = dateFormatters && dateFormatters [ rowAttrs [ i ] ] ? dateFormatters [ rowAttrs [ i ] ] ( r ) : r ; return ( < th key = { `rowKeyLabel-${ i }` } className = { valueCellClassName } rowSpan = { rowSpan } colSpan = { colSpan } onClick = { this . clickHeaderHandler ( pivotData , rowKey , this . props . rows , i , this . props . tableOptions . clickRowHeaderCallback ) } > { displayHeaderCell ( needRowToggle , this . state . collapsedRows [ flatRowKey ] ? arrowCollapsed : arrowExpanded , onArrowClick , headerCellFormattedValue , namesMapping ) } < /th>); } return null ; } ) ; const attrValuePaddingCell = rowKey . length < rowAttrs . length ? ( < th className = "pvtRowLabel pvtSubtotalLabel" key = "rowKeyBuffer" colSpan = { rowAttrs . length - rowKey . length + colIncrSpan } rowSpan = { 1 } onClick = { this . clickHeaderHandler ( pivotData , rowKey , this . props . rows , rowKey . length , this . props . tableOptions . clickRowHeaderCallback , true ) } > Subtotal < /th>) : null; const rowClickHandlers = cellCallbacks [ flatRowKey ] || { } ; const valueCells = visibleColKeys . map ( colKey => { const flatColKey = flatKey ( colKey ) ; const agg = pivotData . getAggregator ( rowKey , colKey ) ; const aggValue = agg . value ( ) ; const keys = [ ... rowKey , ... colKey ] ; let backgroundColor ; if ( cellColorFormatters ) { Object . values ( cellColorFormatters ) . forEach ( cellColorFormatter => { if ( Array . isArray ( cellColorFormatter ) ) { keys . forEach ( key => { if ( backgroundColor ) { return ; } cellColorFormatter . filter ( formatter => formatter . column === key ) . forEach ( formatter => { const formatterResult = formatter . getColorFromValue ( aggValue ) ; if ( formatterResult ) { backgroundColor = formatterResult ; } } ) ; } ) ; } } ) ; } const style = agg . isSubtotal ? { fontWeight : 'bold' } : { backgroundColor } ; return ( < td role = "gridcell" className = "pvtVal" key = { `pvtVal-${ flatColKey }` } onClick = { rowClickHandlers [ flatColKey ] } style = { style } > { agg . format ( aggValue ) } < /td>); } ) ; let totalCell = null ; if ( rowTotals ) { const agg = pivotData . getAggregator ( rowKey , [ ] ) ; const aggValue = agg . value ( ) ; totalCell = ( < td role = "gridcell" key = "total" className = "pvtTotal" onClick = { rowTotalCallbacks [ flatRowKey ] } > { agg . format ( aggValue ) } < /td>); } const rowCells = [ ... attrValueCells , attrValuePaddingCell , ... valueCells , totalCell , ] ; return < tr key = { `keyRow-${ flatRowKey }` } > { rowCells } < /tr>; } renderTotalsRow ( pivotSettings ) { const { rowAttrs , colAttrs , visibleColKeys , rowTotals , pivotData , colTotalCallbacks , grandTotalCallback , } = pivotSettings ; const totalLabelCell = ( < th key = "label" className = "pvtTotalLabel pvtRowTotalLabel" colSpan = { rowAttrs . length + Math . min ( colAttrs . length , 1 ) } onClick = { this . clickHeaderHandler ( pivotData , [ ] , this . props . rows , 0 , this . props . tableOptions . clickRowHeaderCallback , false , true ) } > { `Total (${ this . props . aggregatorName })` } < /th>); const totalValueCells = visibleColKeys . map ( colKey => { const flatColKey = flatKey ( colKey ) ; const agg = pivotData . getAggregator ( [ ] , colKey ) ; const aggValue = agg . value ( ) ; return ( < td role = "gridcell" className = "pvtTotal pvtRowTotal" key = { `total-${ flatColKey }` } onClick = { colTotalCallbacks [ flatColKey ] } style = { { padding : '5px' } } > { agg . format ( aggValue ) } < /td>); } ) ; let grandTotalCell = null ; if ( rowTotals ) { const agg = pivotData . getAggregator ( [ ] , [ ] ) ; const aggValue = agg . value ( ) ; grandTotalCell = ( < td role = "gridcell" key = "total" className = "pvtGrandTotal pvtRowTotal" onClick = { grandTotalCallback } > { agg . format ( aggValue ) } < /td>); } const totalCells = [ totalLabelCell , ... totalValueCells , grandTotalCell ] ; return ( < tr key = "total" className = "pvtRowTotals" > { totalCells } < /tr>); } visibleKeys ( keys , collapsed , numAttrs , subtotalDisplay ) { return keys . filter ( key => ! key . some ( ( k , j ) => collapsed [ flatKey ( key . slice ( 0 , j ) ) ] ) && ( key . length === numAttrs || flatKey ( key ) in collapsed || ! subtotalDisplay . hideOnExpand ) ) ; } render ( ) { if ( this . cachedProps !== this . props ) { this . cachedProps = this . props ; this . cachedBasePivotSettings = this . getBasePivotSettings ( ) ; } const { colAttrs , rowAttrs , rowKeys , colKeys , colTotals , rowSubtotalDisplay , colSubtotalDisplay , } = this . cachedBasePivotSettings ; const visibleRowKeys = this . visibleKeys ( rowKeys , this . state . collapsedRows , rowAttrs . length , rowSubtotalDisplay ) ; const visibleColKeys = this . visibleKeys ( colKeys , this . state . collapsedCols , colAttrs . length , colSubtotalDisplay ) ; const pivotSettings = { visibleRowKeys , maxRowVisible : Math . max ( ... visibleRowKeys . map ( k => k . length ) ) , visibleColKeys , maxColVisible : Math . max ( ... visibleColKeys . map ( k => k . length ) ) , rowAttrSpans : this . calcAttrSpans ( visibleRowKeys , rowAttrs . length ) , colAttrSpans : this . calcAttrSpans ( visibleColKeys , colAttrs . length ) , ... this . cachedBasePivotSettings , } ; return ( < Styles > < table className = "pvtTable" role = "grid" > < thead > { colAttrs . map ( ( c , j ) => this . renderColHeaderRow ( c , j , pivotSettings ) ) } { rowAttrs . length !== 0 && this . renderRowHeaderRow ( pivotSettings ) } < /thead> < tbody > { visibleRowKeys . map ( ( r , i ) => this . renderTableRow ( r , i , pivotSettings ) ) } { colTotals && this . renderTotalsRow ( pivotSettings ) } < /tbody> < /table> < /Styles>); } } TableRenderer . propTypes = { ... PivotData . propTypes , tableOptions : PropTypes . object , } ; TableRenderer . defaultProps = { ... PivotData . defaultProps , tableOptions : { } } ;