diff --git a/src/TableRenderers.jsx b/src/TableRenderers.jsx index c11a858..205f2a6 100644 --- a/src/TableRenderers.jsx +++ b/src/TableRenderers.jsx @@ -1,45 +1,28 @@ import React from 'react'; import PropTypes from 'prop-types'; import {PivotData} from './Utilities'; +import memoize from 'memoize-one'; // helper function for setting row/col-span in pivotTableRenderer -const spanSize = function(arr, i, j) { - let x; - if (i !== 0) { - let asc, end; - let noDraw = true; - for ( - x = 0, end = j, asc = end >= 0; - asc ? x <= end : x >= end; - asc ? x++ : x-- - ) { - if (arr[i - 1][x] !== arr[i][x]) { - noDraw = false; - } - } - if (noDraw) { - return -1; +const sliceSame = function(arr, i1, i2, j) { + // Compare a slice of the passed in column/row attribute array up to depth j. + for (let x = 0; x <= j; x++) { + if (arr[i1][x] !== arr[i2][x]) { + return false; } } - let len = 0; - while (i + len < arr.length) { - let asc1, end1; - let stop = false; - for ( - x = 0, end1 = j, asc1 = end1 >= 0; - asc1 ? x <= end1 : x >= end1; - asc1 ? x++ : x-- - ) { - if (arr[i][x] !== arr[i + len][x]) { - stop = true; - } - } - if (stop) { - break; - } - len++; + return true; +} + +const spanSize = function(arr, i, j) { + if (i !== 0 && sliceSame(arr, i, i - 1, j)) { + return -1; } - return len; + let k = i + 1; + while (k < arr.length && sliceSame(arr, i, k, j)) { + k++; + } + return k - i; }; function redColorScaleGenerator(values) { @@ -53,253 +36,328 @@ function redColorScaleGenerator(values) { } function makeRenderer(opts = {}) { - class TableRenderer extends React.PureComponent { - render() { - const pivotData = new PivotData(this.props); + class TableRenderer extends React.Component { + getPivotSettings = memoize(props => { + // One-time extraction of pivot settings that we'll use throughout the render. + + const pivotData = new PivotData(props); const colAttrs = pivotData.props.cols; const rowAttrs = pivotData.props.rows; const rowKeys = pivotData.getRowKeys(); const colKeys = pivotData.getColKeys(); - const grandTotalAggregator = pivotData.getAggregator([], []); - const tableOptions = this.props.tableOptions; - const rowTotals = ('rowTotals' in tableOptions ? tableOptions.rowTotals : true) || colAttrs.length === 0; - const colTotals = ('colTotals' in tableOptions ? tableOptions.colTotals : true) || rowAttrs.length === 0; + const tableOptions = { + rowTotals: true, + colTotals: true, + ...this.props.tableOptions + }; + const rowTotals = tableOptions.rowTotals || colAttrs.length === 0; + const colTotals = tableOptions.colTotals || rowAttrs.length === 0; + + return { + pivotData, + colAttrs, + rowAttrs, + colKeys, + rowKeys, + rowTotals, + colTotals, + ...this.heatmapMappers( + pivotData, + this.props.tableColorScaleGenerator, + colTotals, + rowTotals, + ), + }; + }); + + heatmapMappers = (pivotData, colorScaleGenerator, colTotals, rowTotals) => { let valueCellColors = () => {}; let rowTotalColors = () => {}; let colTotalColors = () => {}; if (opts.heatmapMode) { - const colorScaleGenerator = this.props.tableColorScaleGenerator; if (colTotals) { - const rowTotalValues = colKeys.map(x => - pivotData.getAggregator([], x).value() - ); - rowTotalColors = colorScaleGenerator(rowTotalValues); + const colTotalValues = Object.values(pivotData.colTotals).map(a => a.value()); + colTotalColors = colorScaleGenerator(colTotalValues); } if (rowTotals) { - const colTotalValues = rowKeys.map(x => - pivotData.getAggregator(x, []).value() - ); - colTotalColors = colorScaleGenerator(colTotalValues); + const rowTotalValues = Object.values(pivotData.rowTotals).map(a => a.value()); + rowTotalColors = colorScaleGenerator(rowTotalValues); } if (opts.heatmapMode === 'full') { const allValues = []; - rowKeys.map(r => - colKeys.map(c => - allValues.push(pivotData.getAggregator(r, c).value()) - ) + Object.values(pivotData.tree).map(cd => + Object.values(cd).map(a => allValues.push(a.value())) ); const colorScale = colorScaleGenerator(allValues); valueCellColors = (r, c, v) => colorScale(v); } else if (opts.heatmapMode === 'row') { const rowColorScales = {}; - rowKeys.map(r => { - const rowValues = colKeys.map(x => - pivotData.getAggregator(r, x).value() - ); - rowColorScales[r] = colorScaleGenerator(rowValues); + Object.entries(pivotData.tree).map(([rk, cd]) => { + const rowValues = Object.values(cd).map(a => a.value()); + rowColorScales[rk] = colorScaleGenerator(rowValues); }); - valueCellColors = (r, c, v) => rowColorScales[r](v); + valueCellColors = (r, c, v) => rowColorScales[r.join(String.fromCharCode(0))](v); } else if (opts.heatmapMode === 'col') { const colColorScales = {}; - colKeys.map(c => { - const colValues = rowKeys.map(x => - pivotData.getAggregator(x, c).value() - ); - colColorScales[c] = colorScaleGenerator(colValues); - }); - valueCellColors = (r, c, v) => colColorScales[c](v); + const colValues = {}; + Object.values(pivotData.tree).map(cd => + Object.entries(cd).map(([ck, a]) => { + if (!(ck in colValues)) { + colValues[ck] = []; + } + colValues[ck].push(a.value()); + }) + ); + for (const k in colValues) { + colColorScales[k] = colorScaleGenerator(colValues[k]); + } + valueCellColors = (r, c, v) => colColorScales[c.join(String.fromCharCode(0))](v); } } + return {valueCellColors, rowTotalColors, colTotalColors}; + } - const getClickHandler = - this.props.tableOptions && this.props.tableOptions.clickCallback - ? (value, rowValues, colValues) => { - const filters = {}; - for (const i of Object.keys(colAttrs || {})) { - const attr = colAttrs[i]; - if (colValues[i] !== null) { - filters[attr] = colValues[i]; - } - } - for (const i of Object.keys(rowAttrs || {})) { - const attr = rowAttrs[i]; - if (rowValues[i] !== null) { - filters[attr] = rowValues[i]; - } - } - return e => - this.props.tableOptions.clickCallback( - e, - value, - filters, - pivotData - ); - } - : null; + clickHandler = (value, rowValues, colValues) => { + const colAttrs = this.props.cols; + const rowAttrs = this.props.rows; + if (this.props.tableOptions && this.props.tableOptions.clickCallback ) { + const filters = {}; + for (const i of Object.keys(colAttrs)) { + const attr = colAttrs[i]; + if (colValues[i] !== null) { + filters[attr] = colValues[i]; + } + } + for (const i of Object.keys(rowAttrs)) { + const attr = rowAttrs[i]; + if (rowValues[i] !== null) { + filters[attr] = rowValues[i]; + } + } + return e => + tableOptions.clickCallback( + e, + value, + filters, + pivotData + ); + } else { + return null; + } + } + + renderColHeaderRow = (attrName, attrIdx, pivotSettings) => { + // Render a single row in the column header at the top of the pivot table. + + const {rowAttrs, colAttrs, colKeys, rowTotals} = pivotSettings; + + const spaceCell = (attrIdx === 0 && rowAttrs.length !== 0) + ? () + : null; + + const attrNameCell = ({attrName}); + + const rowSpan = (attrIdx === colAttrs.length - 1 && rowAttrs.length !== 0) ? 2 : 1; + const attrValueCells = colKeys.map((c, i) => { + const colSpan = spanSize(colKeys, i, attrIdx); + if (colSpan !== -1) { + return ( + + {colKeys[i][attrIdx]} + + ) + } + }); + + const totalCell = (attrIdx === 0 && rowTotals) + ? ( + + Totals + + ) + : 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} = pivotSettings; return ( - - - {colAttrs.map(function(c, j) { - return ( - - {j === 0 && - rowAttrs.length !== 0 && ( - - {colKeys.map(function(colKey, i) { - const x = spanSize(colKeys, i, j); - if (x === -1) { - return null; - } - return ( - - ); - })} + + {rowAttrs.map((r, i) => ( + + ))} + + + ); + } + + renderTableRow = (rowKey, rowIdx, pivotSettings) => { + // Render a single row in the pivot table. + + const { + rowAttrs, + colAttrs, + rowKeys, + colKeys, + pivotData, + rowTotals, + valueCellColors, + rowTotalColors, + } = pivotSettings; + + const attrValueCells = rowKey.map((r, i) => { + const rowSpan = spanSize(rowKeys, rowIdx, i); + if (rowSpan > 0) { + const colSpan = (i === rowKey.length - 1 && colAttrs.length !== 0) ? 2 : 1; + return ( + + ) + } + }); + + const valueCells = colKeys.map((colKey, j) => { + const agg = pivotData.getAggregator(rowKey, colKey); + const aggValue = agg.value(); + const style = valueCellColors(rowKey, colKey, aggValue); + return ( + + ); + }); + + let totalCell = null; + if (rowTotals) { + const agg = pivotData.getAggregator(rowKey, []); + const aggValue = agg.value(); + const style = rowTotalColors(aggValue); + totalCell = ( + + ); + } + + const rowCells = [ + ...attrValueCells, + ...valueCells, + totalCell, + ]; + + return ({rowCells}); + } - {j === 0 && rowTotals && ( - - )} - - ); - })} + renderTotalsRow = (pivotSettings) => { + // Render the final totals rows that has the totals for all the columns. + + const { + rowAttrs, + colAttrs, + colKeys, + colTotalColors, + rowTotals, + pivotData + } = pivotSettings; + + const totalLabelCell = ( + + ); + + const totalValueCells = colKeys.map((colKey, j) => { + const agg = pivotData.getAggregator([], colKey); + const aggValue = agg.value(); + const style = colTotalColors([], colKey, aggValue); + return ( + + ); + }); + + let grandTotalCell = null; + if (rowTotals) { + const agg = pivotData.getAggregator([], []); + const aggValue = agg.value(); + grandTotalCell = ( + + ); + } + + const totalCells = [ + totalLabelCell, + ...totalValueCells, + grandTotalCell, + ]; + + return ({totalCells}); + } - {rowAttrs.length !== 0 && ( - - {rowAttrs.map(function(r, i) { - return ( - - ); - })} - - - )} + render() { + const pivotSettings = this.getPivotSettings(this.props); + const {colAttrs, rowAttrs, rowKeys, colTotals} = pivotSettings; + return ( +
- )} - {c} - {colKey[j]} -
+ {r} + + {colAttrs.length === 0 ? 'Totals' : null} +
+ {r} + + {agg.format(aggValue)} + + {agg.format(aggValue)} +
- Totals -
+ Totals + + {agg.format(aggValue)} + + {agg.format(aggValue)} +
- {r} - - {colAttrs.length === 0 ? 'Totals' : null} -
+ + {colAttrs.map((c, j) => this.renderColHeaderRow(c, j, pivotSettings))} + {rowAttrs.length !== 0 && this.renderRowHeaderRow(pivotSettings)} - - {rowKeys.map(function(rowKey, i) { - const totalAggregator = pivotData.getAggregator(rowKey, []); - return ( - - {rowKey.map(function(txt, j) { - const x = spanSize(rowKeys, i, j); - if (x === -1) { - return null; - } - return ( - - ); - })} - {colKeys.map(function(colKey, j) { - const aggregator = pivotData.getAggregator(rowKey, colKey); - return ( - - ); - })} - {rowTotals && ( - - )} - - ); - })} - - {colTotals && ( - - - - {colKeys.map(function(colKey, i) { - const totalAggregator = pivotData.getAggregator([], colKey); - return ( - - ); - })} - - {rowTotals && ( - - )} - - )} + {rowKeys.map((r, i) => this.renderTableRow(r, i, pivotSettings))} + {colTotals && this.renderTotalsRow(pivotSettings)}
- {txt} - - {aggregator.format(aggregator.value())} - - {totalAggregator.format(totalAggregator.value())} -
- Totals - - {totalAggregator.format(totalAggregator.value())} - - {grandTotalAggregator.format(grandTotalAggregator.value())} -
); @@ -309,7 +367,7 @@ function makeRenderer(opts = {}) { TableRenderer.defaultProps = PivotData.defaultProps; TableRenderer.propTypes = PivotData.propTypes; TableRenderer.defaultProps.tableColorScaleGenerator = redColorScaleGenerator; - TableRenderer.defaultProps.tableOptions = {rowTotals: true, colTotals: true}; + TableRenderer.defaultProps.tableOptions = {}; TableRenderer.propTypes.tableColorScaleGenerator = PropTypes.func; TableRenderer.propTypes.tableOptions = PropTypes.object; return TableRenderer; diff --git a/src/Utilities.js b/src/Utilities.js index 6d3eefb..c9dd1a2 100644 --- a/src/Utilities.js +++ b/src/Utilities.js @@ -588,16 +588,9 @@ class PivotData { } arrSort(attrs) { - let a; - const sortersArr = (() => { - const result = []; - for (a of Array.from(attrs)) { - result.push(getSort(this.props.sorters, a)); - } - return result; - })(); + const sortersArr = attrs.map(a => getSort(this.props.sorters, a)); return function(a, b) { - for (const i of Object.keys(sortersArr || {})) { + for (const i of Object.keys(sortersArr)) { const sorter = sortersArr[i]; const comparison = sorter(a[i], b[i]); if (comparison !== 0) { @@ -649,10 +642,10 @@ class PivotData { // this code is called in a tight loop const colKey = []; const rowKey = []; - for (const x of Array.from(this.props.cols)) { + for (const x of this.props.cols) { colKey.push(x in record ? record[x] : 'null'); } - for (const x of Array.from(this.props.rows)) { + for (const x of this.props.rows) { rowKey.push(x in record ? record[x] : 'null'); } const flatRowKey = rowKey.join(String.fromCharCode(0));