UNPKG

react-pivot

Version:

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

443 lines (369 loc) 10.6 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' 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", subDimensionText: "Sub Dimension..." } }, 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, 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() } }, 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 columns.push({ type:'calculation', title: c.title, template: c.template, value: c.value, className: c.className, sortBy: c.sortBy }) }) return columns }, render: function() { var self = this var html = ( <div className='reactPivot'> { this.props.hideDimensionFilter ? '' : <Dimensions dimensions={this.props.dimensions} subDimensionText={this.props.subDimensionText} selectedDimensions={this.state.dimensions} onChange={this.setDimensions} /> } <ColumnControl hiddenColumns={this.state.hiddenColumns} onChange={this.setHiddenColumns} /> <div className="reactPivot-csvExport"> <button onClick={partial(this.downloadCSV, this.state.rows)}> Export CSV </button> </div> { Object.keys(this.state.solo).map(function (title) { var value = self.state.solo[title] return ( <div style={{clear: 'both'}} className='reactPivot-soloDisplay' key={'solo-' + title} > <span className='reactPivot-clearSolo' onClick={partial(self.clearSolo, title)} > &times; </span> {title}: {value} </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} /> </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 } var filter = this.state.solo if (filter) { calcOpts.filter = function(dVals) { var pass = true Object.keys(filter).forEach(function (title) { if (dVals[title] !== filter[title]) pass = false }) return pass } } 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) { var newSolo = this.state.solo newSolo[solo.title] = solo.value this.props.eventBus.emit('solo', newSolo) this.setState({solo: newSolo }) setTimeout(this.updateRows, 0) }, clearSolo: function(title) { var oldSolo = this.state.solo var newSolo = {} Object.keys(oldSolo).forEach(function (k) { if (k !== title) newSolo[k] = oldSolo[k] }) this.props.eventBus.emit('solo', newSolo) this.setState({solo: newSolo}) setTimeout(this.updateRows, 0) }, 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-soloDisplay { padding: 5px; } .reactPivot-clearSolo { opacity: 0.5; cursor: pointer; font-size: 120%; margin-right: 2px; } .reactPivot-clearSolo:hover { font-weight: bold; } .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-indent { border: none; } .reactPivot-results tr:hover td { background: #f5f5f5 } .reactPivot-results tr:hover td.reactPivot-indent { background: none; } .reactPivot-solo {opacity: 0} .reactPivot-solo:hover {font-weight: bold} td:hover .reactPivot-solo {opacity: 0.5} .reactPivot-csvExport, .reactPivot-columnControl { float: right; margin-left: 5px; } .reactPivot-csvExport button { background-color: #FFF; border: 1px solid #CCC; height: 28px; color: #555; cursor: pointer; padding: 0 10px; border-radius: 4px; margin-top: 5px; } .reactPivot-dimensions { float: left; padding: 10px 0; text-align: left; } .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) }