From dbc8168d3adee08c95ed3a2c314ef7cf03ce3c5b Mon Sep 17 00:00:00 2001 From: Prakhar Goel Date: Thu, 6 Jun 2019 00:11:11 +0000 Subject: [PATCH 01/14] Added in support for the rowTotal and colTotal table options. --- src/TableRenderers.jsx | 112 +++++++++++++++++++++++------------------ 1 file changed, 62 insertions(+), 50 deletions(-) diff --git a/src/TableRenderers.jsx b/src/TableRenderers.jsx index 849894d..c11a858 100644 --- a/src/TableRenderers.jsx +++ b/src/TableRenderers.jsx @@ -61,21 +61,27 @@ function makeRenderer(opts = {}) { 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; let valueCellColors = () => {}; let rowTotalColors = () => {}; let colTotalColors = () => {}; if (opts.heatmapMode) { const colorScaleGenerator = this.props.tableColorScaleGenerator; - const rowTotalValues = colKeys.map(x => - pivotData.getAggregator([], x).value() - ); - rowTotalColors = colorScaleGenerator(rowTotalValues); - const colTotalValues = rowKeys.map(x => - pivotData.getAggregator(x, []).value() - ); - colTotalColors = colorScaleGenerator(colTotalValues); - + if (colTotals) { + const rowTotalValues = colKeys.map(x => + pivotData.getAggregator([], x).value() + ); + rowTotalColors = colorScaleGenerator(rowTotalValues); + } + if (rowTotals) { + const colTotalValues = rowKeys.map(x => + pivotData.getAggregator(x, []).value() + ); + colTotalColors = colorScaleGenerator(colTotalValues); + } if (opts.heatmapMode === 'full') { const allValues = []; rowKeys.map(r => @@ -164,7 +170,7 @@ function makeRenderer(opts = {}) { ); })} - {j === 0 && ( + {j === 0 && rowTotals && ( ); })} - - {totalAggregator.format(totalAggregator.value())} - + {rowTotals && ( + + {totalAggregator.format(totalAggregator.value())} + + )} ); })} - - - Totals - - - {colKeys.map(function(colKey, i) { - const totalAggregator = pivotData.getAggregator([], colKey); - return ( + {colTotals && ( + + + Totals + + + {colKeys.map(function(colKey, i) { + const totalAggregator = pivotData.getAggregator([], colKey); + return ( + + {totalAggregator.format(totalAggregator.value())} + + ); + })} + + {rowTotals && ( - {totalAggregator.format(totalAggregator.value())} + {grandTotalAggregator.format(grandTotalAggregator.value())} - ); - })} - - - {grandTotalAggregator.format(grandTotalAggregator.value())} - - + )} + + )} ); @@ -297,7 +309,7 @@ function makeRenderer(opts = {}) { TableRenderer.defaultProps = PivotData.defaultProps; TableRenderer.propTypes = PivotData.propTypes; TableRenderer.defaultProps.tableColorScaleGenerator = redColorScaleGenerator; - TableRenderer.defaultProps.tableOptions = {}; + TableRenderer.defaultProps.tableOptions = {rowTotals: true, colTotals: true}; TableRenderer.propTypes.tableColorScaleGenerator = PropTypes.func; TableRenderer.propTypes.tableOptions = PropTypes.object; return TableRenderer; From f067c1d7f4e35c9bd78764ac91b2ff6c1ec96d51 Mon Sep 17 00:00:00 2001 From: Prakhar Goel Date: Fri, 7 Jun 2019 22:00:18 +0000 Subject: [PATCH 02/14] Refactored the table renderer code to make it easier to understand and modify. Made minor updates to Utilities as well. --- src/TableRenderers.jsx | 558 +++++++++++++++++++++++------------------ src/Utilities.js | 15 +- 2 files changed, 312 insertions(+), 261 deletions(-) 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)); From 817b72073806c5f3b806776e0054c9d3b757d5da Mon Sep 17 00:00:00 2001 From: Prakhar Goel Date: Fri, 7 Jun 2019 22:59:47 +0000 Subject: [PATCH 03/14] Some more cleanup. --- src/TableRenderers.jsx | 96 +++++++++++++++++++++++++----------------- src/Utilities.js | 13 ++++-- 2 files changed, 66 insertions(+), 43 deletions(-) diff --git a/src/TableRenderers.jsx b/src/TableRenderers.jsx index 205f2a6..eb7b9fb 100644 --- a/src/TableRenderers.jsx +++ b/src/TableRenderers.jsx @@ -3,28 +3,6 @@ import PropTypes from 'prop-types'; import {PivotData} from './Utilities'; import memoize from 'memoize-one'; -// helper function for setting row/col-span in pivotTableRenderer -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; - } - } - return true; -} - -const spanSize = function(arr, i, j) { - if (i !== 0 && sliceSame(arr, i, i - 1, j)) { - return -1; - } - let k = i + 1; - while (k < arr.length && sliceSame(arr, i, k, j)) { - k++; - } - return k - i; -}; - function redColorScaleGenerator(values) { const min = Math.min.apply(Math, values); const max = Math.max.apply(Math, values); @@ -60,6 +38,8 @@ function makeRenderer(opts = {}) { rowAttrs, colKeys, rowKeys, + colAttrSpans: this.calcAttrSpans(colKeys), + rowAttrSpans: this.calcAttrSpans(rowKeys), rowTotals, colTotals, ...this.heatmapMappers( @@ -71,6 +51,41 @@ function makeRenderer(opts = {}) { }; }); + calcAttrSpans = (attrArr) => { + // 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. + + if (attrArr.length === 0) { + return [] + } + + const spans = []; + const li = attrArr[0].map(() => 0); // Index of the last new value + let lv = attrArr[0].map(() => null); + for(let i = 0;i < attrArr.length;i++) { + // Keep increasing span values as long as the last keys are the same. For + // the rest, record spans of 1. Update the indices too. + let cv = attrArr[i]; + let ent = []; + let depth = 0; + while (lv[depth] === cv[depth]) { + ent.push(-1); + spans[li[depth]][depth]++; + depth++; + } + while (depth < cv.length) { + li[depth] = i; + ent.push(1); + depth++; + } + spans.push(ent); + lv = cv; + } + return spans; + } + heatmapMappers = (pivotData, colorScaleGenerator, colTotals, rowTotals) => { let valueCellColors = () => {}; let rowTotalColors = () => {}; @@ -150,7 +165,7 @@ function makeRenderer(opts = {}) { 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 {rowAttrs, colAttrs, colKeys, colAttrSpans, rowTotals} = pivotSettings; const spaceCell = (attrIdx === 0 && rowAttrs.length !== 0) ? () @@ -158,22 +173,24 @@ function makeRenderer(opts = {}) { const attrNameCell = ({attrName}); + const attrValueCells = []; 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]} - - ) - } - }); + // Iterate through columns. Jump over duplicate values. + let i = 0; + while (i < colKeys.length) { + const colSpan = colAttrSpans[i][attrIdx]; + attrValueCells.push( + + {colKeys[i][attrIdx]} + + ) + i = i + colSpan; // The next colSpan columns will have the same value anyway... + }; const totalCell = (attrIdx === 0 && rowTotals) ? ( @@ -221,6 +238,7 @@ function makeRenderer(opts = {}) { rowAttrs, colAttrs, rowKeys, + rowAttrSpans, colKeys, pivotData, rowTotals, @@ -229,7 +247,7 @@ function makeRenderer(opts = {}) { } = pivotSettings; const attrValueCells = rowKey.map((r, i) => { - const rowSpan = spanSize(rowKeys, rowIdx, i); + const rowSpan = rowAttrSpans[rowIdx][i]; if (rowSpan > 0) { const colSpan = (i === rowKey.length - 1 && colAttrs.length !== 0) ? 2 : 1; return ( diff --git a/src/Utilities.js b/src/Utilities.js index c9dd1a2..7bbf0ad 100644 --- a/src/Utilities.js +++ b/src/Utilities.js @@ -522,6 +522,10 @@ const derivers = { }, }; +// Given an array of attribute values, convert to a key that +// can be used in objects. +const flatKey = (attrVals) => attrVals.join(String.fromCharCode(0)) + /* Data Model class */ @@ -648,8 +652,8 @@ class PivotData { for (const x of this.props.rows) { rowKey.push(x in record ? record[x] : 'null'); } - const flatRowKey = rowKey.join(String.fromCharCode(0)); - const flatColKey = colKey.join(String.fromCharCode(0)); + const flatRowKey = flatKey(rowKey); + const flatColKey = flatKey(colKey); this.allTotal.push(record); @@ -686,8 +690,8 @@ class PivotData { getAggregator(rowKey, colKey) { let agg; - const flatRowKey = rowKey.join(String.fromCharCode(0)); - const flatColKey = colKey.join(String.fromCharCode(0)); + const flatRowKey = flatKey(rowKey); + const flatColKey = flatKey(colKey); if (rowKey.length === 0 && colKey.length === 0) { agg = this.allTotal; } else if (rowKey.length === 0) { @@ -801,5 +805,6 @@ export { numberFormat, getSort, sortAs, + flatKey, PivotData, }; From f097b1cbf464d6e93e8c2c140043f2c834f795c8 Mon Sep 17 00:00:00 2001 From: Prakhar Goel Date: Fri, 7 Jun 2019 23:09:13 +0000 Subject: [PATCH 04/14] And even more minor cleanup. --- src/TableRenderers.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/TableRenderers.jsx b/src/TableRenderers.jsx index eb7b9fb..c7907eb 100644 --- a/src/TableRenderers.jsx +++ b/src/TableRenderers.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import {PivotData} from './Utilities'; +import {PivotData, flatKey} from './Utilities'; import memoize from 'memoize-one'; function redColorScaleGenerator(values) { @@ -112,7 +112,7 @@ function makeRenderer(opts = {}) { const rowValues = Object.values(cd).map(a => a.value()); rowColorScales[rk] = colorScaleGenerator(rowValues); }); - valueCellColors = (r, c, v) => rowColorScales[r.join(String.fromCharCode(0))](v); + valueCellColors = (r, c, v) => rowColorScales[flatKey(r)](v); } else if (opts.heatmapMode === 'col') { const colColorScales = {}; const colValues = {}; @@ -127,7 +127,7 @@ function makeRenderer(opts = {}) { for (const k in colValues) { colColorScales[k] = colorScaleGenerator(colValues[k]); } - valueCellColors = (r, c, v) => colColorScales[c.join(String.fromCharCode(0))](v); + valueCellColors = (r, c, v) => colColorScales[flatKey(c)](v); } } return {valueCellColors, rowTotalColors, colTotalColors}; From 391be642ff8f355faec4b60a0091ce380e319711 Mon Sep 17 00:00:00 2001 From: Prakhar Goel Date: Sat, 8 Jun 2019 02:58:23 +0000 Subject: [PATCH 05/14] Added the ability to compute and show subtotals. --- src/TableRenderers.jsx | 309 +++++++++++++++++++++++++++++++++-------- src/Utilities.js | 56 +++++--- 2 files changed, 284 insertions(+), 81 deletions(-) diff --git a/src/TableRenderers.jsx b/src/TableRenderers.jsx index c7907eb..5722390 100644 --- a/src/TableRenderers.jsx +++ b/src/TableRenderers.jsx @@ -15,14 +15,20 @@ function redColorScaleGenerator(values) { function makeRenderer(opts = {}) { class TableRenderer extends React.Component { - getPivotSettings = memoize(props => { + 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: {}}; + } + + getBasePivotSettings = 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 colAttrs = this.props.cols; + const rowAttrs = this.props.rows; const tableOptions = { rowTotals: true, @@ -32,16 +38,48 @@ function makeRenderer(opts = {}) { const rowTotals = tableOptions.rowTotals || colAttrs.length === 0; const colTotals = tableOptions.colTotals || rowAttrs.length === 0; + const subtotalOptions = { + arrowCollapsed: "\u25B6", + arrowExpanded: "\u25E2", + ...this.props.subtotalOptions + }; + const colSubtotalDisplay = { + displayOnTop: true, + enabled: colTotals, // by default enable if col totals are enabled. + hideOnExpand: false, + ...subtotalOptions.colSubtotalDisplay + }; + const rowSubtotalDisplay = { + displayOnTop: false, + enabled: rowTotals, // by default enable if row totals are enabled. + 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(); + return { pivotData, colAttrs, rowAttrs, colKeys, rowKeys, - colAttrSpans: this.calcAttrSpans(colKeys), - rowAttrSpans: this.calcAttrSpans(rowKeys), rowTotals, colTotals, + arrowCollapsed: subtotalOptions.arrowCollapsed, + arrowExpanded: subtotalOptions.arrowExpanded, + colSubtotalDisplay, + rowSubtotalDisplay, ...this.heatmapMappers( pivotData, this.props.tableColorScaleGenerator, @@ -50,27 +88,53 @@ function makeRenderer(opts = {}) { ), }; }); + + toggleAttr = (rowOrCol, attrIdx, allKeys) => () => { + // Toggle an entire attribute. This only collapses the entire + // attribute. Important to keep things snappy. - calcAttrSpans = (attrArr) => { + 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}})); + } + } + + toggleRowKey = flatRowKey => () => { + this.setState(state => ( + {collapsedRows: {...state.collapsedRows, [flatRowKey]: !state.collapsedRows[flatRowKey]}} + )) + } + + toggleColKey = flatColKey => () => { + 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. - if (attrArr.length === 0) { - return [] - } - const spans = []; - const li = attrArr[0].map(() => 0); // Index of the last new value - let lv = attrArr[0].map(() => null); + const li = Array(numAttrs).map(() => 0); // Index of the last new value + let lv = Array(numAttrs).map(() => null); for(let i = 0;i < attrArr.length;i++) { // Keep increasing span values as long as the last keys are the same. For // the rest, record spans of 1. Update the indices too. let cv = attrArr[i]; let ent = []; let depth = 0; - while (lv[depth] === cv[depth]) { + const limit = Math.min(lv.length, cv.length); + while (depth < limit && lv[depth] === cv[depth]) { ent.push(-1); spans[li[depth]][depth]++; depth++; @@ -136,15 +200,17 @@ function makeRenderer(opts = {}) { clickHandler = (value, rowValues, colValues) => { const colAttrs = this.props.cols; const rowAttrs = this.props.rows; - if (this.props.tableOptions && this.props.tableOptions.clickCallback ) { + if (this.props.tableOptions && this.props.tableOptions.clickCallback) { const filters = {}; - for (const i of Object.keys(colAttrs)) { + const colLimit = Math.min(colAttrs.length, colValues.length); + for (let i = 0; i < colLimit;i++) { const attr = colAttrs[i]; if (colValues[i] !== null) { filters[attr] = colValues[i]; } } - for (const i of Object.keys(rowAttrs)) { + const rowLimit = Math.min(rowAttrs.length, rowValues.length); + for (let i = 0; i < rowLimit;i++) { const attr = rowAttrs[i]; if (rowValues[i] !== null) { filters[attr] = rowValues[i]; @@ -165,36 +231,74 @@ function makeRenderer(opts = {}) { renderColHeaderRow = (attrName, attrIdx, pivotSettings) => { // Render a single row in the column header at the top of the pivot table. - const {rowAttrs, colAttrs, colKeys, colAttrSpans, rowTotals} = pivotSettings; + const { + rowAttrs, + colAttrs, + colKeys, + visibleColKeys, + colAttrSpans, + rowTotals, + arrowExpanded, + arrowCollapsed, + } = pivotSettings; const spaceCell = (attrIdx === 0 && rowAttrs.length !== 0) - ? () + ? () : null; - const attrNameCell = ({attrName}); + const needLabelToggle = opts.subtotals && attrIdx !== colAttrs.length - 1; + const attrNameCell = ( + + {needLabelToggle ? arrowExpanded + ' ' : null} {attrName} + + ); const attrValueCells = []; - const rowSpan = (attrIdx === colAttrs.length - 1 && rowAttrs.length !== 0) ? 2 : 1; + const rowIncrSpan = (rowAttrs.length !== 0) ? 1 : 0; // Iterate through columns. Jump over duplicate values. let i = 0; - while (i < colKeys.length) { - const colSpan = colAttrSpans[i][attrIdx]; - attrValueCells.push( - - {colKeys[i][attrIdx]} - - ) + while (i < visibleColKeys.length) { + const colKey = visibleColKeys[i] + const colSpan = (attrIdx < colKey.length) ? colAttrSpans[i][attrIdx] : 1; + if (attrIdx < colKey.length) { + const rowSpan = 1 + ((attrIdx === colAttrs.length - 1) ? rowIncrSpan : 0); + const flatColKey = flatKey(colKey.slice(0, attrIdx + 1)); + const needColToggle = opts.subtotals && attrIdx !== colAttrs.length - 1; + const onClick = needColToggle ? this.toggleColKey(flatColKey) : null; + attrValueCells.push( + + {needColToggle ? (this.state.collapsedCols[flatColKey] ? arrowCollapsed : arrowExpanded) + ' ' : null} + {colKey[attrIdx]} + + ) + } else if (attrIdx === colKey.length) { + const rowSpan = colAttrs.length - colKey.length + rowIncrSpan; + attrValueCells.push( + + ) + } i = i + colSpan; // The next colSpan columns will have the same value anyway... }; const totalCell = (attrIdx === 0 && rowTotals) ? ( @@ -209,22 +313,29 @@ function makeRenderer(opts = {}) { ...attrValueCells, totalCell, ]; - return {cells}; + 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; + const {rowAttrs, colAttrs, rowKeys, arrowExpanded} = pivotSettings; return ( - - {rowAttrs.map((r, i) => ( - - {r} - - ))} - + + {rowAttrs.map((r, i) => { + const needLabelToggle = opts.subtotals && i !== rowAttrs.length - 1; + return ( + + {needLabelToggle ? arrowExpanded + ' ': null} {r} + + ); + })} + {colAttrs.length === 0 ? 'Totals' : null} @@ -237,40 +348,59 @@ function makeRenderer(opts = {}) { const { rowAttrs, colAttrs, - rowKeys, + visibleRowKeys, rowAttrSpans, - colKeys, + visibleColKeys, pivotData, rowTotals, valueCellColors, rowTotalColors, + arrowExpanded, + arrowCollapsed, } = pivotSettings; + const colIncrSpan = (colAttrs.length !== 0) ? 1 : 0 const attrValueCells = rowKey.map((r, i) => { const rowSpan = rowAttrSpans[rowIdx][i]; if (rowSpan > 0) { - const colSpan = (i === rowKey.length - 1 && colAttrs.length !== 0) ? 2 : 1; + const flatRowKey = flatKey(rowKey.slice(0, i + 1)); + const colSpan = 1 + ((i === rowAttrs.length - 1) ? colIncrSpan : 0); + const needRowToggle = opts.subtotals && i !== rowAttrs.length - 1 + const onClick = needRowToggle ? this.toggleRowKey(flatRowKey) : null; return ( + {needRowToggle ? (this.state.collapsedRows[flatRowKey] ? arrowCollapsed : arrowExpanded) + ' ': null} {r} ) } }); + + const attrValuePaddingCell = (rowKey.length < rowAttrs.length) + ? ( + + ) + : null; - const valueCells = colKeys.map((colKey, j) => { + const valueCells = visibleColKeys.map((colKey, j) => { const agg = pivotData.getAggregator(rowKey, colKey); const aggValue = agg.value(); const style = valueCellColors(rowKey, colKey, aggValue); return ( @@ -286,6 +416,7 @@ function makeRenderer(opts = {}) { const style = rowTotalColors(aggValue); totalCell = ( {rowCells}); + + return ({rowCells}); } renderTotalsRow = (pivotSettings) => { @@ -310,7 +442,7 @@ function makeRenderer(opts = {}) { const { rowAttrs, colAttrs, - colKeys, + visibleColKeys, colTotalColors, rowTotals, pivotData @@ -318,6 +450,7 @@ function makeRenderer(opts = {}) { const totalLabelCell = ( @@ -325,14 +458,14 @@ function makeRenderer(opts = {}) { ); - const totalValueCells = colKeys.map((colKey, j) => { + const totalValueCells = visibleColKeys.map((colKey, j) => { const agg = pivotData.getAggregator([], colKey); const aggValue = agg.value(); const style = colTotalColors([], colKey, aggValue); return ( @@ -347,6 +480,7 @@ function makeRenderer(opts = {}) { const aggValue = agg.value(); grandTotalCell = ( @@ -360,13 +494,62 @@ function makeRenderer(opts = {}) { ...totalValueCells, grandTotalCell, ]; - - return ({totalCells}); + + return ({totalCells}); } + visibleKeys = (keys, collapsed, numAttrs, subtotalDisplay) => keys.filter( + key => ( + // Is the key hidden by one of its parents? + !key.slice(0, key.length - 1).some( + (k, j) => collapsed[flatKey(key.slice(0, j + 1))] + ) + && ( + key.length == numAttrs // Leaf key. + || flatKey(key) in collapsed // Children hidden. Must show total. + || !subtotalDisplay.hideOnExpand // Don't hide totals. + ) + ) + ) + render() { - const pivotSettings = this.getPivotSettings(this.props); - const {colAttrs, rowAttrs, rowKeys, colTotals} = pivotSettings; + const basePivotSettings = this.getBasePivotSettings(this.props); + const { + colAttrs, + rowAttrs, + rowKeys, + colKeys, + colTotals, + rowSubtotalDisplay, + colSubtotalDisplay, + } = basePivotSettings; + + // 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, + visibleColKeys, + rowAttrSpans: this.calcAttrSpans(visibleRowKeys, rowAttrs.length), + colAttrSpans: this.calcAttrSpans(visibleColKeys, colAttrs.length), + ...basePivotSettings, + }; + return ( @@ -374,7 +557,7 @@ function makeRenderer(opts = {}) { {rowAttrs.length !== 0 && this.renderRowHeaderRow(pivotSettings)} - {rowKeys.map((r, i) => this.renderTableRow(r, i, pivotSettings))} + {visibleRowKeys.map((r, i) => this.renderTableRow(r, i, pivotSettings))} {colTotals && this.renderTotalsRow(pivotSettings)}
@@ -435,9 +618,13 @@ TSVExportRenderer.defaultProps = PivotData.defaultProps; TSVExportRenderer.propTypes = PivotData.propTypes; export default { - Table: makeRenderer(), + 'Table': makeRenderer(), 'Table Heatmap': makeRenderer({heatmapMode: 'full'}), 'Table Col Heatmap': makeRenderer({heatmapMode: 'col'}), 'Table Row Heatmap': makeRenderer({heatmapMode: 'row'}), + 'Table With Subtotal': makeRenderer({subtotals: true}), + 'Table With Subtotal Heatmap': makeRenderer({heatmapMode: 'full', subtotals: true}), + 'Table With Subtotal Col Heatmap': makeRenderer({heatmapMode: 'col', subtotals: true}), + 'Table With Subtotal Row Heatmap': makeRenderer({heatmapMode: 'row', subtotals: true}), 'Exportable TSV': TSVExportRenderer, }; diff --git a/src/Utilities.js b/src/Utilities.js index 7bbf0ad..738df9e 100644 --- a/src/Utilities.js +++ b/src/Utilities.js @@ -531,7 +531,7 @@ Data Model class */ class PivotData { - constructor(inputProps = {}) { + constructor(inputProps = {}, subtotals = {}) { this.props = Object.assign({}, PivotData.defaultProps, inputProps); PropTypes.checkPropTypes( PivotData.propTypes, @@ -549,6 +549,7 @@ class PivotData { this.rowTotals = {}; this.colTotals = {}; this.allTotal = this.aggregator(this, [], []); + this.subtotals = subtotals; this.sorted = false; // iterate through input, accumulating data for cells @@ -591,17 +592,18 @@ class PivotData { ); } - arrSort(attrs) { + arrSort(attrs, partialOnTop) { const sortersArr = attrs.map(a => getSort(this.props.sorters, a)); return function(a, b) { - for (const i of Object.keys(sortersArr)) { + const limit = Math.min(a.length, b.length); + for (let i = 0; i < limit; i++) { const sorter = sortersArr[i]; const comparison = sorter(a[i], b[i]); if (comparison !== 0) { return comparison; } } - return 0; + return partialOnTop ? b.length - a.length : a.length - b.length; }; } @@ -617,7 +619,7 @@ class PivotData { this.rowKeys.sort((a, b) => -naturalSort(v(a, []), v(b, []))); break; default: - this.rowKeys.sort(this.arrSort(this.props.rows)); + this.rowKeys.sort(this.arrSort(this.props.rows, this.subtotals.rowPartialOnTop)); } switch (this.props.colOrder) { case 'value_a_to_z': @@ -627,7 +629,7 @@ class PivotData { this.colKeys.sort((a, b) => -naturalSort(v([], a), v([], b))); break; default: - this.colKeys.sort(this.arrSort(this.props.cols)); + this.colKeys.sort(this.arrSort(this.props.cols, this.subtotals.colPartialOnTop)); } } } @@ -657,34 +659,48 @@ class PivotData { this.allTotal.push(record); - if (rowKey.length !== 0) { + const rowStart = this.subtotals.colEnabled ? 1 : Math.max(1, rowKey.length); + const colStart = this.subtotals.rowEnabled ? 1 : Math.max(1, colKey.length); + + for (let ri = rowStart; ri <= rowKey.length; ri++) { + const fRowKey = rowKey.slice(0, ri); + const flatRowKey = flatKey(fRowKey); if (!this.rowTotals[flatRowKey]) { - this.rowKeys.push(rowKey); - this.rowTotals[flatRowKey] = this.aggregator(this, rowKey, []); + this.rowKeys.push(fRowKey); + this.rowTotals[flatRowKey] = this.aggregator(this, fRowKey, []); } this.rowTotals[flatRowKey].push(record); } - if (colKey.length !== 0) { + for (let ci = colStart; ci <= colKey.length; ci++) { + const fColKey = colKey.slice(0, ci); + const flatColKey = flatKey(fColKey); if (!this.colTotals[flatColKey]) { - this.colKeys.push(colKey); - this.colTotals[flatColKey] = this.aggregator(this, [], colKey); + this.colKeys.push(fColKey); + this.colTotals[flatColKey] = this.aggregator(this, [], fColKey); } this.colTotals[flatColKey].push(record); } - if (colKey.length !== 0 && rowKey.length !== 0) { + // And now fill in for all the sub-cells. + for (let ri = rowStart; ri <= rowKey.length; ri++) { + const fRowKey = rowKey.slice(0, ri); + const flatRowKey = flatKey(fRowKey); if (!this.tree[flatRowKey]) { this.tree[flatRowKey] = {}; } - if (!this.tree[flatRowKey][flatColKey]) { - this.tree[flatRowKey][flatColKey] = this.aggregator( - this, - rowKey, - colKey - ); + for (let ci = colStart; ci <= colKey.length; ci++) { + const fColKey = colKey.slice(0, ci); + const flatColKey = flatKey(fColKey); + if (!this.tree[flatRowKey][flatColKey]) { + this.tree[flatRowKey][flatColKey] = this.aggregator( + this, + fRowKey, + fColKey + ); + } + this.tree[flatRowKey][flatColKey].push(record); } - this.tree[flatRowKey][flatColKey].push(record); } } From 61739b2e416b1e7b9dc1843be9ae0f73415cc441 Mon Sep 17 00:00:00 2001 From: Prakhar Goel Date: Mon, 10 Jun 2019 18:23:19 +0000 Subject: [PATCH 06/14] Flip colSubtotal and rowSubtotal enabled flags. Also add in proper expand operations for complete attributes. Also cleanup some edge cases and pre-calculate the callbacks. --- src/TableRenderers.jsx | 226 ++++++++++++++++++++++++++++++----------- src/Utilities.js | 4 +- 2 files changed, 169 insertions(+), 61 deletions(-) diff --git a/src/TableRenderers.jsx b/src/TableRenderers.jsx index 5722390..452473c 100644 --- a/src/TableRenderers.jsx +++ b/src/TableRenderers.jsx @@ -45,13 +45,13 @@ function makeRenderer(opts = {}) { }; const colSubtotalDisplay = { displayOnTop: true, - enabled: colTotals, // by default enable if col totals are enabled. + enabled: rowTotals, // by default enable if row totals are enabled. hideOnExpand: false, ...subtotalOptions.colSubtotalDisplay }; const rowSubtotalDisplay = { displayOnTop: false, - enabled: rowTotals, // by default enable if row totals are enabled. + enabled: colTotals, // by default enable if col totals are enabled. hideOnExpand: false, ...subtotalOptions.rowSubtotalDisplay }; @@ -68,6 +68,55 @@ function makeRenderer(opts = {}) { 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 = {}; + const grandTotalCallback = null; + if(tableOptions.clickCallback) { + for (rowKey in rowKeys) { + const flatRowKey = flatKey(rowKey); + if (cellCallbacks[flatRowKey] === undefined) { + cellCallbacks[flatRowKey] = {} + }; + for (colKey in colKeys) { + cellCallbacks[flatRowKey][flatKey(colKey)] = this.clickHandler( + pivotData.getAggregator(rowKey, colKey).value(), + rowKey, + colKey, + ); + } + } + + // Add in totals as well. + if (rowTotals) { + for (rowKey in rowKeys) { + rowTotalCallbacks[flatKey(rowKey)] = this.clickHandler( + pivotData.getAggregator(rowKey, []).value(), + rowKey, + [], + ); + } + } + if (colTotals) { + for (colKey in colKeys) { + colTotalCallbacks[flatKey(rowKey)] = this.clickHandler( + pivotData.getAggregator([], colKey).value(), + [], + colKey, + ); + } + } + if (rowTotals && colTotals) { + grandTotalCallback = this.clickHandler( + pivotData.getAggregator([], []).value(), + [], + [], + ); + } + } + return { pivotData, colAttrs, @@ -80,6 +129,10 @@ function makeRenderer(opts = {}) { arrowExpanded: subtotalOptions.arrowExpanded, colSubtotalDisplay, rowSubtotalDisplay, + cellCallbacks, + rowTotalCallbacks, + colTotalCallbacks, + grandTotalCallback, ...this.heatmapMappers( pivotData, this.props.tableColorScaleGenerator, @@ -89,9 +142,34 @@ function makeRenderer(opts = {}) { }; }); - toggleAttr = (rowOrCol, attrIdx, allKeys) => () => { - // Toggle an entire attribute. This only collapses the entire - // attribute. Important to keep things snappy. + clickHandler = (value, rowValues, colValues) => { + const colAttrs = this.props.cols; + const rowAttrs = this.props.rows; + const filters = {}; + const colLimit = Math.min(colAttrs.length, colValues.length); + for (let i = 0; i < colLimit;i++) { + 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++) { + 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) => () => { + // Collapse an entire attribute. const keyLen = attrIdx + 1; const collapsed = allKeys.filter(k => k.length == keyLen).map(flatKey); @@ -105,6 +183,25 @@ function makeRenderer(opts = {}) { this.setState(state => ({collapsedCols: {...state.collapsedCols, ...updates}})); } } + + expandAttr = (rowOrCol, attrIdx, allKeys) => () => { + // Expand an entire attribute. This implicitly implies expanding all of the + // parents as well. It's a bit inefficient but ah well... + + const updates = {}; + allKeys.forEach(k => { + for(let i = 0;i <= attrIdx;i++) { + 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 => () => { this.setState(state => ( @@ -197,37 +294,6 @@ function makeRenderer(opts = {}) { return {valueCellColors, rowTotalColors, colTotalColors}; } - clickHandler = (value, rowValues, colValues) => { - const colAttrs = this.props.cols; - const rowAttrs = this.props.rows; - if (this.props.tableOptions && this.props.tableOptions.clickCallback) { - const filters = {}; - const colLimit = Math.min(colAttrs.length, colValues.length); - for (let i = 0; i < colLimit;i++) { - 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++) { - 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. @@ -240,20 +306,34 @@ function makeRenderer(opts = {}) { rowTotals, arrowExpanded, arrowCollapsed, + colSubtotalDisplay, + maxColVisible, } = pivotSettings; const spaceCell = (attrIdx === 0 && rowAttrs.length !== 0) ? () : null; - const needLabelToggle = opts.subtotals && attrIdx !== colAttrs.length - 1; + const needToggle = ( + opts.subtotals + && colSubtotalDisplay.enabled + && attrIdx !== colAttrs.length - 1 + ); + let clickHandle = null; + let subArrow = null; + if (needToggle) { + clickHandle = (attrIdx + 1 < maxColVisible) + ? this.collapseAttr(false, attrIdx, colKeys) + : this.expandAttr(false, attrIdx, colKeys) + subArrow = ((attrIdx + 1 < maxColVisible) ? arrowExpanded : arrowCollapsed) + ' '; + } const attrNameCell = ( - {needLabelToggle ? arrowExpanded + ' ' : null} {attrName} + {subArrow}{attrName} ); @@ -267,8 +347,7 @@ function makeRenderer(opts = {}) { if (attrIdx < colKey.length) { const rowSpan = 1 + ((attrIdx === colAttrs.length - 1) ? rowIncrSpan : 0); const flatColKey = flatKey(colKey.slice(0, attrIdx + 1)); - const needColToggle = opts.subtotals && attrIdx !== colAttrs.length - 1; - const onClick = needColToggle ? this.toggleColKey(flatColKey) : null; + const onClick = needToggle ? this.toggleColKey(flatColKey) : null; attrValueCells.push( - {needColToggle ? (this.state.collapsedCols[flatColKey] ? arrowCollapsed : arrowExpanded) + ' ' : null} + {needToggle ? (this.state.collapsedCols[flatColKey] ? arrowCollapsed : arrowExpanded) + ' ' : null} {colKey[attrIdx]} ) @@ -320,18 +399,38 @@ function makeRenderer(opts = {}) { // Render just the attribute names of the rows (the actual attribute values // will show up in the individual rows). - const {rowAttrs, colAttrs, rowKeys, arrowExpanded} = pivotSettings; + const { + rowAttrs, + colAttrs, + rowKeys, + arrowCollapsed, + arrowExpanded, + rowSubtotalDisplay, + maxRowVisible, + } = pivotSettings; return ( {rowAttrs.map((r, i) => { - const needLabelToggle = opts.subtotals && i !== rowAttrs.length - 1; + const needLabelToggle = ( + opts.subtotals + && rowSubtotalDisplay.enabled + && i !== rowAttrs.length - 1 + ); + let clickHandle = null; + let subArrow = null; + if (needLabelToggle) { + clickHandle = (i + 1 < maxRowVisible) + ? this.collapseAttr(true, i, rowKeys) + : this.expandAttr(true, i, rowKeys) + subArrow = ((i + 1 < maxRowVisible) ? arrowExpanded : arrowCollapsed) + ' '; + } return ( - {needLabelToggle ? arrowExpanded + ' ': null} {r} + {subArrow}{r} ); })} @@ -357,8 +456,12 @@ function makeRenderer(opts = {}) { rowTotalColors, arrowExpanded, arrowCollapsed, + cellCallbacks, + rowTotalCallbacks, } = pivotSettings; + const flatRowKey = flatKey(rowKey); + const colIncrSpan = (colAttrs.length !== 0) ? 1 : 0 const attrValueCells = rowKey.map((r, i) => { const rowSpan = rowAttrSpans[rowIdx][i]; @@ -393,15 +496,17 @@ function makeRenderer(opts = {}) { ) : null; + const rowClickHandlers = cellCallbacks[flatRowKey] || {}; const valueCells = visibleColKeys.map((colKey, j) => { + const flatColKey = flatKey(colKey); const agg = pivotData.getAggregator(rowKey, colKey); const aggValue = agg.value(); const style = valueCellColors(rowKey, colKey, aggValue); return ( {agg.format(aggValue)} @@ -418,7 +523,7 @@ function makeRenderer(opts = {}) { {agg.format(aggValue)} @@ -433,7 +538,7 @@ function makeRenderer(opts = {}) { totalCell, ]; - return ({rowCells}); + return ({rowCells}); } renderTotalsRow = (pivotSettings) => { @@ -445,7 +550,9 @@ function makeRenderer(opts = {}) { visibleColKeys, colTotalColors, rowTotals, - pivotData + pivotData, + colTotalCallbacks, + grandTotalCallback, } = pivotSettings; const totalLabelCell = ( @@ -459,21 +566,22 @@ function makeRenderer(opts = {}) { ); const totalValueCells = visibleColKeys.map((colKey, j) => { + const flatColKey = flatKey(colKey); const agg = pivotData.getAggregator([], colKey); const aggValue = agg.value(); const style = colTotalColors([], colKey, aggValue); return ( {agg.format(aggValue)} ); }); - + let grandTotalCell = null; if (rowTotals) { const agg = pivotData.getAggregator([], []); @@ -482,13 +590,13 @@ function makeRenderer(opts = {}) { {agg.format(aggValue)} ); } - + const totalCells = [ totalLabelCell, ...totalValueCells, @@ -501,9 +609,7 @@ function makeRenderer(opts = {}) { visibleKeys = (keys, collapsed, numAttrs, subtotalDisplay) => keys.filter( key => ( // Is the key hidden by one of its parents? - !key.slice(0, key.length - 1).some( - (k, j) => collapsed[flatKey(key.slice(0, j + 1))] - ) + !key.some((k, j) => collapsed[flatKey(key.slice(0, j))]) && ( key.length == numAttrs // Leaf key. || flatKey(key) in collapsed // Children hidden. Must show total. @@ -544,7 +650,9 @@ function makeRenderer(opts = {}) { : 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), ...basePivotSettings, diff --git a/src/Utilities.js b/src/Utilities.js index 738df9e..6f0cab0 100644 --- a/src/Utilities.js +++ b/src/Utilities.js @@ -659,8 +659,8 @@ class PivotData { this.allTotal.push(record); - const rowStart = this.subtotals.colEnabled ? 1 : Math.max(1, rowKey.length); - const colStart = this.subtotals.rowEnabled ? 1 : Math.max(1, colKey.length); + const rowStart = this.subtotals.rowEnabled ? 1 : Math.max(1, rowKey.length); + const colStart = this.subtotals.colEnabled ? 1 : Math.max(1, colKey.length); for (let ri = rowStart; ri <= rowKey.length; ri++) { const fRowKey = rowKey.slice(0, ri); From c9a2067c80375088889353d9375587e5f8ce1877 Mon Sep 17 00:00:00 2001 From: Prakhar Goel Date: Mon, 10 Jun 2019 20:09:22 +0000 Subject: [PATCH 07/14] Refactor PivotTableUI a bit to make it easier to extend. --- src/PivotTableUI.jsx | 93 ++++++++++++++++++-------------------------- 1 file changed, 37 insertions(+), 56 deletions(-) diff --git a/src/PivotTableUI.jsx b/src/PivotTableUI.jsx index 40296cb..f55d64e 100644 --- a/src/PivotTableUI.jsx +++ b/src/PivotTableUI.jsx @@ -367,32 +367,30 @@ class PivotTableUI extends React.PureComponent { ); } - render() { + rendererCell = () => ( + + + this.setState({ + openDropdown: this.isOpen('renderer') ? false : 'renderer', + }) + } + setValue={this.propUpdater('rendererName')} + /> + + ); + + aggregatorCell = () => { const numValsAllowed = this.props.aggregators[this.props.aggregatorName]([])().numInputs || 0; - const rendererName = - this.props.rendererName in this.props.renderers - ? this.props.rendererName - : Object.keys(this.props.renderers)[0]; - - const rendererCell = ( - - - this.setState({ - openDropdown: this.isOpen('renderer') ? false : 'renderer', - }) - } - setValue={this.propUpdater('rendererName')} - /> - - ); - const sortIcons = { key_a_to_z: { rowSymbol: '↕', @@ -407,7 +405,7 @@ class PivotTableUI extends React.PureComponent { value_z_to_a: {rowSymbol: '↑', colSymbol: '←', next: 'key_a_to_z'}, }; - const aggregatorCell = ( + return ( ); - + } + + render() { const unusedAttrs = Object.keys(this.attrValues) .filter( e => @@ -520,40 +520,21 @@ class PivotTableUI extends React.PureComponent { ); - if (horizUnused) { - return ( - - this.setState({openDropdown: false})}> - - {rendererCell} - {unusedAttrsCell} - - - {aggregatorCell} - {colAttrsCell} - - - {rowAttrsCell} - {outputCell} - - -
- ); - } - + const outputRows = horizUnused + ? [ + ({this.rendererCell()}{unusedAttrsCell}), + ({this.aggregatorCell()}{colAttrsCell}), + ({rowAttrsCell}{outputCell}), + ] + : [ + ({this.rendererCell()}{this.aggregatorCell()}{colAttrsCell}), + ({unusedAttrsCell}{rowAttrsCell}{outputCell}), + ]; + return ( this.setState({openDropdown: false})}> - - {rendererCell} - {aggregatorCell} - {colAttrsCell} - - - {unusedAttrsCell} - {rowAttrsCell} - {outputCell} - + {outputRows}
); From b836a36213e92bc32f559c68f2f6602ed29dd7d4 Mon Sep 17 00:00:00 2001 From: Prakhar Goel Date: Mon, 10 Jun 2019 21:07:50 +0000 Subject: [PATCH 08/14] eslint cleanup. --- src/PivotTableUI.jsx | 42 +++++----- src/TableRenderers.jsx | 186 +++++++++++++++++++++++------------------ src/Utilities.js | 2 - 3 files changed, 126 insertions(+), 104 deletions(-) diff --git a/src/PivotTableUI.jsx b/src/PivotTableUI.jsx index f55d64e..4d985cc 100644 --- a/src/PivotTableUI.jsx +++ b/src/PivotTableUI.jsx @@ -367,27 +367,29 @@ class PivotTableUI extends React.PureComponent { ); } - rendererCell = () => ( - - - this.setState({ - openDropdown: this.isOpen('renderer') ? false : 'renderer', - }) - } - setValue={this.propUpdater('rendererName')} - /> - - ); + rendererCell() { + return ( + + + this.setState({ + openDropdown: this.isOpen('renderer') ? false : 'renderer', + }) + } + setValue={this.propUpdater('rendererName')} + /> + + ); + } - aggregatorCell = () => { + aggregatorCell() { const numValsAllowed = this.props.aggregators[this.props.aggregatorName]([])().numInputs || 0; diff --git a/src/TableRenderers.jsx b/src/TableRenderers.jsx index 452473c..5a724ea 100644 --- a/src/TableRenderers.jsx +++ b/src/TableRenderers.jsx @@ -27,13 +27,13 @@ function makeRenderer(opts = {}) { getBasePivotSettings = memoize(props => { // One-time extraction of pivot settings that we'll use throughout the render. - const colAttrs = this.props.cols; - const rowAttrs = this.props.rows; + const colAttrs = props.cols; + const rowAttrs = props.rows; const tableOptions = { rowTotals: true, colTotals: true, - ...this.props.tableOptions + ...props.tableOptions }; const rowTotals = tableOptions.rowTotals || colAttrs.length === 0; const colTotals = tableOptions.colTotals || rowAttrs.length === 0; @@ -41,17 +41,17 @@ function makeRenderer(opts = {}) { const subtotalOptions = { arrowCollapsed: "\u25B6", arrowExpanded: "\u25E2", - ...this.props.subtotalOptions + ...props.subtotalOptions }; const colSubtotalDisplay = { displayOnTop: true, - enabled: rowTotals, // by default enable if row totals are enabled. + enabled: rowTotals, hideOnExpand: false, ...subtotalOptions.colSubtotalDisplay }; const rowSubtotalDisplay = { displayOnTop: false, - enabled: colTotals, // by default enable if col totals are enabled. + enabled: colTotals, hideOnExpand: false, ...subtotalOptions.rowSubtotalDisplay }; @@ -73,15 +73,17 @@ function makeRenderer(opts = {}) { const cellCallbacks = {}; const rowTotalCallbacks = {}; const colTotalCallbacks = {}; - const grandTotalCallback = null; + let grandTotalCallback = null; if(tableOptions.clickCallback) { - for (rowKey in rowKeys) { + for (const rowKey in rowKeys) { const flatRowKey = flatKey(rowKey); - if (cellCallbacks[flatRowKey] === undefined) { + if (!(flatRowKey in cellCallbacks)) { cellCallbacks[flatRowKey] = {} }; - for (colKey in colKeys) { - cellCallbacks[flatRowKey][flatKey(colKey)] = this.clickHandler( + for (const colKey in colKeys) { + cellCallbacks[flatRowKey][flatKey(colKey)] = TableRenderer.clickHandler( + props, + pivotData, pivotData.getAggregator(rowKey, colKey).value(), rowKey, colKey, @@ -91,8 +93,10 @@ function makeRenderer(opts = {}) { // Add in totals as well. if (rowTotals) { - for (rowKey in rowKeys) { - rowTotalCallbacks[flatKey(rowKey)] = this.clickHandler( + for (const rowKey in rowKeys) { + rowTotalCallbacks[flatKey(rowKey)] = TableRenderer.clickHandler( + props, + pivotData, pivotData.getAggregator(rowKey, []).value(), rowKey, [], @@ -100,8 +104,10 @@ function makeRenderer(opts = {}) { } } if (colTotals) { - for (colKey in colKeys) { - colTotalCallbacks[flatKey(rowKey)] = this.clickHandler( + for (const colKey in colKeys) { + colTotalCallbacks[flatKey(colKey)] = TableRenderer.clickHandler( + props, + pivotData, pivotData.getAggregator([], colKey).value(), [], colKey, @@ -109,14 +115,16 @@ function makeRenderer(opts = {}) { } } if (rowTotals && colTotals) { - grandTotalCallback = this.clickHandler( + grandTotalCallback = TableRenderer.clickHandler( + props, + pivotData, pivotData.getAggregator([], []).value(), [], [], ); } } - + return { pivotData, colAttrs, @@ -133,18 +141,18 @@ function makeRenderer(opts = {}) { rowTotalCallbacks, colTotalCallbacks, grandTotalCallback, - ...this.heatmapMappers( + ...TableRenderer.heatmapMappers( pivotData, - this.props.tableColorScaleGenerator, + props.tableColorScaleGenerator, colTotals, rowTotals, ), }; }); - clickHandler = (value, rowValues, colValues) => { - const colAttrs = this.props.cols; - const rowAttrs = this.props.rows; + static clickHandler(props, pivotData, value, rowValues, colValues) { + const colAttrs = props.cols; + const rowAttrs = props.rows; const filters = {}; const colLimit = Math.min(colAttrs.length, colValues.length); for (let i = 0; i < colLimit;i++) { @@ -160,7 +168,7 @@ function makeRenderer(opts = {}) { filters[attr] = rowValues[i]; } } - return e => this.props.tableOptions.clickCallback( + return e => props.tableOptions.clickCallback( e, value, filters, @@ -168,67 +176,75 @@ function makeRenderer(opts = {}) { ); } - collapseAttr = (rowOrCol, attrIdx, allKeys) => () => { - // Collapse an entire attribute. - - const keyLen = attrIdx + 1; - const collapsed = allKeys.filter(k => k.length == keyLen).map(flatKey); + collapseAttr(rowOrCol, attrIdx, allKeys) { + return () => { + // Collapse an entire attribute. - 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}})); + 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) => () => { - // Expand an entire attribute. This implicitly implies expanding all of the - // parents as well. It's a bit inefficient but ah well... - - const updates = {}; - allKeys.forEach(k => { - for(let i = 0;i <= attrIdx;i++) { - updates[flatKey(k.slice(0, i + 1))] = false; + expandAttr(rowOrCol, attrIdx, allKeys) { + return () => { + // Expand an entire attribute. This implicitly implies expanding all of the + // parents as well. It's a bit inefficient but ah well... + + const updates = {}; + allKeys.forEach(k => { + for(let i = 0;i <= attrIdx;i++) { + 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}})); } - }); + } + } - if (rowOrCol) { - this.setState(state => ({collapsedRows: {...state.collapsedRows, ...updates}})); - } else { - this.setState(state => ({collapsedCols: {...state.collapsedCols, ...updates}})); + toggleRowKey(flatRowKey) { + return () => { + this.setState(state => ( + {collapsedRows: {...state.collapsedRows, [flatRowKey]: !state.collapsedRows[flatRowKey]}} + )) } } - - - toggleRowKey = flatRowKey => () => { - this.setState(state => ( - {collapsedRows: {...state.collapsedRows, [flatRowKey]: !state.collapsedRows[flatRowKey]}} - )) - } - toggleColKey = flatColKey => () => { - this.setState(state => ( - {collapsedCols: {...state.collapsedCols, [flatColKey]: !state.collapsedCols[flatColKey]}} - )) + toggleColKey(flatColKey) { + return () => { + this.setState(state => ( + {collapsedCols: {...state.collapsedCols, [flatColKey]: !state.collapsedCols[flatColKey]}} + )) + } } - - calcAttrSpans = (attrArr, numAttrs) => { + + 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 = []; - const li = Array(numAttrs).map(() => 0); // Index of the last new value + // 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++) { // Keep increasing span values as long as the last keys are the same. For // the rest, record spans of 1. Update the indices too. - let cv = attrArr[i]; - let ent = []; + const cv = attrArr[i]; + const ent = []; let depth = 0; const limit = Math.min(lv.length, cv.length); while (depth < limit && lv[depth] === cv[depth]) { @@ -247,7 +263,7 @@ function makeRenderer(opts = {}) { return spans; } - heatmapMappers = (pivotData, colorScaleGenerator, colTotals, rowTotals) => { + static heatmapMappers(pivotData, colorScaleGenerator, colTotals, rowTotals) { let valueCellColors = () => {}; let rowTotalColors = () => {}; let colTotalColors = () => {}; @@ -294,7 +310,7 @@ function makeRenderer(opts = {}) { return {valueCellColors, rowTotalColors, colTotalColors}; } - renderColHeaderRow = (attrName, attrIdx, pivotSettings) => { + renderColHeaderRow(attrName, attrIdx, pivotSettings) { // Render a single row in the column header at the top of the pivot table. const { @@ -371,7 +387,8 @@ function makeRenderer(opts = {}) { /> ) } - i = i + colSpan; // The next colSpan columns will have the same value anyway... + // The next colSpan columns will have the same value anyway... + i = i + colSpan; }; const totalCell = (attrIdx === 0 && rowTotals) @@ -395,7 +412,7 @@ function makeRenderer(opts = {}) { return {cells}; } - renderRowHeaderRow = (pivotSettings) => { + renderRowHeaderRow(pivotSettings) { // Render just the attribute names of the rows (the actual attribute values // will show up in the individual rows). @@ -441,13 +458,12 @@ function makeRenderer(opts = {}) { ); } - renderTableRow = (rowKey, rowIdx, pivotSettings) => { + renderTableRow(rowKey, rowIdx, pivotSettings) { // Render a single row in the pivot table. const { rowAttrs, colAttrs, - visibleRowKeys, rowAttrSpans, visibleColKeys, pivotData, @@ -483,6 +499,7 @@ function makeRenderer(opts = {}) { ) } + return null; }); const attrValuePaddingCell = (rowKey.length < rowAttrs.length) @@ -497,7 +514,7 @@ function makeRenderer(opts = {}) { : null; const rowClickHandlers = cellCallbacks[flatRowKey] || {}; - const valueCells = visibleColKeys.map((colKey, j) => { + const valueCells = visibleColKeys.map(colKey => { const flatColKey = flatKey(colKey); const agg = pivotData.getAggregator(rowKey, colKey); const aggValue = agg.value(); @@ -541,7 +558,7 @@ function makeRenderer(opts = {}) { return ({rowCells}); } - renderTotalsRow = (pivotSettings) => { + renderTotalsRow(pivotSettings) { // Render the final totals rows that has the totals for all the columns. const { @@ -565,7 +582,7 @@ function makeRenderer(opts = {}) { ); - const totalValueCells = visibleColKeys.map((colKey, j) => { + const totalValueCells = visibleColKeys.map(colKey => { const flatColKey = flatKey(colKey); const agg = pivotData.getAggregator([], colKey); const aggValue = agg.value(); @@ -606,17 +623,22 @@ function makeRenderer(opts = {}) { return ({totalCells}); } - visibleKeys = (keys, collapsed, numAttrs, subtotalDisplay) => keys.filter( - key => ( - // Is the key hidden by one of its parents? - !key.some((k, j) => collapsed[flatKey(key.slice(0, j))]) - && ( - key.length == numAttrs // Leaf key. - || flatKey(key) in collapsed // Children hidden. Must show total. - || !subtotalDisplay.hideOnExpand // Don't hide totals. + 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 + ) ) ) - ) + } render() { const basePivotSettings = this.getBasePivotSettings(this.props); diff --git a/src/Utilities.js b/src/Utilities.js index 6f0cab0..00ccf7c 100644 --- a/src/Utilities.js +++ b/src/Utilities.js @@ -654,8 +654,6 @@ class PivotData { for (const x of this.props.rows) { rowKey.push(x in record ? record[x] : 'null'); } - const flatRowKey = flatKey(rowKey); - const flatColKey = flatKey(colKey); this.allTotal.push(record); From ee8f1dcca46c9e3fed15c2c48ce5c4771f0ac533 Mon Sep 17 00:00:00 2001 From: Prakhar Goel Date: Mon, 10 Jun 2019 21:11:40 +0000 Subject: [PATCH 09/14] Strip trailing whitespace. --- src/PivotTableUI.jsx | 4 +- src/TableRenderers.jsx | 98 +++++++++++++++++++++--------------------- src/Utilities.js | 2 +- 3 files changed, 52 insertions(+), 52 deletions(-) diff --git a/src/PivotTableUI.jsx b/src/PivotTableUI.jsx index 4d985cc..6f6edee 100644 --- a/src/PivotTableUI.jsx +++ b/src/PivotTableUI.jsx @@ -467,7 +467,7 @@ class PivotTableUI extends React.PureComponent { ); } - + render() { const unusedAttrs = Object.keys(this.attrValues) .filter( @@ -532,7 +532,7 @@ class PivotTableUI extends React.PureComponent { ({this.rendererCell()}{this.aggregatorCell()}{colAttrsCell}), ({unusedAttrsCell}{rowAttrsCell}{outputCell}), ]; - + return ( this.setState({openDropdown: false})}> diff --git a/src/TableRenderers.jsx b/src/TableRenderers.jsx index 5a724ea..e24af34 100644 --- a/src/TableRenderers.jsx +++ b/src/TableRenderers.jsx @@ -17,7 +17,7 @@ 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. @@ -26,7 +26,7 @@ function makeRenderer(opts = {}) { getBasePivotSettings = memoize(props => { // One-time extraction of pivot settings that we'll use throughout the render. - + const colAttrs = props.cols; const rowAttrs = props.rows; @@ -90,7 +90,7 @@ function makeRenderer(opts = {}) { ); } } - + // Add in totals as well. if (rowTotals) { for (const rowKey in rowKeys) { @@ -142,7 +142,7 @@ function makeRenderer(opts = {}) { colTotalCallbacks, grandTotalCallback, ...TableRenderer.heatmapMappers( - pivotData, + pivotData, props.tableColorScaleGenerator, colTotals, rowTotals, @@ -176,16 +176,16 @@ function makeRenderer(opts = {}) { ); } - collapseAttr(rowOrCol, attrIdx, allKeys) { + collapseAttr(rowOrCol, attrIdx, allKeys) { return () => { // Collapse an entire attribute. - + 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 { @@ -193,12 +193,12 @@ function makeRenderer(opts = {}) { } } } - + expandAttr(rowOrCol, attrIdx, allKeys) { return () => { // Expand an entire attribute. This implicitly implies expanding all of the // parents as well. It's a bit inefficient but ah well... - + const updates = {}; allKeys.forEach(k => { for(let i = 0;i <= attrIdx;i++) { @@ -212,7 +212,7 @@ function makeRenderer(opts = {}) { this.setState(state => ({collapsedCols: {...state.collapsedCols, ...updates}})); } } - } + } toggleRowKey(flatRowKey) { return () => { @@ -221,7 +221,7 @@ function makeRenderer(opts = {}) { )) } } - + toggleColKey(flatColKey) { return () => { this.setState(state => ( @@ -235,7 +235,7 @@ function makeRenderer(opts = {}) { // 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); @@ -262,7 +262,7 @@ function makeRenderer(opts = {}) { } return spans; } - + static heatmapMappers(pivotData, colorScaleGenerator, colTotals, rowTotals) { let valueCellColors = () => {}; let rowTotalColors = () => {}; @@ -312,9 +312,9 @@ function makeRenderer(opts = {}) { renderColHeaderRow(attrName, attrIdx, pivotSettings) { // Render a single row in the column header at the top of the pivot table. - + const { - rowAttrs, + rowAttrs, colAttrs, colKeys, visibleColKeys, @@ -329,7 +329,7 @@ function makeRenderer(opts = {}) { const spaceCell = (attrIdx === 0 && rowAttrs.length !== 0) ? ( ); - + const attrValueCells = []; const rowIncrSpan = (rowAttrs.length !== 0) ? 1 : 0; // Iterate through columns. Jump over duplicate values. @@ -411,11 +411,11 @@ function makeRenderer(opts = {}) { ]; 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, @@ -442,8 +442,8 @@ function makeRenderer(opts = {}) { subArrow = ((i + 1 < maxRowVisible) ? arrowExpanded : arrowCollapsed) + ' '; } return ( - ); } - + renderTableRow(rowKey, rowIdx, pivotSettings) { // Render a single row in the pivot table. - + const { - rowAttrs, - colAttrs, + rowAttrs, + colAttrs, rowAttrSpans, - visibleColKeys, + visibleColKeys, pivotData, rowTotals, valueCellColors, @@ -475,9 +475,9 @@ function makeRenderer(opts = {}) { cellCallbacks, rowTotalCallbacks, } = pivotSettings; - + const flatRowKey = flatKey(rowKey); - + const colIncrSpan = (colAttrs.length !== 0) ? 1 : 0 const attrValueCells = rowKey.map((r, i) => { const rowSpan = rowAttrSpans[rowIdx][i]; @@ -501,7 +501,7 @@ function makeRenderer(opts = {}) { } return null; }); - + const attrValuePaddingCell = (rowKey.length < rowAttrs.length) ? (
) : null; - + const needToggle = ( opts.subtotals && colSubtotalDisplay.enabled @@ -344,15 +344,15 @@ function makeRenderer(opts = {}) { subArrow = ((attrIdx + 1 < maxColVisible) ? arrowExpanded : arrowCollapsed) + ' '; } const attrNameCell = ( - {subArrow}{attrName}
@@ -457,15 +457,15 @@ function makeRenderer(opts = {}) {
) : null; - + const rowClickHandlers = cellCallbacks[flatRowKey] || {}; const valueCells = visibleColKeys.map(colKey => { const flatColKey = flatKey(colKey); @@ -530,7 +530,7 @@ function makeRenderer(opts = {}) { ); }); - + let totalCell = null; if (rowTotals) { const agg = pivotData.getAggregator(rowKey, []); @@ -547,7 +547,7 @@ function makeRenderer(opts = {}) { ); } - + const rowCells = [ ...attrValueCells, attrValuePaddingCell, @@ -560,18 +560,18 @@ function makeRenderer(opts = {}) { renderTotalsRow(pivotSettings) { // Render the final totals rows that has the totals for all the columns. - + const { rowAttrs, colAttrs, visibleColKeys, colTotalColors, - rowTotals, + rowTotals, pivotData, colTotalCallbacks, grandTotalCallback, } = pivotSettings; - + const totalLabelCell = ( ); - + const totalValueCells = visibleColKeys.map(colKey => { const flatColKey = flatKey(colKey); const agg = pivotData.getAggregator([], colKey); @@ -630,28 +630,28 @@ function makeRenderer(opts = {}) { !key.some((k, j) => collapsed[flatKey(key.slice(0, j))]) && ( // Leaf key. - key.length === numAttrs + key.length === numAttrs // Children hidden. Must show total. - || flatKey(key) in collapsed + || flatKey(key) in collapsed // Don't hide totals. - || !subtotalDisplay.hideOnExpand + || !subtotalDisplay.hideOnExpand ) ) ) } - + render() { const basePivotSettings = this.getBasePivotSettings(this.props); const { - colAttrs, - rowAttrs, - rowKeys, - colKeys, + colAttrs, + rowAttrs, + rowKeys, + colKeys, colTotals, rowSubtotalDisplay, colSubtotalDisplay, } = basePivotSettings; - + // Need to account for exclusions to compute the effective row // and column keys. const visibleRowKeys = opts.subtotals diff --git a/src/Utilities.js b/src/Utilities.js index 00ccf7c..3cebf6b 100644 --- a/src/Utilities.js +++ b/src/Utilities.js @@ -659,7 +659,7 @@ class PivotData { const rowStart = this.subtotals.rowEnabled ? 1 : Math.max(1, rowKey.length); const colStart = this.subtotals.colEnabled ? 1 : Math.max(1, colKey.length); - + for (let ri = rowStart; ri <= rowKey.length; ri++) { const fRowKey = rowKey.slice(0, ri); const flatRowKey = flatKey(fRowKey); From d3de731eec45012c8ab94f90d1bb5f9053fbb856 Mon Sep 17 00:00:00 2001 From: Prakhar Goel Date: Mon, 10 Jun 2019 23:16:08 +0000 Subject: [PATCH 10/14] Stop using ES features not supported by the babel presets. --- src/TableRenderers.jsx | 269 ++++++++++++++++++++--------------------- 1 file changed, 134 insertions(+), 135 deletions(-) diff --git a/src/TableRenderers.jsx b/src/TableRenderers.jsx index e24af34..1783d2b 100644 --- a/src/TableRenderers.jsx +++ b/src/TableRenderers.jsx @@ -24,132 +24,6 @@ function makeRenderer(opts = {}) { this.state = {collapsedRows: {}, collapsedCols: {}}; } - getBasePivotSettings = memoize(props => { - // One-time extraction of pivot settings that we'll use throughout the render. - - 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 subtotalOptions = { - arrowCollapsed: "\u25B6", - arrowExpanded: "\u25E2", - ...props.subtotalOptions - }; - const colSubtotalDisplay = { - displayOnTop: true, - 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) { - for (const rowKey in rowKeys) { - const flatRowKey = flatKey(rowKey); - if (!(flatRowKey in cellCallbacks)) { - cellCallbacks[flatRowKey] = {} - }; - for (const colKey in colKeys) { - cellCallbacks[flatRowKey][flatKey(colKey)] = TableRenderer.clickHandler( - props, - pivotData, - pivotData.getAggregator(rowKey, colKey).value(), - rowKey, - colKey, - ); - } - } - - // Add in totals as well. - if (rowTotals) { - for (const rowKey in rowKeys) { - rowTotalCallbacks[flatKey(rowKey)] = TableRenderer.clickHandler( - props, - pivotData, - pivotData.getAggregator(rowKey, []).value(), - rowKey, - [], - ); - } - } - if (colTotals) { - for (const colKey in colKeys) { - colTotalCallbacks[flatKey(colKey)] = TableRenderer.clickHandler( - props, - pivotData, - pivotData.getAggregator([], colKey).value(), - [], - colKey, - ); - } - } - if (rowTotals && colTotals) { - grandTotalCallback = TableRenderer.clickHandler( - props, - pivotData, - pivotData.getAggregator([], []).value(), - [], - [], - ); - } - } - - return { - pivotData, - colAttrs, - rowAttrs, - colKeys, - rowKeys, - rowTotals, - colTotals, - arrowCollapsed: subtotalOptions.arrowCollapsed, - arrowExpanded: subtotalOptions.arrowExpanded, - colSubtotalDisplay, - rowSubtotalDisplay, - cellCallbacks, - rowTotalCallbacks, - colTotalCallbacks, - grandTotalCallback, - ...TableRenderer.heatmapMappers( - pivotData, - props.tableColorScaleGenerator, - colTotals, - rowTotals, - ), - }; - }); - static clickHandler(props, pivotData, value, rowValues, colValues) { const colAttrs = props.cols; const rowAttrs = props.rows; @@ -187,9 +61,9 @@ function makeRenderer(opts = {}) { collapsed.forEach(k => {updates[k] = true;}); if (rowOrCol) { - this.setState(state => ({collapsedRows: {...state.collapsedRows, ...updates}})); + this.setState(state => ({collapsedRows: Object.assign({}, state.collapsedRows, updates)})); } else { - this.setState(state => ({collapsedCols: {...state.collapsedCols, ...updates}})); + this.setState(state => ({collapsedCols: Object.assign({}, state.collapsedCols, updates)})); } } } @@ -207,9 +81,9 @@ function makeRenderer(opts = {}) { }); if (rowOrCol) { - this.setState(state => ({collapsedRows: {...state.collapsedRows, ...updates}})); + this.setState(state => ({collapsedRows: Object.assign({}, state.collapsedRows, updates)})); } else { - this.setState(state => ({collapsedCols: {...state.collapsedCols, ...updates}})); + this.setState(state => ({collapsedCols: Object.assign({}, state.collapsedCols, updates)})); } } } @@ -217,7 +91,7 @@ function makeRenderer(opts = {}) { toggleRowKey(flatRowKey) { return () => { this.setState(state => ( - {collapsedRows: {...state.collapsedRows, [flatRowKey]: !state.collapsedRows[flatRowKey]}} + {collapsedRows: Object.assign({}, state.collapsedRows, {[flatRowKey]: !state.collapsedRows[flatRowKey]})} )) } } @@ -225,7 +99,7 @@ function makeRenderer(opts = {}) { toggleColKey(flatColKey) { return () => { this.setState(state => ( - {collapsedCols: {...state.collapsedCols, [flatColKey]: !state.collapsedCols[flatColKey]}} + {collapsedCols: Object.assign({}, state.collapsedCols, {[flatColKey]: !state.collapsedCols[flatColKey]})} )) } } @@ -670,15 +544,14 @@ function makeRenderer(opts = {}) { colSubtotalDisplay, ) : colKeys; - const pivotSettings = { + const pivotSettings = Object.assign({ 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), - ...basePivotSettings, - }; + }, basePivotSettings); return ( @@ -695,6 +568,132 @@ function makeRenderer(opts = {}) { } } + TableRenderer.getBasePivotSettings = memoize(props => { + // One-time extraction of pivot settings that we'll use throughout the render. + + const colAttrs = props.cols; + const rowAttrs = props.rows; + + const tableOptions = Object.assign({ + rowTotals: true, + colTotals: true, + }, props.tableOptions); + const rowTotals = tableOptions.rowTotals || colAttrs.length === 0; + const colTotals = tableOptions.colTotals || rowAttrs.length === 0; + + const subtotalOptions = Object.assign({ + arrowCollapsed: "\u25B6", + arrowExpanded: "\u25E2", + }, props.subtotalOptions); + + const colSubtotalDisplay = Object.assign({ + displayOnTop: true, + enabled: rowTotals, + hideOnExpand: false, + }, subtotalOptions.colSubtotalDisplay); + + const rowSubtotalDisplay = Object.assign({ + 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) { + for (const rowKey in rowKeys) { + const flatRowKey = flatKey(rowKey); + if (!(flatRowKey in cellCallbacks)) { + cellCallbacks[flatRowKey] = {} + }; + for (const colKey in colKeys) { + cellCallbacks[flatRowKey][flatKey(colKey)] = TableRenderer.clickHandler( + props, + pivotData, + pivotData.getAggregator(rowKey, colKey).value(), + rowKey, + colKey, + ); + } + } + + // Add in totals as well. + if (rowTotals) { + for (const rowKey in rowKeys) { + rowTotalCallbacks[flatKey(rowKey)] = TableRenderer.clickHandler( + props, + pivotData, + pivotData.getAggregator(rowKey, []).value(), + rowKey, + [], + ); + } + } + if (colTotals) { + for (const colKey in colKeys) { + colTotalCallbacks[flatKey(colKey)] = TableRenderer.clickHandler( + props, + pivotData, + pivotData.getAggregator([], colKey).value(), + [], + colKey, + ); + } + } + if (rowTotals && colTotals) { + grandTotalCallback = TableRenderer.clickHandler( + props, + pivotData, + pivotData.getAggregator([], []).value(), + [], + [], + ); + } + } + + return Object.assign( + { + pivotData, + colAttrs, + rowAttrs, + colKeys, + rowKeys, + rowTotals, + colTotals, + arrowCollapsed: subtotalOptions.arrowCollapsed, + arrowExpanded: subtotalOptions.arrowExpanded, + colSubtotalDisplay, + rowSubtotalDisplay, + cellCallbacks, + rowTotalCallbacks, + colTotalCallbacks, + grandTotalCallback, + }, + TableRenderer.heatmapMappers( + pivotData, + props.tableColorScaleGenerator, + colTotals, + rowTotals, + ), + ); + }); + TableRenderer.defaultProps = PivotData.defaultProps; TableRenderer.propTypes = PivotData.propTypes; TableRenderer.defaultProps.tableColorScaleGenerator = redColorScaleGenerator; From f4cef8698cbacdf1a85d2851709570b8c2787156 Mon Sep 17 00:00:00 2001 From: Prakhar Goel Date: Tue, 11 Jun 2019 00:02:51 +0000 Subject: [PATCH 11/14] Manual caching since memoize-one seems to have problems. --- src/TableRenderers.jsx | 263 ++++++++++++++++++++--------------------- 1 file changed, 131 insertions(+), 132 deletions(-) diff --git a/src/TableRenderers.jsx b/src/TableRenderers.jsx index 1783d2b..7746ded 100644 --- a/src/TableRenderers.jsx +++ b/src/TableRenderers.jsx @@ -1,7 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import {PivotData, flatKey} from './Utilities'; -import memoize from 'memoize-one'; + +/* eslint-disable react/prop-types */ +// eslint can't see inherited propTypes! function redColorScaleGenerator(values) { const min = Math.min.apply(Math, values); @@ -24,9 +26,129 @@ function makeRenderer(opts = {}) { this.state = {collapsedRows: {}, collapsedCols: {}}; } - static clickHandler(props, pivotData, value, rowValues, colValues) { + getBasePivotSettings() { + // One-time extraction of pivot settings that we'll use throughout the render. + + const props = this.props; const colAttrs = props.cols; const rowAttrs = props.rows; + + const tableOptions = Object.assign({ + rowTotals: true, + colTotals: true, + }, props.tableOptions); + const rowTotals = tableOptions.rowTotals || colAttrs.length === 0; + const colTotals = tableOptions.colTotals || rowAttrs.length === 0; + + const subtotalOptions = Object.assign({ + arrowCollapsed: "\u25B6", + arrowExpanded: "\u25E2", + }, props.subtotalOptions); + + const colSubtotalDisplay = Object.assign({ + displayOnTop: true, + enabled: rowTotals, + hideOnExpand: false, + }, subtotalOptions.colSubtotalDisplay); + + const rowSubtotalDisplay = Object.assign({ + 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) { + for (const rowKey in rowKeys) { + const flatRowKey = flatKey(rowKey); + if (!(flatRowKey in cellCallbacks)) { + cellCallbacks[flatRowKey] = {} + }; + for (const colKey in colKeys) { + cellCallbacks[flatRowKey][flatKey(colKey)] = this.clickHandler( + pivotData, + rowKey, + colKey, + ); + } + } + + // Add in totals as well. + if (rowTotals) { + for (const rowKey in rowKeys) { + rowTotalCallbacks[flatKey(rowKey)] = TableRenderer.clickHandler( + pivotData, + rowKey, + [], + ); + } + } + if (colTotals) { + for (const colKey in colKeys) { + colTotalCallbacks[flatKey(colKey)] = TableRenderer.clickHandler( + pivotData, + [], + colKey, + ); + } + } + if (rowTotals && colTotals) { + grandTotalCallback = TableRenderer.clickHandler( + pivotData, + [], + [], + ); + } + } + + return Object.assign( + { + pivotData, + colAttrs, + rowAttrs, + colKeys, + rowKeys, + rowTotals, + colTotals, + arrowCollapsed: subtotalOptions.arrowCollapsed, + arrowExpanded: subtotalOptions.arrowExpanded, + colSubtotalDisplay, + rowSubtotalDisplay, + cellCallbacks, + rowTotalCallbacks, + colTotalCallbacks, + grandTotalCallback, + }, + TableRenderer.heatmapMappers( + pivotData, + props.tableColorScaleGenerator, + colTotals, + rowTotals, + ), + ); + }; + + 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++) { @@ -42,7 +164,7 @@ function makeRenderer(opts = {}) { filters[attr] = rowValues[i]; } } - return e => props.tableOptions.clickCallback( + return e => this.props.tableOptions.clickCallback( e, value, filters, @@ -515,7 +637,10 @@ function makeRenderer(opts = {}) { } render() { - const basePivotSettings = this.getBasePivotSettings(this.props); + if (this.cachedProps !== this.props) { + this.cachedProps = this.props; + this.cachedBasePivotSettings = this.getBasePivotSettings(); + } const { colAttrs, rowAttrs, @@ -524,7 +649,7 @@ function makeRenderer(opts = {}) { colTotals, rowSubtotalDisplay, colSubtotalDisplay, - } = basePivotSettings; + } = this.cachedBasePivotSettings; // Need to account for exclusions to compute the effective row // and column keys. @@ -551,7 +676,7 @@ function makeRenderer(opts = {}) { maxColVisible: Math.max(...visibleColKeys.map(k => k.length)), rowAttrSpans: this.calcAttrSpans(visibleRowKeys, rowAttrs.length), colAttrSpans: this.calcAttrSpans(visibleColKeys, colAttrs.length), - }, basePivotSettings); + }, this.cachedBasePivotSettings); return (
@@ -568,132 +693,6 @@ function makeRenderer(opts = {}) { } } - TableRenderer.getBasePivotSettings = memoize(props => { - // One-time extraction of pivot settings that we'll use throughout the render. - - const colAttrs = props.cols; - const rowAttrs = props.rows; - - const tableOptions = Object.assign({ - rowTotals: true, - colTotals: true, - }, props.tableOptions); - const rowTotals = tableOptions.rowTotals || colAttrs.length === 0; - const colTotals = tableOptions.colTotals || rowAttrs.length === 0; - - const subtotalOptions = Object.assign({ - arrowCollapsed: "\u25B6", - arrowExpanded: "\u25E2", - }, props.subtotalOptions); - - const colSubtotalDisplay = Object.assign({ - displayOnTop: true, - enabled: rowTotals, - hideOnExpand: false, - }, subtotalOptions.colSubtotalDisplay); - - const rowSubtotalDisplay = Object.assign({ - 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) { - for (const rowKey in rowKeys) { - const flatRowKey = flatKey(rowKey); - if (!(flatRowKey in cellCallbacks)) { - cellCallbacks[flatRowKey] = {} - }; - for (const colKey in colKeys) { - cellCallbacks[flatRowKey][flatKey(colKey)] = TableRenderer.clickHandler( - props, - pivotData, - pivotData.getAggregator(rowKey, colKey).value(), - rowKey, - colKey, - ); - } - } - - // Add in totals as well. - if (rowTotals) { - for (const rowKey in rowKeys) { - rowTotalCallbacks[flatKey(rowKey)] = TableRenderer.clickHandler( - props, - pivotData, - pivotData.getAggregator(rowKey, []).value(), - rowKey, - [], - ); - } - } - if (colTotals) { - for (const colKey in colKeys) { - colTotalCallbacks[flatKey(colKey)] = TableRenderer.clickHandler( - props, - pivotData, - pivotData.getAggregator([], colKey).value(), - [], - colKey, - ); - } - } - if (rowTotals && colTotals) { - grandTotalCallback = TableRenderer.clickHandler( - props, - pivotData, - pivotData.getAggregator([], []).value(), - [], - [], - ); - } - } - - return Object.assign( - { - pivotData, - colAttrs, - rowAttrs, - colKeys, - rowKeys, - rowTotals, - colTotals, - arrowCollapsed: subtotalOptions.arrowCollapsed, - arrowExpanded: subtotalOptions.arrowExpanded, - colSubtotalDisplay, - rowSubtotalDisplay, - cellCallbacks, - rowTotalCallbacks, - colTotalCallbacks, - grandTotalCallback, - }, - TableRenderer.heatmapMappers( - pivotData, - props.tableColorScaleGenerator, - colTotals, - rowTotals, - ), - ); - }); - TableRenderer.defaultProps = PivotData.defaultProps; TableRenderer.propTypes = PivotData.propTypes; TableRenderer.defaultProps.tableColorScaleGenerator = redColorScaleGenerator; From 2243e3cbaf92b495f2cc868b80a35e14f57554d5 Mon Sep 17 00:00:00 2001 From: Prakhar Goel Date: Thu, 13 Jun 2019 15:54:53 +0000 Subject: [PATCH 12/14] Fixed a bug in the sorting code for partial row/column keys. Added in more tests for PivotData. Also added a separate build target. --- package.json | 3 +- src/TableRenderers.jsx | 4 +- src/Utilities.js | 2 +- src/__tests__/Utilities-test.js | 85 +++++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 161720e..742c0cc 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "test:jest": "jest", "test": "npm run test:eslint && npm run test:prettier && npm run test:jest", "clean": "rm -rf __tests__ PivotTable.js* PivotTableUI.js* PlotlyRenderers.js* TableRenderers.js* Utilities.js* pivottable.css", - "doPublish": "npm run clean && cp src/pivottable.css . && babel src --out-dir=. --source-maps --presets=env,react --plugins babel-plugin-add-module-exports && npm publish", + "build": "npm run clean && cp src/pivottable.css . && babel src --out-dir=. --source-maps --presets=env,react --plugins babel-plugin-add-module-exports", + "doPublish": "npm run build && npm publish", "postpublish": "npm run clean", "deploy": "webpack -p && mv bundle.js examples && cd examples && git init && git add . && git commit -m build && git push --force git@github.com:plotly/react-pivottable.git master:gh-pages && rm -rf .git bundle.js" }, diff --git a/src/TableRenderers.jsx b/src/TableRenderers.jsx index 7746ded..a66245e 100644 --- a/src/TableRenderers.jsx +++ b/src/TableRenderers.jsx @@ -46,13 +46,13 @@ function makeRenderer(opts = {}) { }, props.subtotalOptions); const colSubtotalDisplay = Object.assign({ - displayOnTop: true, + displayOnTop: false, enabled: rowTotals, hideOnExpand: false, }, subtotalOptions.colSubtotalDisplay); const rowSubtotalDisplay = Object.assign({ - displayOnTop: false, + displayOnTop: true, enabled: colTotals, hideOnExpand: false, }, subtotalOptions.rowSubtotalDisplay); diff --git a/src/Utilities.js b/src/Utilities.js index 3cebf6b..e189eee 100644 --- a/src/Utilities.js +++ b/src/Utilities.js @@ -603,7 +603,7 @@ class PivotData { return comparison; } } - return partialOnTop ? b.length - a.length : a.length - b.length; + return partialOnTop ? a.length - b.length : b.length - a.length; }; } diff --git a/src/__tests__/Utilities-test.js b/src/__tests__/Utilities-test.js index d7f41bd..901fbb9 100644 --- a/src/__tests__/Utilities-test.js +++ b/src/__tests__/Utilities-test.js @@ -136,6 +136,91 @@ describe(' utils', function() { expect(agg.format(val)).toBe('4'); }); }); + + describe('with rows/cols and subtotals', function() { + const pd = new utils.PivotData( + { + data: fixtureData, + rows: ['name', 'colour'], + cols: ['trials', 'successes'], + }, + // Subtotals settings. + { + rowEnabled: true, + colEnabled: true, + rowPartialOnTop: true, + colPartialOnTop: false, + }, + ); + + it('has correctly-ordered row keys', () => + expect(pd.getRowKeys()).toEqual([ + ['Carol'], + ['Carol', 'yellow'], + ['Jane'], + ['Jane', 'red'], + ['John'], + ['John', 'blue'], + ['Nick'], + ['Nick', 'blue'], + ])); + + it('has correctly-ordered col keys', () => + expect(pd.getColKeys()).toEqual([ + [95, 25], + [95], + [102, 14], + [102], + [103, 12], + [103], + [112, 30], + [112], + ])); + + it('can be iterated over', function() { + let numNotNull = 0; + let numNull = 0; + for (const r of Array.from(pd.getRowKeys())) { + for (const c of Array.from(pd.getColKeys())) { + if (pd.getAggregator(r, c).value() !== null) { + numNotNull++; + } else { + numNull++; + } + } + } + expect(numNotNull).toBe(16); + expect(numNull).toBe(48); + }); + + it('has a correct spot-checked aggregator', function() { + const agg = pd.getAggregator(['Carol', 'yellow'], [102, 14]); + const val = agg.value(); + expect(val).toBe(1); + expect(agg.format(val)).toBe('1'); + }); + + it('has a correct spot-checked aggregator #2', function() { + const agg = pd.getAggregator(['John'], [112, 30]); + const val = agg.value(); + expect(val).toBe(1); + expect(agg.format(val)).toBe('1'); + }); + + it('has a correct spot-checked aggregator #3', function() { + const agg = pd.getAggregator(['Jane', 'red'], [102]); + const val = agg.value(); + expect(val).toBe(null); + expect(agg.format(val)).toBe(''); + }); + + it('has a correct grand total aggregator', function() { + const agg = pd.getAggregator([], []); + const val = agg.value(); + expect(val).toBe(4); + expect(agg.format(val)).toBe('4'); + }); + }); }); describe('.aggregatorTemplates', function() { From 428298474c82b5941a75fee8d081852291b16ece Mon Sep 17 00:00:00 2001 From: Prakhar Goel Date: Thu, 13 Jun 2019 15:55:58 +0000 Subject: [PATCH 13/14] Ran prettier. --- src/PivotTableUI.jsx | 42 +++-- src/TableRenderers.jsx | 365 +++++++++++++++++++++++------------------ src/Utilities.js | 10 +- 3 files changed, 242 insertions(+), 175 deletions(-) diff --git a/src/PivotTableUI.jsx b/src/PivotTableUI.jsx index 6f6edee..77e6082 100644 --- a/src/PivotTableUI.jsx +++ b/src/PivotTableUI.jsx @@ -54,7 +54,7 @@ export class DraggableAttribute extends React.Component { style={{ display: 'block', cursor: 'initial', - zIndex: this.props.zIndex + zIndex: this.props.zIndex, }} onClick={() => this.props.moveFilterBoxToTop(this.props.name)} > @@ -135,7 +135,7 @@ export class DraggableAttribute extends React.Component { } toggleFilterBox() { - this.setState({ open: !this.state.open}); + this.setState({open: !this.state.open}); this.props.moveFilterBoxToTop(this.props.name); } @@ -371,9 +371,10 @@ class PivotTableUI extends React.PureComponent { return ( ), - ({this.aggregatorCell()}{colAttrsCell}), - ({rowAttrsCell}{outputCell}), - ] + + {this.rendererCell()} + {unusedAttrsCell} + , + + {this.aggregatorCell()} + {colAttrsCell} + , + + {rowAttrsCell} + {outputCell} + , + ] : [ - ({this.rendererCell()}{this.aggregatorCell()}{colAttrsCell}), - ({unusedAttrsCell}{rowAttrsCell}{outputCell}), - ]; + + {this.rendererCell()} + {this.aggregatorCell()} + {colAttrsCell} + , + + {unusedAttrsCell} + {rowAttrsCell} + {outputCell} + , + ]; return (
{this.rendererCell()}{unusedAttrsCell}
diff --git a/src/TableRenderers.jsx b/src/TableRenderers.jsx index a66245e..f9c85ed 100644 --- a/src/TableRenderers.jsx +++ b/src/TableRenderers.jsx @@ -33,38 +33,52 @@ function makeRenderer(opts = {}) { const colAttrs = props.cols; const rowAttrs = props.rows; - const tableOptions = Object.assign({ - rowTotals: true, - colTotals: true, - }, props.tableOptions); + const tableOptions = Object.assign( + { + rowTotals: true, + colTotals: true, + }, + props.tableOptions + ); const rowTotals = tableOptions.rowTotals || colAttrs.length === 0; const colTotals = tableOptions.colTotals || rowAttrs.length === 0; - const subtotalOptions = Object.assign({ - arrowCollapsed: "\u25B6", - arrowExpanded: "\u25E2", - }, props.subtotalOptions); - - const colSubtotalDisplay = Object.assign({ - displayOnTop: false, - enabled: rowTotals, - hideOnExpand: false, - }, subtotalOptions.colSubtotalDisplay); - - const rowSubtotalDisplay = Object.assign({ - displayOnTop: true, - enabled: colTotals, - hideOnExpand: false, - }, subtotalOptions.rowSubtotalDisplay); + const subtotalOptions = Object.assign( + { + arrowCollapsed: '\u25B6', + arrowExpanded: '\u25E2', + }, + props.subtotalOptions + ); + + const colSubtotalDisplay = Object.assign( + { + displayOnTop: false, + enabled: rowTotals, + hideOnExpand: false, + }, + subtotalOptions.colSubtotalDisplay + ); + + const rowSubtotalDisplay = Object.assign( + { + displayOnTop: true, + 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, - }, + props, + !opts.subtotals + ? {} + : { + rowEnabled: rowSubtotalDisplay.enabled, + colEnabled: colSubtotalDisplay.enabled, + rowPartialOnTop: rowSubtotalDisplay.displayOnTop, + colPartialOnTop: colSubtotalDisplay.displayOnTop, + } ); const rowKeys = pivotData.getRowKeys(); const colKeys = pivotData.getColKeys(); @@ -75,17 +89,17 @@ function makeRenderer(opts = {}) { const rowTotalCallbacks = {}; const colTotalCallbacks = {}; let grandTotalCallback = null; - if(tableOptions.clickCallback) { + if (tableOptions.clickCallback) { for (const rowKey in rowKeys) { const flatRowKey = flatKey(rowKey); if (!(flatRowKey in cellCallbacks)) { - cellCallbacks[flatRowKey] = {} - }; + cellCallbacks[flatRowKey] = {}; + } for (const colKey in colKeys) { cellCallbacks[flatRowKey][flatKey(colKey)] = this.clickHandler( pivotData, rowKey, - colKey, + colKey ); } } @@ -96,7 +110,7 @@ function makeRenderer(opts = {}) { rowTotalCallbacks[flatKey(rowKey)] = TableRenderer.clickHandler( pivotData, rowKey, - [], + [] ); } } @@ -105,16 +119,12 @@ function makeRenderer(opts = {}) { colTotalCallbacks[flatKey(colKey)] = TableRenderer.clickHandler( pivotData, [], - colKey, + colKey ); } } if (rowTotals && colTotals) { - grandTotalCallback = TableRenderer.clickHandler( - pivotData, - [], - [], - ); + grandTotalCallback = TableRenderer.clickHandler(pivotData, [], []); } } @@ -140,36 +150,32 @@ function makeRenderer(opts = {}) { pivotData, props.tableColorScaleGenerator, colTotals, - rowTotals, - ), + rowTotals + ) ); - }; + } clickHandler(pivotData, rowValues, colValues) { const colAttrs = this.props.cols; const rowAttrs = this.props.rows; - const value = pivotData.getAggregator(rowValues, colValues).value() + const value = pivotData.getAggregator(rowValues, colValues).value(); const filters = {}; const colLimit = Math.min(colAttrs.length, colValues.length); - for (let i = 0; i < colLimit;i++) { + for (let i = 0; i < colLimit; i++) { 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++) { + for (let i = 0; i < rowLimit; i++) { const attr = rowAttrs[i]; if (rowValues[i] !== null) { filters[attr] = rowValues[i]; } } - return e => this.props.tableOptions.clickCallback( - e, - value, - filters, - pivotData - ); + return e => + this.props.tableOptions.clickCallback(e, value, filters, pivotData); } collapseAttr(rowOrCol, attrIdx, allKeys) { @@ -180,14 +186,20 @@ function makeRenderer(opts = {}) { const collapsed = allKeys.filter(k => k.length === keyLen).map(flatKey); const updates = {}; - collapsed.forEach(k => {updates[k] = true;}); + collapsed.forEach(k => { + updates[k] = true; + }); if (rowOrCol) { - this.setState(state => ({collapsedRows: Object.assign({}, state.collapsedRows, updates)})); + this.setState(state => ({ + collapsedRows: Object.assign({}, state.collapsedRows, updates), + })); } else { - this.setState(state => ({collapsedCols: Object.assign({}, state.collapsedCols, updates)})); + this.setState(state => ({ + collapsedCols: Object.assign({}, state.collapsedCols, updates), + })); } - } + }; } expandAttr(rowOrCol, attrIdx, allKeys) { @@ -197,33 +209,41 @@ function makeRenderer(opts = {}) { const updates = {}; allKeys.forEach(k => { - for(let i = 0;i <= attrIdx;i++) { + for (let i = 0; i <= attrIdx; i++) { updates[flatKey(k.slice(0, i + 1))] = false; } }); if (rowOrCol) { - this.setState(state => ({collapsedRows: Object.assign({}, state.collapsedRows, updates)})); + this.setState(state => ({ + collapsedRows: Object.assign({}, state.collapsedRows, updates), + })); } else { - this.setState(state => ({collapsedCols: Object.assign({}, state.collapsedCols, updates)})); + this.setState(state => ({ + collapsedCols: Object.assign({}, state.collapsedCols, updates), + })); } - } + }; } toggleRowKey(flatRowKey) { return () => { - this.setState(state => ( - {collapsedRows: Object.assign({}, state.collapsedRows, {[flatRowKey]: !state.collapsedRows[flatRowKey]})} - )) - } + this.setState(state => ({ + collapsedRows: Object.assign({}, state.collapsedRows, { + [flatRowKey]: !state.collapsedRows[flatRowKey], + }), + })); + }; } toggleColKey(flatColKey) { return () => { - this.setState(state => ( - {collapsedCols: Object.assign({}, state.collapsedCols, {[flatColKey]: !state.collapsedCols[flatColKey]})} - )) - } + this.setState(state => ({ + collapsedCols: Object.assign({}, state.collapsedCols, { + [flatColKey]: !state.collapsedCols[flatColKey], + }), + })); + }; } calcAttrSpans(attrArr, numAttrs) { @@ -236,7 +256,7 @@ function makeRenderer(opts = {}) { // 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++) { + for (let i = 0; i < attrArr.length; i++) { // 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]; @@ -259,17 +279,26 @@ function makeRenderer(opts = {}) { return spans; } - static heatmapMappers(pivotData, colorScaleGenerator, colTotals, rowTotals) { + static heatmapMappers( + pivotData, + colorScaleGenerator, + colTotals, + rowTotals + ) { let valueCellColors = () => {}; let rowTotalColors = () => {}; let colTotalColors = () => {}; if (opts.heatmapMode) { if (colTotals) { - const colTotalValues = Object.values(pivotData.colTotals).map(a => a.value()); + const colTotalValues = Object.values(pivotData.colTotals).map(a => + a.value() + ); colTotalColors = colorScaleGenerator(colTotalValues); } if (rowTotals) { - const rowTotalValues = Object.values(pivotData.rowTotals).map(a => a.value()); + const rowTotalValues = Object.values(pivotData.rowTotals).map(a => + a.value() + ); rowTotalColors = colorScaleGenerator(rowTotalValues); } if (opts.heatmapMode === 'full') { @@ -322,42 +351,46 @@ function makeRenderer(opts = {}) { maxColVisible, } = pivotSettings; - const spaceCell = (attrIdx === 0 && rowAttrs.length !== 0) - ? ( ); const attrValueCells = []; - const rowIncrSpan = (rowAttrs.length !== 0) ? 1 : 0; + 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; + const colKey = visibleColKeys[i]; + const colSpan = attrIdx < colKey.length ? colAttrSpans[i][attrIdx] : 1; if (attrIdx < colKey.length) { - const rowSpan = 1 + ((attrIdx === colAttrs.length - 1) ? rowIncrSpan : 0); + const rowSpan = + 1 + (attrIdx === colAttrs.length - 1 ? rowIncrSpan : 0); const flatColKey = flatKey(colKey.slice(0, attrIdx + 1)); const onClick = needToggle ? this.toggleColKey(flatColKey) : null; attrValueCells.push( @@ -368,10 +401,14 @@ function makeRenderer(opts = {}) { rowSpan={rowSpan} onClick={onClick} > - {needToggle ? (this.state.collapsedCols[flatColKey] ? arrowCollapsed : arrowExpanded) + ' ' : null} + {needToggle + ? (this.state.collapsedCols[flatColKey] + ? arrowCollapsed + : arrowExpanded) + ' ' + : null} {colKey[attrIdx]} - ) + ); } else if (attrIdx === colKey.length) { const rowSpan = colAttrs.length - colKey.length + rowIncrSpan; attrValueCells.push( @@ -381,14 +418,14 @@ function makeRenderer(opts = {}) { colSpan={colSpan} rowSpan={rowSpan} /> - ) + ); } // The next colSpan columns will have the same value anyway... i = i + colSpan; - }; + } - const totalCell = (attrIdx === 0 && rowTotals) - ? ( + const totalCell = + attrIdx === 0 && rowTotals ? ( - ) - : null; + ) : null; - const cells = [ - spaceCell, - attrNameCell, - ...attrValueCells, - totalCell, - ]; + const cells = [spaceCell, attrNameCell, ...attrValueCells, totalCell]; return {cells}; } @@ -424,18 +455,19 @@ function makeRenderer(opts = {}) { return ( {rowAttrs.map((r, i) => { - const needLabelToggle = ( - opts.subtotals - && rowSubtotalDisplay.enabled - && i !== rowAttrs.length - 1 - ); + const needLabelToggle = + opts.subtotals && + rowSubtotalDisplay.enabled && + i !== rowAttrs.length - 1; let clickHandle = null; let subArrow = null; if (needLabelToggle) { - clickHandle = (i + 1 < maxRowVisible) - ? this.collapseAttr(true, i, rowKeys) - : this.expandAttr(true, i, rowKeys) - subArrow = ((i + 1 < maxRowVisible) ? arrowExpanded : arrowCollapsed) + ' '; + clickHandle = + i + 1 < maxRowVisible + ? this.collapseAttr(true, i, rowKeys) + : this.expandAttr(true, i, rowKeys); + subArrow = + (i + 1 < maxRowVisible ? arrowExpanded : arrowCollapsed) + ' '; } return ( ); })} @@ -474,13 +507,13 @@ function makeRenderer(opts = {}) { const flatRowKey = flatKey(rowKey); - const colIncrSpan = (colAttrs.length !== 0) ? 1 : 0 + const colIncrSpan = colAttrs.length !== 0 ? 1 : 0; const attrValueCells = rowKey.map((r, i) => { 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 && i !== rowAttrs.length - 1 + const colSpan = 1 + (i === rowAttrs.length - 1 ? colIncrSpan : 0); + const needRowToggle = opts.subtotals && i !== rowAttrs.length - 1; const onClick = needRowToggle ? this.toggleRowKey(flatRowKey) : null; return ( - ) + ); } return null; }); - const attrValuePaddingCell = (rowKey.length < rowAttrs.length) - ? ( + const attrValuePaddingCell = + rowKey.length < rowAttrs.length ? ( {rowCells}); + return {rowCells}; } renderTotalsRow(pivotSettings) { @@ -586,7 +622,7 @@ function makeRenderer(opts = {}) { return ( {totalCells}); + return {totalCells}; } visibleKeys(keys, collapsed, numAttrs, subtotalDisplay) { return keys.filter( - key => ( + 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 + !key.some((k, j) => collapsed[flatKey(key.slice(0, j))]) && + // Leaf key. + (key.length === numAttrs || // Children hidden. Must show total. - || flatKey(key) in collapsed + flatKey(key) in collapsed || // Don't hide totals. - || !subtotalDisplay.hideOnExpand - ) - ) - ) + !subtotalDisplay.hideOnExpand) + ); } render() { @@ -658,7 +687,7 @@ function makeRenderer(opts = {}) { rowKeys, this.state.collapsedRows, rowAttrs.length, - rowSubtotalDisplay, + rowSubtotalDisplay ) : rowKeys; const visibleColKeys = opts.subtotals @@ -666,26 +695,33 @@ function makeRenderer(opts = {}) { colKeys, this.state.collapsedCols, colAttrs.length, - colSubtotalDisplay, + colSubtotalDisplay ) : colKeys; - const pivotSettings = Object.assign({ - 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 pivotSettings = Object.assign( + { + 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 (
) - : null; + const spaceCell = + attrIdx === 0 && rowAttrs.length !== 0 ? ( + + ) : null; - const needToggle = ( - opts.subtotals - && colSubtotalDisplay.enabled - && attrIdx !== colAttrs.length - 1 - ); + const needToggle = + opts.subtotals && + colSubtotalDisplay.enabled && + attrIdx !== colAttrs.length - 1; let clickHandle = null; let subArrow = null; if (needToggle) { - clickHandle = (attrIdx + 1 < maxColVisible) - ? this.collapseAttr(false, attrIdx, colKeys) - : this.expandAttr(false, attrIdx, colKeys) - subArrow = ((attrIdx + 1 < maxColVisible) ? arrowExpanded : arrowCollapsed) + ' '; + clickHandle = + attrIdx + 1 < maxColVisible + ? this.collapseAttr(false, attrIdx, colKeys) + : this.expandAttr(false, attrIdx, colKeys); + subArrow = + (attrIdx + 1 < maxColVisible ? arrowExpanded : arrowCollapsed) + ' '; } const attrNameCell = ( - - {subArrow}{attrName} + + {subArrow} + {attrName} Totals
- {subArrow}{r} + {subArrow} + {r} - {needRowToggle ? (this.state.collapsedRows[flatRowKey] ? arrowCollapsed : arrowExpanded) + ' ': null} + {needRowToggle + ? (this.state.collapsedRows[flatRowKey] + ? arrowCollapsed + : arrowExpanded) + ' ' + : null} {r} - ) - : null; + ) : null; const rowClickHandlers = cellCallbacks[flatRowKey] || {}; const valueCells = visibleColKeys.map(colKey => { @@ -518,7 +554,7 @@ function makeRenderer(opts = {}) { return ( @@ -551,7 +587,7 @@ function makeRenderer(opts = {}) { totalCell, ]; - return (
@@ -610,30 +646,23 @@ function makeRenderer(opts = {}) { ); } - const totalCells = [ - totalLabelCell, - ...totalValueCells, - grandTotalCell, - ]; + const totalCells = [totalLabelCell, ...totalValueCells, grandTotalCell]; - return (
- {colAttrs.map((c, j) => this.renderColHeaderRow(c, j, pivotSettings))} + {colAttrs.map((c, j) => + this.renderColHeaderRow(c, j, pivotSettings) + )} {rowAttrs.length !== 0 && this.renderRowHeaderRow(pivotSettings)} - {visibleRowKeys.map((r, i) => this.renderTableRow(r, i, pivotSettings))} + {visibleRowKeys.map((r, i) => + this.renderTableRow(r, i, pivotSettings) + )} {colTotals && this.renderTotalsRow(pivotSettings)}
@@ -746,13 +782,22 @@ TSVExportRenderer.defaultProps = PivotData.defaultProps; TSVExportRenderer.propTypes = PivotData.propTypes; export default { - 'Table': makeRenderer(), + Table: makeRenderer(), 'Table Heatmap': makeRenderer({heatmapMode: 'full'}), 'Table Col Heatmap': makeRenderer({heatmapMode: 'col'}), 'Table Row Heatmap': makeRenderer({heatmapMode: 'row'}), 'Table With Subtotal': makeRenderer({subtotals: true}), - 'Table With Subtotal Heatmap': makeRenderer({heatmapMode: 'full', subtotals: true}), - 'Table With Subtotal Col Heatmap': makeRenderer({heatmapMode: 'col', subtotals: true}), - 'Table With Subtotal Row Heatmap': makeRenderer({heatmapMode: 'row', subtotals: true}), + 'Table With Subtotal Heatmap': makeRenderer({ + heatmapMode: 'full', + subtotals: true, + }), + 'Table With Subtotal Col Heatmap': makeRenderer({ + heatmapMode: 'col', + subtotals: true, + }), + 'Table With Subtotal Row Heatmap': makeRenderer({ + heatmapMode: 'row', + subtotals: true, + }), 'Exportable TSV': TSVExportRenderer, }; diff --git a/src/Utilities.js b/src/Utilities.js index e189eee..651c10a 100644 --- a/src/Utilities.js +++ b/src/Utilities.js @@ -524,7 +524,7 @@ const derivers = { // Given an array of attribute values, convert to a key that // can be used in objects. -const flatKey = (attrVals) => attrVals.join(String.fromCharCode(0)) +const flatKey = attrVals => attrVals.join(String.fromCharCode(0)); /* Data Model class @@ -619,7 +619,9 @@ class PivotData { this.rowKeys.sort((a, b) => -naturalSort(v(a, []), v(b, []))); break; default: - this.rowKeys.sort(this.arrSort(this.props.rows, this.subtotals.rowPartialOnTop)); + this.rowKeys.sort( + this.arrSort(this.props.rows, this.subtotals.rowPartialOnTop) + ); } switch (this.props.colOrder) { case 'value_a_to_z': @@ -629,7 +631,9 @@ class PivotData { this.colKeys.sort((a, b) => -naturalSort(v([], a), v([], b))); break; default: - this.colKeys.sort(this.arrSort(this.props.cols, this.subtotals.colPartialOnTop)); + this.colKeys.sort( + this.arrSort(this.props.cols, this.subtotals.colPartialOnTop) + ); } } } From 98852c3b302f73dabc3d2b0207f6fcb4bf0d90c5 Mon Sep 17 00:00:00 2001 From: Prakhar Goel Date: Fri, 14 Jun 2019 21:45:32 +0000 Subject: [PATCH 14/14] Added prepare script to make this project easier to use from GitHub directly. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 742c0cc..c3ad56c 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "test": "npm run test:eslint && npm run test:prettier && npm run test:jest", "clean": "rm -rf __tests__ PivotTable.js* PivotTableUI.js* PlotlyRenderers.js* TableRenderers.js* Utilities.js* pivottable.css", "build": "npm run clean && cp src/pivottable.css . && babel src --out-dir=. --source-maps --presets=env,react --plugins babel-plugin-add-module-exports", + "prepare": "npm run build", "doPublish": "npm run build && npm publish", "postpublish": "npm run clean", "deploy": "webpack -p && mv bundle.js examples && cd examples && git init && git add . && git commit -m build && git push --force git@github.com:plotly/react-pivottable.git master:gh-pages && rm -rf .git bundle.js"