From e18bfdcfa61a3fc12a1bf06984fa01a892e8a723 Mon Sep 17 00:00:00 2001 From: Kamil Gabryjelski Date: Fri, 17 Dec 2021 10:30:44 +0100 Subject: [PATCH] Clean up the code --- .../src/PivotTableChart.tsx | 1 - .../src/react-pivottable/PivotTable.jsx | 24 +- .../src/react-pivottable/TableRenderers.jsx | 1515 ++++++++--------- 3 files changed, 748 insertions(+), 792 deletions(-) diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx b/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx index 4b3dd4a2af4d9..ab2d9b727ef1d 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx @@ -363,7 +363,6 @@ export default function PivotTableChart(props: PivotTableProps) { customFormatters={metricFormatters} aggregatorName={aggregateFunction} vals={vals} - rendererName="Table With Subtotal" colOrder={colOrder} rowOrder={rowOrder} sorters={sorters} diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/PivotTable.jsx b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/PivotTable.jsx index 833ce2ae169e6..bb79069d90192 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/PivotTable.jsx +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/PivotTable.jsx @@ -18,32 +18,16 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; import { PivotData } from './utilities'; -import { TableRenderers } from './TableRenderers'; +import { TableRenderer } from './TableRenderers'; class PivotTable extends React.PureComponent { render() { - const Renderer = - this.props.renderers[ - this.props.rendererName in this.props.renderers - ? this.props.rendererName - : Object.keys(this.props.renderers)[0] - ]; - return ; + return ; } } -PivotTable.propTypes = { - ...PivotData.propTypes, - rendererName: PropTypes.string, - renderers: PropTypes.objectOf(PropTypes.func), -}; - -PivotTable.defaultProps = { - ...PivotData.defaultProps, - rendererName: 'Table', - renderers: TableRenderers, -}; +PivotTable.propTypes = PivotData.propTypes; +PivotTable.defaultProps = PivotData.defaultProps; export default PivotTable; diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.jsx b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.jsx index 321bb7125cd15..fe0c2fb0d522c 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.jsx +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.jsx @@ -54,864 +54,837 @@ function displayHeaderCell( ); } -function makeRenderer(opts = {}) { - 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); - } +export class TableRenderer extends React.Component { + constructor(props) { + super(props); - 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, - !opts.subtotals - ? {} - : { - 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, - ); - }); - }); + // 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: {} }; - // 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, - }; - } + this.clickHeaderHandler = this.clickHeaderHandler.bind(this); + this.clickHandler = this.clickHandler.bind(this); + } - 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]; + 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, + [], + ); + }); } - 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]; - } + if (colTotals) { + colKeys.forEach(colKey => { + colTotalCallbacks[flatKey(colKey)] = this.clickHandler( + pivotData, + [], + colKey, + ); + }); + } + if (rowTotals && colTotals) { + grandTotalCallback = this.clickHandler(pivotData, [], []); } - return e => - this.props.tableOptions.clickCallback(e, value, filters, pivotData); } - clickHeaderHandler( + return { 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]; + 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]; } - return e => - callback( - e, - values[attrIdx], - filters, - pivotData, - isSubtotal, - isGrandTotal, - ); } + 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); + } - 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 }, - })); - } - }; + 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, + ); + } - 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; - } - }); + 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); - if (rowOrCol) { - this.setState(state => ({ - collapsedRows: { ...state.collapsedRows, ...updates }, - })); - } else { - this.setState(state => ({ - collapsedCols: { ...state.collapsedCols, ...updates }, - })); - } - }; - } + const updates = {}; + collapsed.forEach(k => { + updates[k] = true; + }); - toggleRowKey(flatRowKey) { - return e => { - e.stopPropagation(); + if (rowOrCol) { this.setState(state => ({ - collapsedRows: { - ...state.collapsedRows, - [flatRowKey]: !state.collapsedRows[flatRowKey], - }, + collapsedRows: { ...state.collapsedRows, ...updates }, })); - }; - } - - toggleColKey(flatColKey) { - return e => { - e.stopPropagation(); + } else { this.setState(state => ({ - collapsedCols: { - ...state.collapsedCols, - [flatColKey]: !state.collapsedCols[flatColKey], - }, + collapsedCols: { ...state.collapsedCols, ...updates }, })); - }; - } + } + }; + } - 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; + 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; } - 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 = - opts.subtotals && - 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; + if (rowOrCol) { + this.setState(state => ({ + collapsedRows: { ...state.collapsedRows, ...updates }, + })); + } else { + this.setState(state => ({ + collapsedCols: { ...state.collapsedCols, ...updates }, + })); } - const attrNameCell = ( - - {displayHeaderCell( - needToggle, - subArrow, - arrowClickHandle, - attrName, - namesMapping, - )} - - ); + }; + } - const attrValueCells = []; - const rowIncrSpan = rowAttrs.length !== 0 ? 1 : 0; - // Iterate through columns. Jump over duplicate values. - 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'; - } + toggleRowKey(flatRowKey) { + return e => { + e.stopPropagation(); + this.setState(state => ({ + collapsedRows: { + ...state.collapsedRows, + [flatRowKey]: !state.collapsedRows[flatRowKey], + }, + })); + }; + } - 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( - - {displayHeaderCell( - needToggle, - this.state.collapsedCols[flatColKey] - ? arrowCollapsed - : arrowExpanded, - onArrowClick, - headerCellFormattedValue, - namesMapping, - )} - , - ); - } else if (attrIdx === colKey.length) { - const rowSpan = colAttrs.length - colKey.length + rowIncrSpan; - attrValueCells.push( - - Subtotal - , - ); - } - // The next colSpan columns will have the same value anyway... - i += colSpan; + 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 = ( + + {displayHeaderCell( + needToggle, + subArrow, + arrowClickHandle, + attrName, + namesMapping, + )} + + ); + + const attrValueCells = []; + const rowIncrSpan = rowAttrs.length !== 0 ? 1 : 0; + // Iterate through columns. Jump over duplicate values. + 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 totalCell = - attrIdx === 0 && rowTotals ? ( + 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( - {`Total (${this.props.aggregatorName})`} - - ) : null; - - const cells = [spaceCell, attrNameCell, ...attrValueCells, totalCell]; - return {cells}; - } - - renderRowHeaderRow(pivotSettings) { - // Render just the attribute names of the rows (the actual attribute values - // will show up in the individual rows). - - const { - rowAttrs, - colAttrs, - rowKeys, - arrowCollapsed, - arrowExpanded, - rowSubtotalDisplay, - maxRowVisible, - pivotData, - namesMapping, - } = pivotSettings; - return ( - - {rowAttrs.map((r, i) => { - const needLabelToggle = - opts.subtotals && - 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 ( - - {displayHeaderCell( - needLabelToggle, - subArrow, - arrowClickHandle, - r, - namesMapping, - )} - - ); - })} + {displayHeaderCell( + needToggle, + this.state.collapsedCols[flatColKey] + ? arrowCollapsed + : arrowExpanded, + onArrowClick, + headerCellFormattedValue, + namesMapping, + )} + , + ); + } else if (attrIdx === colKey.length) { + const rowSpan = colAttrs.length - colKey.length + rowIncrSpan; + attrValueCells.push( - {colAttrs.length === 0 - ? `Total (${this.props.aggregatorName})` - : null} - - - ); + Subtotal + , + ); + } + // The next colSpan columns will have the same value anyway... + i += colSpan; } - renderTableRow(rowKey, rowIdx, pivotSettings) { - // Render a single row in the pivot table. + const totalCell = + attrIdx === 0 && rowTotals ? ( + + {`Total (${this.props.aggregatorName})`} + + ) : null; - 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 = - opts.subtotals && - rowSubtotalDisplay.enabled && - i !== rowAttrs.length - 1; - const onArrowClick = needRowToggle - ? this.toggleRowKey(flatRowKey) - : null; - - const headerCellFormattedValue = - dateFormatters && dateFormatters[rowAttrs[i]] - ? dateFormatters[rowAttrs[i]](r) - : r; + const cells = [spaceCell, attrNameCell, ...attrValueCells, totalCell]; + return {cells}; + } + + renderRowHeaderRow(pivotSettings) { + // Render just the attribute names of the rows (the actual attribute values + // will show up in the individual rows). + + const { + rowAttrs, + colAttrs, + rowKeys, + arrowCollapsed, + arrowExpanded, + rowSubtotalDisplay, + maxRowVisible, + pivotData, + namesMapping, + } = pivotSettings; + return ( + + {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 ( - + {displayHeaderCell( - needRowToggle, - this.state.collapsedRows[flatRowKey] - ? arrowCollapsed - : arrowExpanded, - onArrowClick, - headerCellFormattedValue, + needLabelToggle, + subArrow, + arrowClickHandle, + r, namesMapping, )} ); - } - return null; - }); + })} + + {colAttrs.length === 0 + ? `Total (${this.props.aggregatorName})` + : null} + + + ); + } - const attrValuePaddingCell = - rowKey.length < rowAttrs.length ? ( + renderTableRow(rowKey, rowIdx, pivotSettings) { + // Render a single row in the pivot table. + + 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 ( - Subtotal + {displayHeaderCell( + needRowToggle, + this.state.collapsedRows[flatRowKey] + ? arrowCollapsed + : arrowExpanded, + onArrowClick, + headerCellFormattedValue, + namesMapping, + )} - ) : 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 ( - - {agg.format(aggValue)} - - ); - }); - - let totalCell = null; - if (rowTotals) { - const agg = pivotData.getAggregator(rowKey, []); - const aggValue = agg.value(); - totalCell = ( - - {agg.format(aggValue)} - ); } + return null; + }); - const rowCells = [ - ...attrValueCells, - attrValuePaddingCell, - ...valueCells, - totalCell, - ]; - - return {rowCells}; - } - - renderTotalsRow(pivotSettings) { - // Render the final totals rows that has the totals for all the columns. - - const { - rowAttrs, - colAttrs, - visibleColKeys, - rowTotals, - pivotData, - colTotalCallbacks, - grandTotalCallback, - } = pivotSettings; - - const totalLabelCell = ( + const attrValuePaddingCell = + rowKey.length < rowAttrs.length ? ( - {`Total (${this.props.aggregatorName})`} + Subtotal - ); - - const totalValueCells = visibleColKeys.map(colKey => { - const flatColKey = flatKey(colKey); - const agg = pivotData.getAggregator([], colKey); - const aggValue = agg.value(); - - return ( - - {agg.format(aggValue)} - - ); - }); - - let grandTotalCell = null; - if (rowTotals) { - const agg = pivotData.getAggregator([], []); - const aggValue = agg.value(); - grandTotalCell = ( - - {agg.format(aggValue)} - - ); + ) : 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 totalCells = [totalLabelCell, ...totalValueCells, grandTotalCell]; + const style = agg.isSubtotal + ? { fontWeight: 'bold' } + : { backgroundColor }; return ( - - {totalCells} - + + {agg.format(aggValue)} + ); - } - - visibleKeys(keys, collapsed, numAttrs, subtotalDisplay) { - return keys.filter( - key => - // Is the key hidden by one of its parents? - !key.some((k, j) => collapsed[flatKey(key.slice(0, j))]) && - // Leaf key. - (key.length === numAttrs || - // Children hidden. Must show total. - flatKey(key) in collapsed || - // Don't hide totals. - !subtotalDisplay.hideOnExpand), + }); + + let totalCell = null; + if (rowTotals) { + const agg = pivotData.getAggregator(rowKey, []); + const aggValue = agg.value(); + totalCell = ( + + {agg.format(aggValue)} + ); } - render() { - if (this.cachedProps !== this.props) { - this.cachedProps = this.props; - this.cachedBasePivotSettings = this.getBasePivotSettings(); - } - const { - colAttrs, - rowAttrs, - rowKeys, - colKeys, - colTotals, - rowSubtotalDisplay, - colSubtotalDisplay, - } = this.cachedBasePivotSettings; - - // Need to account for exclusions to compute the effective row - // and column keys. - const visibleRowKeys = opts.subtotals - ? this.visibleKeys( - rowKeys, - this.state.collapsedRows, - rowAttrs.length, - rowSubtotalDisplay, - ) - : rowKeys; - const visibleColKeys = opts.subtotals - ? this.visibleKeys( - colKeys, - this.state.collapsedCols, - colAttrs.length, - colSubtotalDisplay, - ) - : colKeys; - - 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, - }; + const rowCells = [ + ...attrValueCells, + attrValuePaddingCell, + ...valueCells, + totalCell, + ]; + + return {rowCells}; + } + + renderTotalsRow(pivotSettings) { + // Render the final totals rows that has the totals for all the columns. + + const { + rowAttrs, + colAttrs, + visibleColKeys, + rowTotals, + pivotData, + colTotalCallbacks, + grandTotalCallback, + } = pivotSettings; + + const totalLabelCell = ( + + {`Total (${this.props.aggregatorName})`} + + ); + + const totalValueCells = visibleColKeys.map(colKey => { + const flatColKey = flatKey(colKey); + const agg = pivotData.getAggregator([], colKey); + const aggValue = agg.value(); return ( - - - - {colAttrs.map((c, j) => - this.renderColHeaderRow(c, j, pivotSettings), - )} - {rowAttrs.length !== 0 && this.renderRowHeaderRow(pivotSettings)} - - - {visibleRowKeys.map((r, i) => - this.renderTableRow(r, i, pivotSettings), - )} - {colTotals && this.renderTotalsRow(pivotSettings)} - -
-
+ + {agg.format(aggValue)} + + ); + }); + + let grandTotalCell = null; + if (rowTotals) { + const agg = pivotData.getAggregator([], []); + const aggValue = agg.value(); + grandTotalCell = ( + + {agg.format(aggValue)} + ); } + + const totalCells = [totalLabelCell, ...totalValueCells, grandTotalCell]; + + return ( + + {totalCells} + + ); + } + + visibleKeys(keys, collapsed, numAttrs, subtotalDisplay) { + return keys.filter( + key => + // Is the key hidden by one of its parents? + !key.some((k, j) => collapsed[flatKey(key.slice(0, j))]) && + // Leaf key. + (key.length === numAttrs || + // Children hidden. Must show total. + flatKey(key) in collapsed || + // Don't hide totals. + !subtotalDisplay.hideOnExpand), + ); } - TableRenderer.propTypes = { - ...PivotData.propTypes, - tableOptions: PropTypes.object, - }; - TableRenderer.defaultProps = { ...PivotData.defaultProps, tableOptions: {} }; - return TableRenderer; + render() { + if (this.cachedProps !== this.props) { + this.cachedProps = this.props; + this.cachedBasePivotSettings = this.getBasePivotSettings(); + } + const { + colAttrs, + rowAttrs, + rowKeys, + colKeys, + colTotals, + rowSubtotalDisplay, + colSubtotalDisplay, + } = this.cachedBasePivotSettings; + + // Need to account for exclusions to compute the effective row + // and column keys. + 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 ( + + + + {colAttrs.map((c, j) => + this.renderColHeaderRow(c, j, pivotSettings), + )} + {rowAttrs.length !== 0 && this.renderRowHeaderRow(pivotSettings)} + + + {visibleRowKeys.map((r, i) => + this.renderTableRow(r, i, pivotSettings), + )} + {colTotals && this.renderTotalsRow(pivotSettings)} + +
+
+ ); + } } -export const TableRenderers = { - Table: makeRenderer(), - 'Table With Subtotal': makeRenderer({ subtotals: true }), +TableRenderer.propTypes = { + ...PivotData.propTypes, + tableOptions: PropTypes.object, }; +TableRenderer.defaultProps = { ...PivotData.defaultProps, tableOptions: {} };