UNPKG

react-pivot

Version:

React-Pivot is a data-grid component with pivot-table-like functionality for data display, filtering, and exploration.

560 lines (464 loc) 13.3 kB
import filter from 'lodash/filter' import map from 'lodash/map' import find from 'lodash/find' import React from 'react' import createReactClass from 'create-react-class' import DataFrame from 'dataframe' import Emitter from 'wildemitter' import partial from './lib/partial' import download from './lib/download' import getValue from './lib/get-value' import PivotTable from './lib/pivot-table.jsx' import Dimensions from './lib/dimensions.jsx' import ColumnControl from './lib/column-control.jsx' import SoloControl from './lib/solo-control.jsx' import { serializeSoloValue, createSoloFilter, soloEntries } from './lib/solo-utils.js' const _ = { filter, map, find } export default createReactClass({ displayName: 'ReactPivot', getDefaultProps: function() { return { rows: [], dimensions: [], activeDimensions: [], reduce: function() {}, tableClassName: '', csvDownloadFileName: 'table.csv', csvTemplateFormat: false, defaultStyles: true, nPaginateRows: 25, solo: {}, hiddenColumns: [], hideRows: null, sortBy: null, sortDir: 'asc', eventBus: new Emitter, compact: false, excludeSummaryFromExport: false, onData: function () {}, soloText: "solo", unsoloText: "unsolo", subDimensionText: "Sub Dimension...", showClearFilters: false, clearFiltersText: "Clear Filters" } }, getInitialState: function() { var allDimensions = this.props.dimensions var activeDimensions = _.filter(this.props.activeDimensions, function (title) { return _.find(allDimensions, function(col) { return col.title === title }) }) return { dimensions: activeDimensions, calculations: {}, sortBy: this.props.sortBy, sortDir: this.props.sortDir, hiddenColumns: this.props.hiddenColumns, solo: this.props.solo, filtersPaused: false, hideRows: this.props.hideRows, rows: [] } }, componentDidMount: function() { if (this.props.defaultStyles) loadStyles() this.dataFrame = DataFrame({ rows: this.props.rows, dimensions: this.props.dimensions, reduce: this.props.reduce }) this.updateRows() }, componentDidUpdate: function(prevProps) { if(this.props.hiddenColumns !== prevProps.hiddenColumns) { this.setHiddenColumns(this.props.hiddenColumns); } if(this.props.rows !== prevProps.rows) { this.dataFrame = DataFrame({ rows: this.props.rows, dimensions: this.props.dimensions, reduce: this.props.reduce }) this.updateRows() } if (this.props.solo !== prevProps.solo) { this.setState({solo: this.props.solo}, this.updateRows) } }, getColumns: function() { var self = this var columns = [] this.state.dimensions.forEach(function(title) { var d = _.find(self.props.dimensions, function(col) { return col.title === title }) columns.push({ type: 'dimension', title: d.title, value: d.value, className: d.className, template: d.template, sortBy: d.sortBy }) }) this.props.calculations.forEach(function(c) { if (self.state.hiddenColumns.indexOf(c.title) >= 0) return var className = 'reactPivot-calculation' if (c.className) className += ' ' + c.className columns.push({ type:'calculation', title: c.title, template: c.template, value: c.value, className: className, sortBy: c.sortBy }) }) return columns }, renderFilterActions: function() { var hasFilters = soloEntries(this.state.solo).length > 0 if (!hasFilters) { return <SoloControl solo={this.state.solo} onToggle={this.setSolo} /> } var buttonText = this.state.filtersPaused ? 'Resume Filters' : 'Pause Filters' return ( <div className='reactPivot-filterActions'> <SoloControl solo={this.state.solo} onToggle={this.setSolo} /> {this.props.showClearFilters && ( <button className='reactPivot-clearFilters' onClick={this.clearSolo} > {this.props.clearFiltersText} </button> )} <button onClick={this.toggleFilters}> {buttonText} </button> </div> ) }, render: function() { var html = ( <div className='reactPivot'> <div className='reactPivot-toolbar'> { this.props.hideDimensionFilter ? null : <Dimensions dimensions={this.props.dimensions} subDimensionText={this.props.subDimensionText} selectedDimensions={this.state.dimensions} onChange={this.setDimensions} /> } <div className='reactPivot-controls'> <ColumnControl hiddenColumns={this.state.hiddenColumns} onChange={this.setHiddenColumns} /> {this.renderFilterActions()} <div className="reactPivot-csvExport"> <button onClick={partial(this.downloadCSV, this.state.rows)}> Export CSV </button> </div> </div> </div> <PivotTable columns={this.getColumns()} rows={this.state.rows} sortBy={this.state.sortBy} sortDir={this.state.sortDir} onSort={this.setSort} onColumnHide={this.hideColumn} nPaginateRows={this.props.nPaginateRows} tableClassName={this.props.tableClassName} onSolo={this.setSolo} soloText={this.props.soloText} unsoloText={this.props.unsoloText} solo={this.state.solo} /> </div> ) return html }, updateRows: function () { var columns = this.getColumns() var sortByTitle = this.state.sortBy var sortCol = _.find(columns, function(col) { return col.title === sortByTitle }) || {} var sortBy = sortCol.sortBy || (sortCol.type === 'dimension' ? sortCol.title : sortCol.value); var sortDir = this.state.sortDir var hideRows = this.state.hideRows var calcOpts = { dimensions: this.state.dimensions, sortBy: sortBy, sortDir: sortDir, compact: this.props.compact } if (!this.state.filtersPaused) { calcOpts.filter = createSoloFilter(this.state.solo, this.state.dimensions) } var rows = this.dataFrame .calculate(calcOpts) .filter(function (row) { return hideRows ? !hideRows(row) : true }) this.setState({rows: rows}) this.props.onData(rows) }, setDimensions: function (updatedDimensions) { this.props.eventBus.emit('activeDimensions', updatedDimensions) this.setState({dimensions: updatedDimensions}) setTimeout(this.updateRows, 0) }, setHiddenColumns: function (hidden) { this.props.eventBus.emit('hiddenColumns', hidden) this.setState({hiddenColumns: hidden}) setTimeout(this.updateRows, 0) }, setSort: function(cTitle) { var sortBy = this.state.sortBy var sortDir = this.state.sortDir if (sortBy === cTitle) { sortDir = (sortDir === 'asc') ? 'desc' : 'asc' } else { sortBy = cTitle sortDir = 'asc' } this.props.eventBus.emit('sortBy', sortBy) this.props.eventBus.emit('sortDir', sortDir) this.setState({sortBy: sortBy, sortDir: sortDir}) setTimeout(this.updateRows, 0) }, setSolo: function(solo) { if (!solo || typeof solo !== 'object') return var dimension = solo.title if (!dimension) return var valueKey = serializeSoloValue(solo.value) if (!valueKey) return var newSolo = Object.assign({}, this.state.solo) var valueMap = newSolo[dimension] || {} if (Object.prototype.hasOwnProperty.call(valueMap, valueKey)) { newSolo[dimension] = this.removeSoloValue(valueMap, valueKey) if (!newSolo[dimension]) delete newSolo[dimension] } else { newSolo[dimension] = this.addSoloValue(valueMap, valueKey) } this.props.eventBus.emit('solo', newSolo) // Auto-resume filters when adding or removing a solo value this.setState({solo: newSolo, filtersPaused: false}, this.updateRows) }, clearSolo: function() { this.props.eventBus.emit('solo', {}) this.setState({solo: {}, filtersPaused: false}, this.updateRows) }, addSoloValue: function(valueMap, key) { var updated = Object.assign({}, valueMap) updated[key] = true return updated }, removeSoloValue: function(valueMap, key) { var updated = Object.assign({}, valueMap) delete updated[key] return Object.keys(updated).length > 0 ? updated : null }, toggleFilters: function() { this.setState({filtersPaused: !this.state.filtersPaused}, this.updateRows) }, hideColumn: function(cTitle) { var hidden = this.state.hiddenColumns.concat([cTitle]) this.setHiddenColumns(hidden) setTimeout(this.updateRows, 0) }, downloadCSV: function(rows) { var self = this var columns = this.getColumns() var csv = _.map(columns, 'title') .map(JSON.stringify.bind(JSON)) .join(',') + '\n' var maxLevel = this.state.dimensions.length - 1 var excludeSummary = this.props.excludeSummaryFromExport rows.forEach(function(row) { if (excludeSummary && (row._level < maxLevel)) return var vals = columns.map(function(col) { if (col.type === 'dimension') { var val = row[col.title] } else { var val = getValue(col, row) } if (col.template && self.props.csvTemplateFormat) { val = col.template(val) } return JSON.stringify(val) }) csv += vals.join(',') + '\n' }) download(csv, this.props.csvDownloadFileName, 'text/csv') } }) function loadStyles() { if (typeof document === 'undefined') return // SSR safety if (document.getElementById('react-pivot-styles')) return // Already loaded const css = `.reactPivot { margin-top: 40px; padding: 10px 20px 20px; background: #fff; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); } .reactPivot select { color: #555; height: 28px; border: none; margin-right: 5px; margin-top: 5px; background-color: #FFF; border: 1px solid #CCC; } .reactPivot-results table { width: 100%; clear: both; text-align: left; border-spacing: 0; } .reactPivot-results th.asc:after, .reactPivot-results th.desc:after { font-size: 50%; opacity: 0.5; } .reactPivot-results th.asc:after { content: ' ▲' } .reactPivot-results th.desc:after { content: ' ▼' } .reactPivot-results td { border-top: 1px solid #ddd; padding: 8px; } .reactPivot-results td.reactPivot-calculation, .reactPivot-results th.reactPivot-calculation { text-align: right; } .reactPivot-results td.reactPivot-indent { border: none; } .reactPivot-results tr:hover td { background: #f5f5f5 } .reactPivot-results tr:hover td.reactPivot-indent { background: none; } .reactPivot-solo { opacity: 0; margin-left: 6px; user-select: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; } .reactPivot-solo:hover {font-weight: bold} td:hover .reactPivot-solo {opacity: 0.5} .reactPivot-toolbar { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 8px; margin: 10px 0; } .reactPivot-controls { display: flex; flex-wrap: wrap; gap: 5px; align-items: flex-start; justify-content: flex-end; flex: 0 0 auto; } .reactPivot-controls > * { margin: 0; } .reactPivot-controls select { margin: 0; width: 120px; } .reactPivot-toolbar select { margin-top: 0; } .reactPivot-csvExport { display: flex; align-items: flex-start; flex: 0 0 auto; } .reactPivot-csvExport button { background-color: #FFF; border: 1px solid #CCC; height: 28px; color: #555; cursor: pointer; padding: 0 12px; border-radius: 0; margin-top: 0; white-space: nowrap; flex-shrink: 0; } .reactPivot-filterActions { display: flex; gap: 5px; align-items: flex-start; flex: 0 0 auto; } .reactPivot-filterActions button { background-color: #FFF; border: 1px solid #CCC; height: 28px; color: #555; cursor: pointer; padding: 0 12px; border-radius: 0; margin-top: 0; white-space: nowrap; flex-shrink: 0; } .reactPivot-soloControl { display: flex; gap: 5px; align-items: flex-start; } .reactPivot-dimensions { display: flex; flex-wrap: wrap; gap: 5px; padding: 0; text-align: left; flex: 1 1 300px; min-width: 0; } .reactPivot-dimensions select { margin: 0; } .reactPivot-hideColumn { opacity: 0 } th:hover .reactPivot-hideColumn { opacity: 0.5; margin-right: 4px; margin-bottom: 2px; } .reactPivot-hideColumn:hover { font-weight: bold; cursor: pointer; } .reactPivot-pageNumber { padding: 2px; margin: 4px; cursor: pointer; color: gray; font-size: 14px; } .reactPivot-pageNumber:hover { font-weight: bold; border-bottom: black solid 1px; color: black; } .reactPivot-pageNumber.is-selected { font-weight: bold; border-bottom: black solid 1px; color: black; } .reactPivot-paginate { margin-top: 24px; }` const style = document.createElement('style') style.id = 'react-pivot-styles' style.textContent = css document.head.appendChild(style) }