From dd724f50c0b1cd20f524fb8b653b26ad39f14591 Mon Sep 17 00:00:00 2001 From: Cedric Druck Date: Thu, 24 Jun 2021 13:20:37 +0200 Subject: [PATCH 1/5] minimalistic re-enabling of total col/row controls --- src/PlotlyRenderers.jsx | 531 ++++++++++++++++++++++++---------------- 1 file changed, 325 insertions(+), 206 deletions(-) diff --git a/src/PlotlyRenderers.jsx b/src/PlotlyRenderers.jsx index 4fb4e7f..1aa862c 100644 --- a/src/PlotlyRenderers.jsx +++ b/src/PlotlyRenderers.jsx @@ -1,235 +1,354 @@ import React from 'react'; import PropTypes from 'prop-types'; -import {PivotData} from './Utilities'; - -/* eslint-disable react/prop-types */ -// eslint can't see inherited propTypes! - -function makeRenderer( - PlotlyComponent, - traceOptions = {}, - layoutOptions = {}, - transpose = false -) { - class Renderer extends React.PureComponent { +import { PivotData } from './Utilities'; + +// 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; + } + } + 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 len; +}; + +function redColorScaleGenerator(values) { + const min = Math.min.apply(Math, values); + const max = Math.max.apply(Math, values); + return x => { + // 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 TableRenderer extends React.PureComponent { render() { const pivotData = new PivotData(this.props); + const colAttrs = pivotData.props.cols; + const rowAttrs = pivotData.props.rows; const rowKeys = pivotData.getRowKeys(); const colKeys = pivotData.getColKeys(); - const traceKeys = transpose ? colKeys : rowKeys; - if (traceKeys.length === 0) { - traceKeys.push([]); - } - const datumKeys = transpose ? rowKeys : colKeys; - if (datumKeys.length === 0) { - datumKeys.push([]); - } + const grandTotalAggregator = pivotData.getAggregator([], []); - let fullAggName = this.props.aggregatorName; - const numInputs = - this.props.aggregators[fullAggName]([])().numInputs || 0; - if (numInputs !== 0) { - fullAggName += ` of ${this.props.vals.slice(0, numInputs).join(', ')}`; - } + 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); - const data = traceKeys.map(traceKey => { - const values = []; - const labels = []; - for (const datumKey of datumKeys) { - const val = parseFloat( - pivotData - .getAggregator( - transpose ? datumKey : traceKey, - transpose ? traceKey : datumKey - ) - .value() + if (opts.heatmapMode === 'full') { + const allValues = []; + rowKeys.map(r => + colKeys.map(c => + allValues.push(pivotData.getAggregator(r, c).value()) + ) ); - values.push(isFinite(val) ? val : null); - labels.push(datumKey.join('-') || ' '); - } - const trace = {name: traceKey.join('-') || fullAggName}; - if (traceOptions.type === 'pie') { - trace.values = values; - trace.labels = labels.length > 1 ? labels : [fullAggName]; - } else { - trace.x = transpose ? values : labels; - trace.y = transpose ? labels : values; + 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); + }); + valueCellColors = (r, c, v) => rowColorScales[r](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); } - return Object.assign(trace, traceOptions); - }); - - let titleText = fullAggName; - const hAxisTitle = transpose - ? this.props.rows.join('-') - : this.props.cols.join('-'); - const groupByTitle = transpose - ? this.props.cols.join('-') - : this.props.rows.join('-'); - if (hAxisTitle !== '') { - titleText += ` vs ${hAxisTitle}`; - } - if (groupByTitle !== '') { - titleText += ` by ${groupByTitle}`; } - const layout = { - title: titleText, - hovermode: 'closest', - /* eslint-disable no-magic-numbers */ - width: window.innerWidth / 1.5, - height: window.innerHeight / 1.4 - 50, - /* eslint-enable no-magic-numbers */ - }; - - if (traceOptions.type === 'pie') { - const columns = Math.ceil(Math.sqrt(data.length)); - const rows = Math.ceil(data.length / columns); - layout.grid = {columns, rows}; - data.forEach((d, i) => { - d.domain = { - row: Math.floor(i / columns), - column: i - columns * Math.floor(i / columns), - }; - if (data.length > 1) { - d.title = d.name; + 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 + ); } - }); - if (data[0].labels.length === 1) { - layout.showlegend = false; - } - } else { - layout.xaxis = { - title: transpose ? fullAggName : null, - automargin: true, - }; - layout.yaxis = { - title: transpose ? null : fullAggName, - automargin: true, - }; - } + : null; return ( - + + + {colAttrs.map((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 ( + + ); + })} + + {j === 0 && this?.props.tableOptions.colTotals && ( + + )} + + ); + })} + + {rowAttrs.length !== 0 && ( + + {rowAttrs.map(function (r, i) { + return ( + + ); + })} + {} + + )} + + + + {rowKeys.map((rowKey, i) => { + const totalAggregator = pivotData.getAggregator(rowKey, []); + return ( + + {rowKey.map((txt, j) => { + const x = spanSize(rowKeys, i, j); + if (x === -1) { + return null; + } + return ( + + ); + })} + {colKeys.map((colKey, j) => { + const aggregator = pivotData.getAggregator(rowKey, colKey); + return ( + + ); + })} + {this.props.tableOptions.colTotals && } + + ); + })} + + {this.props.tableOptions.rowTotals && + + + {colKeys.map(function (colKey, i) { + const totalAggregator = pivotData.getAggregator([], colKey); + return ( + + ); + })} + + {this.props.tableOptions.colTotals && } + } + +
+ )} + {c} + {colKey[j]} + + Totals +
+ {r} + + {colAttrs.length === 0 ? 'Totals' : null} +
+ {txt} + + {aggregator.format(aggregator.value())} + + {totalAggregator.format(totalAggregator.value())} +
+ Totals + + {totalAggregator.format(totalAggregator.value())} + + {grandTotalAggregator.format(grandTotalAggregator.value())} +
); } } - Renderer.defaultProps = Object.assign({}, PivotData.defaultProps, { - plotlyOptions: {}, - plotlyConfig: {}, - }); - Renderer.propTypes = Object.assign({}, PivotData.propTypes, { - plotlyOptions: PropTypes.object, - plotlyConfig: PropTypes.object, - onRendererUpdate: PropTypes.func, - }); - - return Renderer; + TableRenderer.defaultProps = PivotData.defaultProps; + TableRenderer.propTypes = PivotData.propTypes; + TableRenderer.defaultProps.tableColorScaleGenerator = redColorScaleGenerator; + TableRenderer.defaultProps.tableOptions = {}; + TableRenderer.propTypes.tableColorScaleGenerator = PropTypes.func; + TableRenderer.propTypes.tableOptions = PropTypes.object; + return TableRenderer; } -function makeScatterRenderer(PlotlyComponent) { - class Renderer 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([]); - } +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 data = {x: [], y: [], text: [], type: 'scatter', mode: 'markers'}; + 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('-'))); + } - rowKeys.map(rowKey => { - colKeys.map(colKey => { - const v = pivotData.getAggregator(rowKey, colKey).value(); - if (v !== null) { - data.x.push(colKey.join('-')); - data.y.push(rowKey.join('-')); - data.text.push(v); - } - }); + const result = rowKeys.map(r => { + const row = r.map(x => x); + colKeys.map(c => { + const v = pivotData.getAggregator(r, c).value(); + row.push(v ? v : ''); }); + return row; + }); - const layout = { - title: this.props.rows.join('-') + ' vs ' + this.props.cols.join('-'), - hovermode: 'closest', - /* eslint-disable no-magic-numbers */ - xaxis: {title: this.props.cols.join('-'), automargin: true}, - yaxis: {title: this.props.rows.join('-'), automargin: true}, - width: window.innerWidth / 1.5, - height: window.innerHeight / 1.4 - 50, - /* eslint-enable no-magic-numbers */ - }; + result.unshift(headerRow); - return ( - - ); - } + return ( +