diff --git a/examples/App.jsx b/examples/App.jsx
index ded8dc8..a7cbc44 100644
--- a/examples/App.jsx
+++ b/examples/App.jsx
@@ -1,9 +1,10 @@
import React from 'react';
import tips from './tips';
import {sortAs} from '../src/Utilities';
+import SubtotalRenderers from '../src/SubtotalRenderers';
import TableRenderers from '../src/TableRenderers';
-import createPlotlyComponent from 'react-plotly.js/factory';
import createPlotlyRenderers from '../src/PlotlyRenderers';
+import createPlotlyComponent from 'react-plotly.js/factory';
import PivotTableUI from '../src/PivotTableUI';
import '../src/pivottable.css';
import Dropzone from 'react-dropzone';
@@ -27,6 +28,7 @@ class PivotTableUISmartWrapper extends React.PureComponent {
renderers={Object.assign(
{},
TableRenderers,
+ SubtotalRenderers,
createPlotlyRenderers(Plot)
)}
{...this.state.pivotState}
@@ -44,11 +46,11 @@ export default class App extends React.Component {
filename: 'Sample Dataset: Tips',
pivotState: {
data: tips,
- rows: ['Payer Gender'],
- cols: ['Party Size'],
- aggregatorName: 'Sum over Sum',
- vals: ['Tip', 'Total Bill'],
- rendererName: 'Grouped Column Chart',
+ rows: ['Day of Week', 'Party Size'],
+ cols: ['Payer Gender', 'Meal'],
+ aggregatorName: 'Sum',
+ vals: ['Tip'],
+ rendererName: 'Table With Subtotal',
sorters: {
Meal: sortAs(['Lunch', 'Dinner']),
'Day of Week': sortAs([
@@ -68,7 +70,6 @@ export default class App extends React.Component {
) {
names.push(record.Meal);
});
- alert(names.join('\n'));
},
},
},
diff --git a/examples/index.jsx b/examples/index.jsx
index 0a4cf74..d2f446f 100644
--- a/examples/index.jsx
+++ b/examples/index.jsx
@@ -1,20 +1,19 @@
import React from 'react'
-import ReactDOM from 'react-dom'
-import { AppContainer } from 'react-hot-loader'
+import { createRoot } from 'react-dom/client'
import App from './App'
-const render = Component => {
- ReactDOM.render(
-
-
- ,
- document.getElementById('app'),
- )
-}
+// Create a root
+const container = document.getElementById('app')
+const root = createRoot(container)
-render(App)
+// Render the app directly without AppContainer
+root.render()
// Webpack Hot Module Replacement API
if (module.hot) {
- module.hot.accept('./App', () => { render(App) })
+ module.hot.accept('./App', () => {
+ // When App is updated, re-render
+ const NextApp = require('./App').default
+ root.render()
+ })
}
diff --git a/package.json b/package.json
index f4adaf6..3fd05df 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,7 @@
"PivotTableUI.js",
"PlotlyRenderers.js",
"TableRenderers.js",
+ "SubtotalsRenderers.js",
"Utilities.js",
"PivotTable.js.map",
"PivotTableUI.js.map",
diff --git a/src/PivotTableUI.jsx b/src/PivotTableUI.jsx
index 0de055f..461a411 100644
--- a/src/PivotTableUI.jsx
+++ b/src/PivotTableUI.jsx
@@ -210,6 +210,7 @@ export class Dropdown extends React.PureComponent {
this.props.toggle();
} else {
this.props.setValue(r);
+ this.props.toggle();
}
}}
className={
@@ -284,12 +285,40 @@ class PivotTableUI extends React.PureComponent {
this.setState(newState);
}
+ handleDuplicates(newAttributes, existingAttributes) {
+ if (!newAttributes || !existingAttributes) {
+ return existingAttributes || [];
+ }
+ const duplicates = newAttributes.filter(item =>
+ existingAttributes.includes(item)
+ );
+ return duplicates.length > 0
+ ? existingAttributes.filter(item => !duplicates.includes(item))
+ : existingAttributes;
+ }
+
sendPropUpdate(command) {
this.props.onChange(update(this.props, command));
}
propUpdater(key) {
- return value => this.sendPropUpdate({[key]: {$set: value}});
+ return value => {
+ const update = {[key]: {$set: value}};
+
+ if (key === 'rows') {
+ const updatedCols = this.handleDuplicates(value, this.props.cols);
+ if (updatedCols.length !== this.props.cols.length) {
+ update.cols = {$set: updatedCols};
+ }
+ } else if (key === 'cols') {
+ const updatedRows = this.handleDuplicates(value, this.props.rows);
+ if (updatedRows.length !== this.props.rows.length) {
+ update.rows = {$set: updatedRows};
+ }
+ }
+
+ this.sendPropUpdate(update);
+ };
}
setValuesInFilter(attribute, values) {
@@ -502,7 +531,6 @@ class PivotTableUI extends React.PureComponent {
!this.props.hiddenAttributes.includes(e) &&
!this.props.hiddenFromDragDrop.includes(e)
);
-
const colAttrsCell = this.makeDnDCell(
colAttrs,
this.propUpdater('cols'),
@@ -519,6 +547,7 @@ class PivotTableUI extends React.PureComponent {
this.propUpdater('rows'),
'pvtAxisContainer pvtVertList pvtRows'
);
+
const outputCell = (
{
+ // eslint-disable-next-line no-magic-numbers
+ const nonRed = 255 - Math.round((255 * (x - min)) / (max - min));
+ return {backgroundColor: `rgb(255,${nonRed},${nonRed})`};
+ };
+}
+
+function makeRenderer(opts = {}) {
+ class SubtotalRenderer extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {collapsedRows: {}, collapsedCols: {}};
+ }
+
+ componentDidMount() {
+ if (
+ opts.subtotals &&
+ !document.getElementById('react-pivottable-subtotal-styles')
+ ) {
+ const style = document.createElement('style');
+ style.id = 'react-pivottable-subtotal-styles';
+ style.innerHTML = `
+ .pvtSubtotal {
+ font-weight: bold;
+ background-color: #f0f0f0;
+ }
+ .pvtSubtotalRow {
+ border-top: 1px solid #ddd;
+ }
+ .pvtSubtotalVal {
+ color: #777;
+ font-style: italic;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ getBasePivotSettings() {
+ 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: 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,
+ }
+ );
+ const rowKeys = pivotData.getRowKeys();
+ const colKeys = pivotData.getColKeys();
+
+ const cellCallbacks = {};
+ const rowTotalCallbacks = {};
+ const colTotalCallbacks = {};
+ let grandTotalCallback = null;
+ if (tableOptions.clickCallback) {
+ rowKeys.forEach(rowKey => {
+ const flatRowKey = flatKey(rowKey);
+ cellCallbacks[flatRowKey] = {};
+ colKeys.forEach(colKey => {
+ const flatColKey = flatKey(colKey);
+ if (!(flatRowKey in cellCallbacks)) {
+ cellCallbacks[flatRowKey] = {};
+ }
+ cellCallbacks[flatRowKey][flatColKey] = this.clickHandler(
+ pivotData,
+ rowKey,
+ colKey
+ );
+ });
+ rowTotalCallbacks[flatRowKey] = this.clickHandler(
+ pivotData,
+ rowKey,
+ []
+ );
+ });
+ colKeys.forEach(colKey => {
+ const flatColKey = flatKey(colKey);
+ colTotalCallbacks[flatColKey] = this.clickHandler(
+ pivotData,
+ [],
+ colKey
+ );
+ });
+ grandTotalCallback = this.clickHandler(pivotData, [], []);
+ }
+
+ return Object.assign(
+ {
+ pivotData,
+ colAttrs,
+ rowAttrs,
+ colKeys,
+ rowKeys,
+ rowTotals,
+ colTotals,
+ arrowCollapsed: subtotalOptions.arrowCollapsed,
+ arrowExpanded: subtotalOptions.arrowExpanded,
+ colSubtotalDisplay,
+ rowSubtotalDisplay,
+ cellCallbacks,
+ rowTotalCallbacks,
+ colTotalCallbacks,
+ grandTotalCallback,
+ },
+ SubtotalRenderer.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++) {
+ 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) {
+ return function() {
+ var flatCollapseKeys = {};
+ for (var i = 0; i < allKeys.length; i++) {
+ var k = allKeys[i];
+ var slicedKey = k.slice(0, attrIdx + 1);
+ flatCollapseKeys[flatKey(slicedKey)] = true;
+ }
+ this.setState(function(prevState) {
+ if (rowOrCol === 'row') {
+ return {
+ collapsedRows: Object.assign(
+ {},
+ prevState.collapsedRows,
+ flatCollapseKeys
+ ),
+ };
+ } else if (rowOrCol === 'col') {
+ return {
+ collapsedCols: Object.assign(
+ {},
+ prevState.collapsedCols,
+ flatCollapseKeys
+ ),
+ };
+ }
+ return null;
+ });
+ }.bind(this);
+ }
+
+ expandAttr(rowOrCol, attrIdx, allKeys) {
+ return function() {
+ var flatCollapseKeys = {};
+ for (var i = 0; i < allKeys.length; i++) {
+ var k = allKeys[i];
+ var slicedKey = k.slice(0, attrIdx + 1);
+ flatCollapseKeys[flatKey(slicedKey)] = false;
+ }
+ this.setState(function(prevState) {
+ if (rowOrCol === 'row') {
+ return {
+ collapsedRows: Object.assign(
+ {},
+ prevState.collapsedRows,
+ flatCollapseKeys
+ ),
+ };
+ } else if (rowOrCol === 'col') {
+ return {
+ collapsedCols: Object.assign(
+ {},
+ prevState.collapsedCols,
+ flatCollapseKeys
+ ),
+ };
+ }
+ return null;
+ });
+ }.bind(this);
+ }
+
+ toggleRowKey(flatRowKey) {
+ return function() {
+ this.setState(function(prevState) {
+ var newCollapsedRows = Object.assign({}, prevState.collapsedRows);
+ newCollapsedRows[flatRowKey] = !prevState.collapsedRows[flatRowKey];
+ return {collapsedRows: newCollapsedRows};
+ });
+ }.bind(this);
+ }
+
+ toggleColKey(flatColKey) {
+ return function() {
+ this.setState(function(prevState) {
+ var newCollapsedCols = Object.assign({}, prevState.collapsedCols);
+ newCollapsedCols[flatColKey] = !prevState.collapsedCols[flatColKey];
+ return {collapsedCols: newCollapsedCols};
+ });
+ }.bind(this);
+ }
+
+ // 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
+ // each level.
+ calcAttrSpans(attrArr, numAttrs) {
+ const spans = [];
+ const li = Array(numAttrs).map(() => 0);
+ let lv = Array(numAttrs).map(() => null);
+ for (let i = 0; i < attrArr.length; i++) {
+ const cv = attrArr[i];
+ const isSubtotal = cv[cv.length - 1] === '__subtotal__';
+ const actualCv = isSubtotal ? cv.slice(0, -1) : cv;
+
+ const ent = [];
+ let depth = 0;
+ const limit = Math.min(lv.length, actualCv.length);
+ while (depth < limit && lv[depth] === actualCv[depth]) {
+ ent.push(-1);
+ spans[li[depth]][depth]++;
+ depth++;
+ }
+ while (depth < actualCv.length) {
+ li[depth] = i;
+ ent.push(1);
+ depth++;
+ }
+ spans.push(ent);
+ lv = actualCv;
+ }
+ return spans;
+ }
+
+ static heatmapMappers(
+ pivotData,
+ colorScaleGenerator,
+ colTotals,
+ rowTotals
+ ) {
+ const colMapper = {};
+ const rowMapper = {};
+
+ if (colorScaleGenerator && opts.heatmapMode) {
+ const valueCellColors = {};
+ const rowTotalColors = {};
+ const colTotalColors = {};
+ let grandTotalColor = null;
+
+ const allValues = [];
+ const rowValues = {};
+ const colValues = {};
+
+ pivotData.forEachCell((val, rowKey, colKey) => {
+ if (val !== null && !isNaN(val)) {
+ allValues.push(val);
+
+ const flatRow = flatKey(rowKey);
+ if (!rowValues[flatRow]) {
+ rowValues[flatRow] = [];
+ }
+ rowValues[flatRow].push(val);
+
+ const flatCol = flatKey(colKey);
+ if (!colValues[flatCol]) {
+ colValues[flatCol] = [];
+ }
+ colValues[flatCol].push(val);
+ }
+ });
+
+ if (opts.heatmapMode === 'row' && colTotals) {
+ pivotData.getRowKeys().forEach(rowKey => {
+ let rowTotal = 0;
+ let hasValidValues = false;
+ pivotData.getColKeys().forEach(colKey => {
+ const agg = pivotData.getAggregator(rowKey, colKey);
+ if (agg) {
+ const val = agg.value();
+ if (val !== null && !isNaN(val)) {
+ rowTotal += val;
+ hasValidValues = true;
+ }
+ }
+ });
+
+ if (hasValidValues && rowTotal !== 0) {
+ const flatRow = flatKey(rowKey);
+ if (!rowValues[flatRow]) {
+ rowValues[flatRow] = [];
+ }
+ rowValues[flatRow].push(rowTotal);
+ }
+ });
+ }
+
+ if (opts.heatmapMode === 'col' && rowTotals) {
+ pivotData.getColKeys().forEach(colKey => {
+ let colTotal = 0;
+ let hasValidValues = false;
+ pivotData.getRowKeys().forEach(rowKey => {
+ const agg = pivotData.getAggregator(rowKey, colKey);
+ if (agg) {
+ const val = agg.value();
+ if (val !== null && !isNaN(val)) {
+ colTotal += val;
+ hasValidValues = true;
+ }
+ }
+ });
+
+ if (hasValidValues && colTotal !== 0) {
+ const flatCol = flatKey(colKey);
+ if (!colValues[flatCol]) {
+ colValues[flatCol] = [];
+ }
+ colValues[flatCol].push(colTotal);
+ }
+ });
+ }
+
+ if (colTotals) {
+ const rowTotalValues = [];
+ pivotData.forEachTotal(([valKey, _x]) => {
+ const val = pivotData.getAggregator([valKey], []).value();
+ if (val !== null && !isNaN(val)) {
+ rowTotalValues.push(val);
+ if (opts.heatmapMode === 'full') {
+ allValues.push(val);
+ }
+ }
+ });
+
+ const rowTotalColorScale =
+ opts.heatmapMode === 'full'
+ ? colorScaleGenerator(allValues)
+ : colorScaleGenerator(rowTotalValues);
+
+ pivotData.forEachTotal(([valKey, _x]) => {
+ const val = pivotData.getAggregator([valKey], []).value();
+ if (val !== null && !isNaN(val)) {
+ rowTotalColors[flatKey([valKey])] = rowTotalColorScale(val);
+ }
+ });
+ }
+
+ if (rowTotals) {
+ const colTotalValues = [];
+ pivotData.forEachTotal(([_x, valKey]) => {
+ const val = pivotData.getAggregator([], [valKey]).value();
+ if (val !== null && !isNaN(val)) {
+ colTotalValues.push(val);
+ if (opts.heatmapMode === 'full') {
+ allValues.push(val);
+ }
+ }
+ });
+
+ const colTotalColorScale =
+ opts.heatmapMode === 'full'
+ ? colorScaleGenerator(allValues)
+ : colorScaleGenerator(colTotalValues);
+
+ pivotData.forEachTotal(([_x, valKey]) => {
+ const val = pivotData.getAggregator([], [valKey]).value();
+ if (val !== null && !isNaN(val)) {
+ colTotalColors[flatKey([valKey])] = colTotalColorScale(val);
+ }
+ });
+ }
+
+ if (colTotals && rowTotals) {
+ const grandTotalVal = pivotData.getAggregator([], []).value();
+ if (grandTotalVal !== null && !isNaN(grandTotalVal)) {
+ if (opts.heatmapMode === 'full') {
+ allValues.push(grandTotalVal);
+ const grandTotalColorScale = colorScaleGenerator(allValues);
+ grandTotalColor = grandTotalColorScale(grandTotalVal);
+ }
+ }
+ }
+
+ if (rowTotals) {
+ colMapper.totalColor = key => colTotalColors[flatKey([key])];
+ }
+ if (colTotals) {
+ rowMapper.totalColor = key => rowTotalColors[flatKey([key])];
+ }
+ if (grandTotalColor) {
+ colMapper.grandTotalColor = grandTotalColor;
+ }
+
+ if (opts.heatmapMode === 'full') {
+ // Full heatmap: Compare values across the entire table
+ // Note: allValues already contains all cell values from earlier collection
+ const colorScale = colorScaleGenerator(allValues);
+ pivotData.forEachCell((val, rowKey, colKey) => {
+ if (val !== null && !isNaN(val)) {
+ valueCellColors[
+ `${flatKey(rowKey)}_${flatKey(colKey)}`
+ ] = colorScale(val);
+ }
+ });
+
+ colMapper.bgColorFromRowColKey = (rowKey, colKey) =>
+ valueCellColors[`${flatKey(rowKey)}_${flatKey(colKey)}`];
+
+ colMapper.bgColorFromSubtotalValue = value => {
+ if (value !== null && !isNaN(value)) {
+ return colorScale(value);
+ }
+ return null;
+ };
+ } else if (opts.heatmapMode === 'row') {
+ // Row heatmap: Compare values within each row
+ // Note: rowValues already populated from earlier collection
+ const rowColorScales = {};
+ Object.entries(rowValues).forEach(([flatRow, values]) => {
+ if (values.length > 0) {
+ rowColorScales[flatRow] = colorScaleGenerator(values);
+ }
+ });
+
+ pivotData.forEachCell((val, rowKey, colKey) => {
+ const flatRow = flatKey(rowKey);
+ if (val !== null && !isNaN(val) && rowColorScales[flatRow]) {
+ valueCellColors[`${flatRow}_${flatKey(colKey)}`] = rowColorScales[
+ flatRow
+ ](val);
+ }
+ });
+
+ colMapper.bgColorFromRowColKey = (rowKey, colKey) =>
+ valueCellColors[`${flatKey(rowKey)}_${flatKey(colKey)}`];
+
+ colMapper.bgColorFromSubtotalValue = (value, rowKey) => {
+ if (value !== null && !isNaN(value)) {
+ const flatRow = flatKey(rowKey);
+ if (rowColorScales[flatRow]) {
+ return rowColorScales[flatRow](value);
+ }
+ }
+ return null;
+ };
+ } else if (opts.heatmapMode === 'col') {
+ // Column heatmap: Compare values within each column
+ // Note: colValues already populated from earlier collection
+ const colColorScales = {};
+ Object.entries(colValues).forEach(([flatCol, values]) => {
+ if (values.length > 0) {
+ colColorScales[flatCol] = colorScaleGenerator(values);
+ }
+ });
+
+ pivotData.forEachCell((val, rowKey, colKey) => {
+ const flatCol = flatKey(colKey);
+ if (val !== null && !isNaN(val) && colColorScales[flatCol]) {
+ valueCellColors[`${flatKey(rowKey)}_${flatCol}`] = colColorScales[
+ flatCol
+ ](val);
+ }
+ });
+
+ colMapper.bgColorFromRowColKey = (rowKey, colKey) =>
+ valueCellColors[`${flatKey(rowKey)}_${flatKey(colKey)}`];
+
+ colMapper.bgColorFromSubtotalValue = (value, rowKey, colKey) => {
+ if (value !== null && !isNaN(value)) {
+ const flatCol = flatKey(colKey);
+ if (colColorScales[flatCol]) {
+ return colColorScales[flatCol](value);
+ }
+ }
+ return null;
+ };
+ }
+ }
+ return {colMapper, rowMapper};
+ }
+
+ renderColHeaderRow(attrName, attrIdx, pivotSettings) {
+ const {
+ rowAttrs,
+ colAttrs,
+ visibleColKeys,
+ colAttrSpans,
+ rowTotals,
+ arrowExpanded,
+ arrowCollapsed,
+ colSubtotalDisplay,
+ } = pivotSettings;
+
+ const spaceCell =
+ attrIdx === 0 && rowAttrs.length !== 0 ? (
+ |
+ ) : null;
+
+ const needToggle =
+ opts.subtotals &&
+ colSubtotalDisplay.enabled &&
+ attrIdx !== colAttrs.length - 1;
+ const attrNameCell = (
+
+ {attrName}
+ |
+ );
+
+ const attrValueCells = [];
+ const rowIncrSpan = rowAttrs.length !== 0 ? 1 : 0;
+ let i = 0;
+ while (i < visibleColKeys.length) {
+ const colKey = visibleColKeys[i];
+ const isSubtotalCol = colKey[colKey.length - 1] === '__subtotal__';
+ const actualColKey = isSubtotalCol ? colKey.slice(0, -1) : colKey;
+
+ const colSpan =
+ attrIdx < actualColKey.length ? colAttrSpans[i][attrIdx] : 1;
+ if (attrIdx < actualColKey.length) {
+ const rowSpan =
+ 1 + (attrIdx === colAttrs.length - 1 ? rowIncrSpan : 0);
+ const flatColKey = flatKey(actualColKey.slice(0, attrIdx + 1));
+ const onClick = needToggle ? this.toggleColKey(flatColKey) : null;
+
+ let headerText = actualColKey[attrIdx];
+ let headerClass = 'pvtColLabel';
+
+ const isCollapsedParent =
+ this.state.collapsedCols[flatColKey] &&
+ actualColKey.length < colAttrs.length;
+
+ if (isSubtotalCol) {
+ headerText = `${headerText} (Subtotal)`;
+ headerClass += ' pvtSubtotal';
+ } else if (isCollapsedParent) {
+ headerClass += ' pvtSubtotal';
+ }
+
+ attrValueCells.push(
+
+ {needToggle
+ ? (this.state.collapsedCols[flatColKey]
+ ? arrowCollapsed
+ : arrowExpanded) + ' '
+ : null}
+ {headerText}
+ |
+ );
+ } else if (attrIdx === actualColKey.length) {
+ const rowSpan = colAttrs.length - actualColKey.length + rowIncrSpan;
+ const flatColKey = flatKey(actualColKey);
+ const isCollapsedParent =
+ this.state.collapsedCols[flatColKey] &&
+ actualColKey.length < colAttrs.length;
+
+ attrValueCells.push(
+ |
+ );
+ }
+ i = i + colSpan;
+ }
+
+ const totalCell =
+ attrIdx === 0 && rowTotals ? (
+
+ Totals
+ |
+ ) : null;
+
+ const cells = [spaceCell, attrNameCell, ...attrValueCells, totalCell];
+ return cells;
+ }
+
+ renderRowHeaderRow(pivotSettings) {
+ const {colAttrs, rowAttrs} = pivotSettings;
+ const cells = [];
+ if (rowAttrs.length !== 0) {
+ rowAttrs.map(function(r, i) {
+ cells.push(
+
+ {r}
+ |
+ );
+ });
+ cells.push(
+
+ {colAttrs.length === 0 ? 'Totals' : null}
+ |
+ );
+ }
+ return cells;
+ }
+
+ renderTableRow(rowKey, rowIdx, pivotSettings) {
+ const {
+ colKeys,
+ rowAttrs,
+ colAttrs,
+ rowTotals,
+ pivotData,
+ rowMapper,
+ colMapper,
+ cellCallbacks,
+ rowTotalCallbacks,
+ } = pivotSettings;
+
+ const visibleColKeys = this.visibleKeys(
+ colKeys,
+ this.state.collapsedCols
+ );
+
+ const cells = [];
+ const isSubtotalRow = rowKey[rowKey.length - 1] === '__subtotal__';
+ const actualRowKey = isSubtotalRow ? rowKey.slice(0, -1) : rowKey;
+
+ visibleColKeys.forEach((colKey, i) => {
+ try {
+ if (!actualRowKey || !colKey) {
+ cells.push( | );
+ return;
+ }
+
+ let aggregator,
+ className,
+ valCss = {};
+
+ const isSubtotalCol = colKey[colKey.length - 1] === '__subtotal__';
+ const actualColKey = isSubtotalCol ? colKey.slice(0, -1) : colKey;
+
+ const needsSubtotalValue =
+ isSubtotalRow ||
+ isSubtotalCol ||
+ (actualColKey.length < colAttrs.length &&
+ this.state.collapsedCols[flatKey(actualColKey)]) ||
+ (actualRowKey.length < rowAttrs.length &&
+ this.state.collapsedRows[flatKey(actualRowKey)]);
+
+ if (needsSubtotalValue) {
+ const value = this.calculateSubtotal(
+ pivotData,
+ actualRowKey,
+ actualColKey,
+ pivotSettings
+ );
+ className = 'pvtSubtotal';
+
+ const tempAggregator = this.safeGetAggregator(pivotData, [], []);
+ aggregator = {
+ value: () => value,
+ format: tempAggregator ? tempAggregator.format : x => x,
+ };
+
+ if (opts.heatmapMode && colMapper.bgColorFromSubtotalValue) {
+ let cellColor;
+ if (opts.heatmapMode === 'full') {
+ cellColor = colMapper.bgColorFromSubtotalValue(value);
+ } else if (opts.heatmapMode === 'row') {
+ cellColor = colMapper.bgColorFromSubtotalValue(
+ value,
+ actualRowKey
+ );
+ } else if (opts.heatmapMode === 'col') {
+ cellColor = colMapper.bgColorFromSubtotalValue(
+ value,
+ actualRowKey,
+ actualColKey
+ );
+ }
+
+ if (cellColor) {
+ valCss = cellColor;
+ }
+ }
+ } else {
+ aggregator = this.safeGetAggregator(
+ pivotData,
+ actualRowKey,
+ actualColKey
+ );
+ className = 'pvtVal';
+
+ if (opts.heatmapMode && colMapper.bgColorFromRowColKey) {
+ const cellColor = colMapper.bgColorFromRowColKey(
+ actualRowKey,
+ actualColKey
+ );
+ if (cellColor) {
+ valCss = cellColor;
+ }
+ }
+ }
+
+ if (!aggregator || aggregator.value() === null) {
+ cells.push(
+ |
+ );
+ return;
+ }
+
+ const val = aggregator.value();
+
+ let formattedVal;
+ if (val === null) {
+ formattedVal = '';
+ } else if (className === 'pvtSubtotal' && val === 0) {
+ formattedVal = '';
+ } else {
+ formattedVal = aggregator.format(val);
+ }
+
+ const cellKey = flatKey(actualRowKey);
+ const colCellKey = flatKey(actualColKey);
+
+ cells.push(
+
+ {formattedVal}
+ |
+ );
+ } catch (error) {
+ cells.push( | );
+ }
+ });
+
+ if (rowTotals) {
+ try {
+ const className = isSubtotalRow ? 'pvtTotal pvtSubtotal' : 'pvtTotal';
+ let valCss = {};
+
+ let totalVal = 0;
+ let formattedTotal = '';
+
+ if (isSubtotalRow) {
+ totalVal = this.calculateSubtotal(
+ pivotData,
+ actualRowKey,
+ [],
+ pivotSettings
+ );
+
+ if (opts.heatmapMode && colMapper.bgColorFromSubtotalValue) {
+ let cellColor;
+ if (opts.heatmapMode === 'full') {
+ cellColor = colMapper.bgColorFromSubtotalValue(totalVal);
+ } else if (opts.heatmapMode === 'row') {
+ cellColor = colMapper.bgColorFromSubtotalValue(
+ totalVal,
+ actualRowKey
+ );
+ } else if (opts.heatmapMode === 'col') {
+ cellColor = colMapper.bgColorFromSubtotalValue(
+ totalVal,
+ actualRowKey,
+ []
+ );
+ }
+
+ if (cellColor) {
+ valCss = cellColor;
+ }
+ }
+ } else if (
+ actualRowKey.length < rowAttrs.length &&
+ this.state.collapsedRows[flatKey(actualRowKey)]
+ ) {
+ totalVal = this.calculateSubtotal(
+ pivotData,
+ actualRowKey,
+ [],
+ pivotSettings
+ );
+
+ if (opts.heatmapMode && colMapper.bgColorFromSubtotalValue) {
+ let cellColor;
+ if (opts.heatmapMode === 'full') {
+ cellColor = colMapper.bgColorFromSubtotalValue(totalVal);
+ } else if (opts.heatmapMode === 'row') {
+ cellColor = colMapper.bgColorFromSubtotalValue(
+ totalVal,
+ actualRowKey
+ );
+ } else if (opts.heatmapMode === 'col') {
+ cellColor = colMapper.bgColorFromSubtotalValue(
+ totalVal,
+ actualRowKey,
+ []
+ );
+ }
+
+ if (cellColor) {
+ valCss = cellColor;
+ }
+ }
+ } else {
+ pivotData.getColKeys().forEach(colKey => {
+ const agg = this.safeGetAggregator(
+ pivotData,
+ actualRowKey,
+ colKey
+ );
+ if (agg) {
+ const val = agg.value();
+ if (val !== null && !isNaN(val)) {
+ totalVal += val;
+ }
+ }
+ });
+
+ if (opts.heatmapMode && totalVal !== 0) {
+ if (
+ opts.heatmapMode === 'row' &&
+ colMapper.bgColorFromSubtotalValue
+ ) {
+ const cellColor = colMapper.bgColorFromSubtotalValue(
+ totalVal,
+ actualRowKey
+ );
+ if (cellColor) {
+ valCss = cellColor;
+ }
+ } else if (rowMapper.totalColor) {
+ const cellColor = rowMapper.totalColor(actualRowKey[0]);
+ if (cellColor) {
+ valCss = cellColor;
+ }
+ }
+ }
+ }
+
+ if (totalVal !== 0 || isSubtotalRow) {
+ const tempAggregator = this.safeGetAggregator(pivotData, [], []);
+ const formatFunc =
+ tempAggregator && tempAggregator.format
+ ? tempAggregator.format
+ : x => x;
+ if (className.includes('pvtSubtotal') && totalVal === 0) {
+ formattedTotal = '';
+ } else {
+ formattedTotal =
+ totalVal === null || totalVal === 0 ? '' : formatFunc(totalVal);
+ }
+ }
+
+ const cellKey = flatKey(actualRowKey);
+
+ cells.push(
+
+ {formattedTotal}
+ |
+ );
+ } catch (error) {
+ cells.push( | );
+ }
+ }
+
+ return cells;
+ }
+
+ renderTotalsRow(pivotSettings) {
+ const {
+ colKeys,
+ colAttrs,
+ rowAttrs,
+ colTotals,
+ pivotData,
+ colMapper,
+ grandTotalCallback,
+ colTotalCallbacks,
+ } = pivotSettings;
+
+ const visibleColKeys = this.visibleKeys(
+ colKeys,
+ this.state.collapsedCols
+ );
+
+ const cells = [];
+ cells.push(
+
+ Totals
+ |
+ );
+
+ visibleColKeys.forEach((colKey, i) => {
+ try {
+ const isSubtotalCol = colKey[colKey.length - 1] === '__subtotal__';
+ const actualColKey = isSubtotalCol ? colKey.slice(0, -1) : colKey;
+
+ if (!actualColKey) {
+ cells.push( | );
+ return;
+ }
+
+ let colTotal = 0;
+
+ const flatColKey = flatKey(actualColKey);
+ const isCollapsedParent =
+ this.state.collapsedCols[flatColKey] &&
+ actualColKey.length < colAttrs.length;
+
+ if (isSubtotalCol || isCollapsedParent) {
+ colTotal = this.calculateSubtotal(
+ pivotData,
+ [],
+ actualColKey,
+ pivotSettings
+ );
+ } else {
+ pivotData.getRowKeys().forEach(rowKey => {
+ const agg = this.safeGetAggregator(
+ pivotData,
+ rowKey,
+ actualColKey
+ );
+ if (agg) {
+ const val = agg.value();
+ if (val !== null && !isNaN(val)) {
+ colTotal += val;
+ }
+ }
+ });
+ }
+
+ let valCss = {};
+ if (isSubtotalCol || isCollapsedParent) {
+ if (opts.heatmapMode && colMapper.bgColorFromSubtotalValue) {
+ let cellColor;
+ if (opts.heatmapMode === 'full') {
+ cellColor = colMapper.bgColorFromSubtotalValue(colTotal);
+ } else if (opts.heatmapMode === 'row') {
+ cellColor = colMapper.bgColorFromSubtotalValue(colTotal, []);
+ } else if (opts.heatmapMode === 'col') {
+ cellColor = colMapper.bgColorFromSubtotalValue(
+ colTotal,
+ [],
+ actualColKey
+ );
+ }
+
+ if (cellColor) {
+ valCss = cellColor;
+ }
+ }
+ } else {
+ if (opts.heatmapMode && colTotal !== 0) {
+ if (
+ opts.heatmapMode === 'col' &&
+ colMapper.bgColorFromSubtotalValue
+ ) {
+ const cellColor = colMapper.bgColorFromSubtotalValue(
+ colTotal,
+ [],
+ actualColKey
+ );
+ if (cellColor) {
+ valCss = cellColor;
+ }
+ } else if (colMapper.totalColor) {
+ const cellColor = colMapper.totalColor(actualColKey[0]);
+ if (cellColor) {
+ valCss = cellColor;
+ }
+ }
+ }
+ }
+
+ const tempAggregator = this.safeGetAggregator(pivotData, [], []);
+ const format =
+ tempAggregator && tempAggregator.format
+ ? tempAggregator.format
+ : x => x;
+
+ let displayValue;
+ if (colTotal === null || colTotal === 0) {
+ displayValue = '';
+ } else {
+ displayValue = format ? format(colTotal) : colTotal;
+ }
+
+ cells.push(
+
+ {displayValue}
+ |
+ );
+ } catch (error) {
+ cells.push( | );
+ }
+ });
+
+ if (colTotals) {
+ try {
+ let grandTotal = 0;
+ let validValuesFound = false;
+
+ try {
+ const grandTotalAggregator = pivotData.getAggregator([], []);
+ if (grandTotalAggregator) {
+ const val = grandTotalAggregator.value();
+ if (val !== null && !isNaN(val)) {
+ grandTotal = val;
+ validValuesFound = true;
+ }
+ }
+ } catch (e) {
+ // Error getting grand total directly, will calculate manually
+ }
+
+ if (!validValuesFound) {
+ pivotData.getRowKeys().forEach(rowKey => {
+ pivotData.getColKeys().forEach(colKey => {
+ try {
+ const agg = this.safeGetAggregator(pivotData, rowKey, colKey);
+ if (agg) {
+ const val = agg.value();
+ if (val !== null && !isNaN(val)) {
+ grandTotal += val;
+ validValuesFound = true;
+ }
+ }
+ } catch (e) {
+ // Ignore errors for missing combinations
+ }
+ });
+ });
+ }
+
+ const tempAggregator = this.safeGetAggregator(pivotData, [], []);
+ const format =
+ tempAggregator && tempAggregator.format
+ ? tempAggregator.format
+ : x => x;
+
+ cells.push(
+
+ {validValuesFound && grandTotal !== 0
+ ? format
+ ? format(grandTotal)
+ : grandTotal
+ : ''}
+ |
+ );
+ } catch (error) {
+ cells.push( | );
+ }
+ }
+
+ return cells;
+ }
+
+ visibleKeys(keys, collapsed) {
+ if (!opts.subtotals) {
+ return keys;
+ }
+
+ const sortedKeys = keys.slice().sort((a, b) => {
+ const minLength = Math.min(a.length, b.length);
+ for (let i = 0; i < minLength; i++) {
+ const aStr = String(a[i]);
+ const bStr = String(b[i]);
+ const cmp = aStr.localeCompare(bStr);
+ if (cmp !== 0) {
+ return cmp;
+ }
+ }
+ return a.length - b.length;
+ });
+
+ const result = [];
+ const processedKeys = new Set();
+
+ for (const key of sortedKeys) {
+ let parentCollapsed = false;
+ let deepestCollapsedParent = null;
+
+ for (let i = 0; i < key.length; i++) {
+ const parentKey = key.slice(0, i + 1);
+ const flatParentKey = flatKey(parentKey);
+ if (collapsed[flatParentKey]) {
+ parentCollapsed = true;
+ deepestCollapsedParent = parentKey;
+ break;
+ }
+ }
+
+ if (parentCollapsed) {
+ const flatParentKey = flatKey(deepestCollapsedParent);
+ if (!processedKeys.has(flatParentKey)) {
+ result.push(deepestCollapsedParent);
+ processedKeys.add(flatParentKey);
+ }
+ } else {
+ const flatKey_ = flatKey(key);
+ if (!processedKeys.has(flatKey_)) {
+ result.push(key);
+ processedKeys.add(flatKey_);
+ }
+ }
+ }
+
+ const finalResult = [];
+ const addedSubtotals = new Set();
+
+ const parentGroups = new Map();
+ for (const key of result) {
+ for (let level = 1; level < key.length; level++) {
+ const parentKey = key.slice(0, level);
+ const parentKeyStr = flatKey(parentKey);
+
+ if (!parentGroups.has(parentKeyStr)) {
+ parentGroups.set(parentKeyStr, {
+ key: parentKey,
+ level: level,
+ lastChildIndex: -1,
+ });
+ }
+ }
+ }
+
+ for (let i = 0; i < result.length; i++) {
+ const key = result[i];
+ for (let level = 1; level < key.length; level++) {
+ const parentKey = key.slice(0, level);
+ const parentKeyStr = flatKey(parentKey);
+ const parentGroup = parentGroups.get(parentKeyStr);
+
+ if (parentGroup) {
+ parentGroup.lastChildIndex = Math.max(
+ parentGroup.lastChildIndex,
+ i
+ );
+ }
+ }
+ }
+
+ for (let i = 0; i < result.length; i++) {
+ const key = result[i];
+ finalResult.push(key);
+
+ const subtotalsToAdd = [];
+
+ for (let level = key.length - 1; level >= 1; level--) {
+ const parentKey = key.slice(0, level);
+ const parentKeyStr = flatKey(parentKey);
+ const parentGroup = parentGroups.get(parentKeyStr);
+
+ if (collapsed[parentKeyStr]) {
+ continue;
+ }
+
+ if (parentGroup && parentGroup.lastChildIndex === i) {
+ const subtotalKey = [...parentKey, '__subtotal__'];
+ const subtotalKeyStr = flatKey(subtotalKey);
+
+ if (!addedSubtotals.has(subtotalKeyStr)) {
+ subtotalsToAdd.push(subtotalKey);
+ addedSubtotals.add(subtotalKeyStr);
+ }
+ }
+ }
+
+ finalResult.push(...subtotalsToAdd);
+ }
+
+ return finalResult;
+ }
+
+ getSubtotal(rowKey, colKey, pivotSettings) {
+ const {pivotData} = pivotSettings;
+ return pivotData.getAggregator(rowKey, colKey).value();
+ }
+
+ hasSubtotals(rowOrCol, key, pivotSettings) {
+ const {rowAttrs, colAttrs} = pivotSettings;
+ const attrs = rowOrCol === 'row' ? rowAttrs : colAttrs;
+
+ return key.length < attrs.length;
+ }
+
+ safeGetAggregator(pivotData, rowKey, colKey) {
+ try {
+ return pivotData.getAggregator(rowKey, colKey);
+ } catch (error) {
+ return null;
+ }
+ }
+
+ calculateSubtotal(pivotData, rowKey, colKey, pivotSettings) {
+ const {rowAttrs, colAttrs} = pivotSettings;
+
+ if (
+ rowKey.length === rowAttrs.length &&
+ colKey.length === colAttrs.length
+ ) {
+ const agg = this.safeGetAggregator(pivotData, rowKey, colKey);
+ return agg ? agg.value() : 0;
+ }
+
+ let total = 0;
+ let hasValidValues = false;
+
+ const childRowKeys = [];
+ if (rowKey.length < rowAttrs.length) {
+ pivotData.getRowKeys().forEach(fullRowKey => {
+ let isChild = true;
+ for (let i = 0; i < rowKey.length; i++) {
+ if (fullRowKey[i] !== rowKey[i]) {
+ isChild = false;
+ break;
+ }
+ }
+
+ if (isChild) {
+ childRowKeys.push(fullRowKey);
+ }
+ });
+ } else {
+ childRowKeys.push(rowKey);
+ }
+
+ const childColKeys = [];
+ if (colKey.length < colAttrs.length) {
+ pivotData.getColKeys().forEach(fullColKey => {
+ let isChild = true;
+ for (let i = 0; i < colKey.length; i++) {
+ if (fullColKey[i] !== colKey[i]) {
+ isChild = false;
+ break;
+ }
+ }
+
+ if (isChild) {
+ childColKeys.push(fullColKey);
+ }
+ });
+ } else {
+ childColKeys.push(colKey);
+ }
+
+ if (childRowKeys.length === 0 || childColKeys.length === 0) {
+ const agg = this.safeGetAggregator(pivotData, rowKey, colKey);
+ return agg ? agg.value() || 0 : 0;
+ }
+
+ childRowKeys.forEach(childRowKey => {
+ childColKeys.forEach(childColKey => {
+ const agg = this.safeGetAggregator(
+ pivotData,
+ childRowKey,
+ childColKey
+ );
+ if (agg) {
+ const val = agg.value();
+ if (val !== null && !isNaN(val)) {
+ total += val;
+ hasValidValues = true;
+ }
+ }
+ });
+ });
+
+ return hasValidValues ? total : 0;
+ }
+
+ render() {
+ const pivotSettings = this.getBasePivotSettings();
+ const {colAttrs, rowAttrs, rowKeys, colKeys, rowTotals} = pivotSettings;
+
+ const renderedLabels = {};
+
+ const visibleRowKeys = opts.subtotals
+ ? this.visibleKeys(rowKeys, this.state.collapsedRows)
+ : rowKeys;
+ const visibleColKeys = opts.subtotals
+ ? this.visibleKeys(colKeys, this.state.collapsedCols)
+ : colKeys;
+
+ const finalPivotSettings = 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),
+ },
+ pivotSettings
+ );
+
+ const rowspans = {};
+ visibleRowKeys.forEach((rowKey, rowIdx) => {
+ const isSubtotalRow = rowKey[rowKey.length - 1] === '__subtotal__';
+ const actualRowKey = isSubtotalRow ? rowKey.slice(0, -1) : rowKey;
+
+ for (let level = 0; level < actualRowKey.length; level++) {
+ const cellKey = `${rowIdx}-${level}`;
+
+ let span = 1;
+ let j = rowIdx + 1;
+ while (j < visibleRowKeys.length) {
+ const nextKey = visibleRowKeys[j];
+ const isNextSubtotal =
+ nextKey[nextKey.length - 1] === '__subtotal__';
+ const actualNextKey = isNextSubtotal
+ ? nextKey.slice(0, -1)
+ : nextKey;
+
+ if (level >= actualNextKey.length) {
+ break;
+ }
+
+ let matches = true;
+ for (let l = 0; l <= level; l++) {
+ if (
+ l >= actualNextKey.length ||
+ actualNextKey[l] !== actualRowKey[l]
+ ) {
+ matches = false;
+ break;
+ }
+ }
+
+ if (!matches) {
+ break;
+ }
+ span++;
+ j++;
+ }
+
+ rowspans[cellKey] = span;
+ }
+ });
+
+ const renderedRows = visibleRowKeys.map((rowKey, i) => {
+ const rowCells = [];
+
+ const isSubtotalRow = rowKey[rowKey.length - 1] === '__subtotal__';
+ const actualRowKey = isSubtotalRow ? rowKey.slice(0, -1) : rowKey;
+
+ if (isSubtotalRow) {
+ rowCells.push(
+ |
+ );
+ } else {
+ for (let level = 0; level < actualRowKey.length; level++) {
+ const labelKey = `${actualRowKey.slice(0, level + 1).join('|')}`;
+
+ if (!renderedLabels[labelKey]) {
+ renderedLabels[labelKey] = true;
+
+ const cellKey = `${i}-${level}`;
+ const rowspan = rowspans[cellKey] || 1;
+
+ const flatRowKey = flatKey(actualRowKey.slice(0, level + 1));
+ const isCollapsed = this.state.collapsedRows[flatRowKey];
+
+ let className = 'pvtRowLabel';
+
+ let icon = null;
+
+ if (level + 1 < rowAttrs.length) {
+ if (isCollapsed) {
+ className += ' collapsed';
+ icon = pivotSettings.arrowCollapsed;
+ } else {
+ className += ' expanded';
+ icon = pivotSettings.arrowExpanded;
+ }
+ rowCells.push(
+
+ {icon && (
+
+ {icon}
+
+ )}
+ {actualRowKey[level]}
+ |
+ );
+ continue;
+ }
+
+ const isLeafLevel =
+ level === actualRowKey.length - 1 &&
+ actualRowKey.length === rowAttrs.length;
+ const leafColspan = isLeafLevel ? 2 : 1;
+
+ rowCells.push(
+
+ {icon && {icon}}
+ {actualRowKey[level]}
+ |
+ );
+ }
+ }
+
+ if (actualRowKey.length < rowAttrs.length) {
+ rowCells.push(
+ |
+ );
+ }
+ }
+
+ const dataCells = this.renderTableRow(rowKey, i, finalPivotSettings);
+
+ return (
+
+ {rowCells}
+ {dataCells}
+
+ );
+ });
+
+ const colAttrsHeaders = colAttrs.map((attrName, i) => {
+ return (
+
+ {this.renderColHeaderRow(attrName, i, finalPivotSettings)}
+
+ );
+ });
+
+ let rowAttrsHeader = null;
+ if (rowAttrs.length > 0) {
+ rowAttrsHeader = (
+ {this.renderRowHeaderRow(finalPivotSettings)}
+ );
+ }
+
+ let totalHeader = null;
+ if (rowTotals) {
+ totalHeader = (
+ {this.renderTotalsRow(finalPivotSettings)}
+ );
+ }
+
+ return (
+
+
+ {colAttrsHeaders}
+ {rowAttrsHeader}
+
+
+ {renderedRows}
+ {totalHeader}
+
+
+ );
+ }
+ }
+
+ SubtotalRenderer.defaultProps = Object.assign({}, PivotData.defaultProps, {
+ tableColorScaleGenerator: redColorScaleGenerator,
+ tableOptions: {},
+ });
+ SubtotalRenderer.propTypes = Object.assign({}, PivotData.propTypes, {
+ tableColorScaleGenerator: PropTypes.func,
+ tableOptions: PropTypes.object,
+ });
+ return SubtotalRenderer;
+}
+
+class TSVExportRenderer extends React.PureComponent {
+ render() {
+ const pivotData = new PivotData(this.props);
+ const rowKeys = pivotData.getRowKeys();
+ const colKeys = pivotData.getColKeys();
+ if (rowKeys.length === 0) {
+ rowKeys.push([]);
+ }
+ if (colKeys.length === 0) {
+ colKeys.push([]);
+ }
+
+ const headerRow = pivotData.props.rows.map(r => r);
+ if (colKeys.length === 1 && colKeys[0].length === 0) {
+ headerRow.push(this.props.aggregatorName);
+ } else {
+ colKeys.map(c => headerRow.push(c.join('-')));
+ }
+
+ const result = rowKeys.map(r => {
+ const row = r.map(x => x);
+ colKeys.map(c => {
+ const aggregator = pivotData.getAggregator(r, c);
+ row.push(aggregator.value());
+ });
+ return row;
+ });
+
+ result.unshift(headerRow);
+
+ return (
+ |