/** * 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 ? ( {ArrowIcon} {parseLabel(name)} ) : ( parseLabel(name) ); } export class TableRenderer extends React.Component { constructor(props) { super(props); // We need state to record which entries are collapsed and which aren't. // This is an object with flat-keys indicating if the corresponding rows // should be collapsed. this.state = { collapsedRows: {}, collapsedCols: {} }; this.clickHeaderHandler = this.clickHeaderHandler.bind(this); this.clickHandler = this.clickHandler.bind(this); } getBasePivotSettings() { // One-time extraction of pivot settings that we'll use throughout the render. 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(); // Also pre-calculate all the callbacks for cells, etc... This is nice to have to // avoid re-calculations of the call-backs on cell expansions, etc... 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, ); }); }); // Add in totals as well. 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 => { // Collapse an entire attribute. 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 => { // Expand an entire attribute. This implicitly implies expanding all of the // parents as well. It's a bit inefficient but ah well... 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) { // Given an array of attribute values (i.e. each element is another array with // the value at every level), compute the spans for every attribute value at // every level. The return value is a nested array of the same shape. It has // -1's for repeated values and the span number otherwise. const spans = []; // Index of the last new value const li = Array(numAttrs).map(() => 0); let lv = Array(numAttrs).map(() => null); for (let i = 0; i < attrArr.length; i += 1) { // Keep increasing span values as long as the last keys are the same. For // the rest, record spans of 1. Update the indices too. 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) { // Render a single row in the column header at the top of the pivot table. 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 ? (
) : 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 = (