Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ csvTemplateFormat | boolean | apply template formatting to data before csv expor
defaultStyles | boolean | apply default styles from style.css | true
hiddenColumns | array | columns that should not display | []
nPaginateRows | number | items per page setting | 25
solo | object | item that should be displayed solo | null
solo | object | active solo filters by dimension | {}
sortBy | string | name of column to use for record sort | null
sortDir | string | sort direction, either 'asc' or 'desc' | 'asc'
tableClassName | string | assign css class to table containing react-pivot elements | ''
Expand Down
75 changes: 58 additions & 17 deletions index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ 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
} from './lib/solo-utils.js'

const _ = { filter, map, find }

Expand Down Expand Up @@ -89,6 +93,10 @@ export default createReactClass({

this.updateRows()
}

if (this.props.solo !== prevProps.solo) {
this.setState({solo: this.props.solo}, this.updateRows)
}
},

getColumns: function() {
Expand Down Expand Up @@ -185,20 +193,15 @@ export default createReactClass({
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 soloFilter = createSoloFilter(this.state.solo, this.state.dimensions)
if (soloFilter) {
calcOpts.filter = soloFilter
}

var rows = this.dataFrame
.calculate(calcOpts)
.filter(function (row) { return hideRows ? !hideRows(row) : true })

this.setState({rows: rows})
this.props.onData(rows)
},
Expand Down Expand Up @@ -232,21 +235,59 @@ export default createReactClass({
},

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)
newSolo[solo.title] = solo.value
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)
this.setState({solo: newSolo })
setTimeout(this.updateRows, 0)
this.setState({solo: newSolo}, 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
},

clearSolo: function(title) {
if (typeof title === 'undefined' || title === null) return
clearSolo: function(payload) {
if (!payload) return

// If clearing a specific value, just toggle it
if (typeof payload === 'object' && Object.prototype.hasOwnProperty.call(payload, 'value')) {
this.setSolo({title: payload.title, value: payload.value})
return
}

// Otherwise, clear the entire dimension
var dimension = typeof payload === 'string' ? payload : payload.title
if (!dimension) return

var newSolo = Object.assign({}, this.state.solo)
delete newSolo[title]
if (!Object.prototype.hasOwnProperty.call(newSolo, dimension)) return

delete newSolo[dimension]
this.props.eventBus.emit('solo', newSolo)
this.setState({solo: newSolo})
setTimeout(this.updateRows, 0)
this.setState({solo: newSolo}, this.updateRows)
},

hideColumn: function(cTitle) {
Expand Down
50 changes: 38 additions & 12 deletions lib/solo-control.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
import React from 'react'
import createReactClass from 'create-react-class'
import { soloEntries, safeParseSoloPayload } from './solo-utils.js'

function formatSoloValue(value) {
if (value === null) return 'null'
if (typeof value === 'object') {
try {
return JSON.stringify(value)
} catch (err) {
return '[object]'
}
}

return String(value)
}

export default createReactClass({
getDefaultProps: function () {
Expand All @@ -10,22 +24,33 @@ export default createReactClass({
},

render: function () {
var entries = Object.keys(this.props.solo)
var entries = soloEntries(this.props.solo)

if (!entries.length) {
return (
<div className='reactPivot-soloControl'></div>
)
}

var options = entries.map(function(title) {
var value = this.props.solo[title]
var labelValue = typeof value === 'object' && value !== null
? JSON.stringify(value)
: String(value)
var label = title + ': ' + labelValue
return <option key={title} value={title}>{label}</option>
}, this)
var options = entries.map(function(entry) {
var valueLabel = formatSoloValue(entry.value)
var label = entry.title + ': ' + valueLabel

var payload
try {
payload = JSON.stringify({title: entry.title, value: entry.value})
} catch (err) {
return null
}

return <option key={entry.title + '::' + entry.key} value={payload}>{label}</option>
}).filter(Boolean)

if (!options.length) {
return (
<div className='reactPivot-soloControl'></div>
)
}

return (
<div className='reactPivot-soloControl'>
Expand All @@ -38,9 +63,10 @@ export default createReactClass({
},

handleClear: function (evt) {
var title = evt.target.value
if (!title) return
var payload = safeParseSoloPayload(evt.target.value)
if (!payload) return

this.props.onClear(title)
evt.target.value = ''
this.props.onClear(payload)
}
})
108 changes: 108 additions & 0 deletions lib/solo-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
export function serializeSoloValue(value) {
try {
return JSON.stringify(value)
} catch (err) {
return null
}
}

export function normalizeSolo(raw) {
if (!raw || typeof raw !== 'object') return {}

var normalized = {}

Object.keys(raw).forEach(function(dimension) {
var valueMap = raw[dimension]
if (!valueMap || typeof valueMap !== 'object' || Array.isArray(valueMap)) return

var filteredMap = {}
Object.keys(valueMap).forEach(function(key) {
if (valueMap[key]) filteredMap[key] = true
})

if (Object.keys(filteredMap).length) normalized[dimension] = filteredMap
})

return normalized
}

export function soloEntries(solo) {
if (!solo || typeof solo !== 'object') return []

var entries = []

Object.keys(solo).forEach(function(dimension) {
var valueMap = solo[dimension]
if (!valueMap || typeof valueMap !== 'object') return

Object.keys(valueMap).forEach(function(key) {
if (!valueMap[key]) return

var value
try {
value = JSON.parse(key)
} catch (err) {
return
}

entries.push({ title: dimension, key: key, value: value })
})
})

return entries
}

export function createSoloFilter(solo, dimensions) {
if (!solo || typeof solo !== 'object') return null
if (!Array.isArray(dimensions)) return null

var activeDimensions = dimensions.filter(function(dimension) {
var valueMap = solo[dimension]
return valueMap && typeof valueMap === 'object' && Object.keys(valueMap).length > 0
})

if (!activeDimensions.length) return null

return function(dimensionValues) {
return matchesSoloFilters(dimensionValues, solo, activeDimensions)
}
}

export function matchesSoloFilters(dimensionValues, soloFilters, soloTitles) {
return soloTitles.every(function(title) {
var valueMap = soloFilters[title]
if (!valueMap) return true

if (!Object.prototype.hasOwnProperty.call(dimensionValues, title)) return true

var key = serializeSoloValue(dimensionValues[title])
if (!key) return false

return Object.prototype.hasOwnProperty.call(valueMap, key)
})
}

export function isSoloValueActive(solo, title, value) {
var key = serializeSoloValue(value)
if (!key) return false

var valueMap = solo && solo[title]
if (!valueMap || typeof valueMap !== 'object') return false

return Object.prototype.hasOwnProperty.call(valueMap, key)
}

export function soloMapsEqual(a, b) {
return JSON.stringify(a || {}) === JSON.stringify(b || {})
}

export function safeParseSoloPayload(raw) {
if (!raw || typeof raw !== 'string') return null

try {
var parsed = JSON.parse(raw)
return (parsed && typeof parsed === 'object') ? parsed : null
} catch (err) {
return null
}
}