Last active
May 6, 2020 10:40
-
-
Save XavierGimenez/bef583d04a759c233324c4ee90b3a45c to your computer and use it in GitHub Desktop.
React component for Vega charts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @fileOverview React component to wrap Vega.js chart rendering. | |
* @author Xavi Giménez ([email protected]) | |
*/ | |
import React, { Component } from 'react'; | |
import * as vega from 'vega'; | |
import * as _ from 'lodash'; | |
var vegaTooltip = require('vega-tooltip/build/vega-tooltip'); | |
class VegaChart extends Component { | |
componentDidMount() { | |
this.renderVega(); | |
} | |
componentDidUpdate() { | |
this.renderVega(); | |
} | |
renderVega() { | |
const { | |
// vega specification | |
spec, | |
// data for first dataset definition (by convention) | |
// the vega spec should have at least one dataset definition | |
data, | |
// some props | |
autoSize, | |
width, | |
height, | |
padding, | |
// config object defining default visual values (https://vega.github.io/vega/docs/config/) | |
vegaConfig, | |
// collection of data definition objects, to be added/overwritten within data block | |
dataDefs, | |
// collection of signals to be added to signals block | |
signals, | |
// collection of VegaSignalListener objects to listen signals defined in the spec. | |
// The VegaSignalListener has a handler for the signal event. The handler receives | |
// the signal name, the signal value, and a reference of the vega View. | |
// Signal listeners are useful to communicate with other VegaCharts, e.g.: | |
// having a small multiples of scatterplots, hovering a dot highlights | |
// the same dot in the other charts: | |
/** | |
let hoverListener = new VegaSignalListener('symbolMouseover', (name, value, view) => { | |
myVegaViews.forEach(vegaView => { | |
if(vegaView !== view) { | |
vegaView.signal('symbolHighlight', value); | |
vegaView.runAsync(); | |
} | |
}) | |
}); | |
*/ | |
signalListeners, | |
// callbacks for handling creation and render of vega View | |
runAfterCallback, | |
onVegaViewCreated } = this.props; | |
// inject data to first data set definition (by convention) | |
data && (spec.data[0].values = data); | |
// concat data definitions safely, by checking | |
// first whether they already exist. | |
// If the data definition is not in the spec, | |
// just insert it, otherwise override only the | |
// values, so other attributes remain (transforms, etc) | |
if(!_.isNil(dataDefs)) | |
dataDefs.forEach( dataDef => { | |
let dd = _.find(spec.data, d => d.name === dataDef.name); | |
if( _.isUndefined(dd) ) | |
spec.data.push(dataDef); | |
else | |
dd.values = dataDef.values; | |
}); | |
// set some top-level properties in the spec: | |
spec.title && (spec.title.text = this.props.title); | |
// merging props and spec signals | |
if(signals) | |
spec.signals.map(s => s.value = (signals.find(x => x.name === s.name) || s).value) | |
// vega specs have its 'autosize' property to 'fit' (automatically adjust | |
// the layout in an attempt to force the total visualization size to fit | |
// within the given width, height and padding values). | |
// When we have a restricted with, this is the property more suitable | |
spec.padding = padding || { | |
top: 20, | |
left: 15, | |
right: 15, | |
bottom: 20 | |
}; | |
spec.width = width || this.node.getBoundingClientRect().width - spec.padding.left - spec.padding.right; | |
spec.autosize = autoSize || 'fit'; | |
spec.height = height || 450; | |
// console.log("Vega spec: " + JSON.stringify(spec)); | |
// create the Vega view | |
let view = new vega.View( | |
vega.parse(spec, vegaConfig || {}) | |
) | |
.renderer('svg') | |
.tooltip( | |
(new vegaTooltip.Handler({theme: 'dark'})).call | |
) | |
.initialize(this.node) | |
.hover(); | |
view.runAsync() | |
.then( () => { | |
// This code need to be run AFTER the view has rendered: | |
// signal listeners will be invoked when the signal value | |
// changes during pulse propagation (e.g., after runAsync | |
// is called, but before its returned Promise resolves). | |
// https://vega.github.io/vega/docs/api/view/#view_runAsync | |
signalListeners && signalListeners.forEach(listener => { | |
view.addSignalListener( | |
listener.name, | |
(name, value) => { | |
listener.handler(name, value, view) | |
} | |
); | |
}); | |
// check if there is any callback to be invoked after | |
// the current dataflow evaluation completes | |
runAfterCallback && runAfterCallback(view); | |
}); | |
// pass the created view | |
onVegaViewCreated && onVegaViewCreated(view); | |
} | |
refCallback = node => { | |
this.node = node; | |
} | |
render() { | |
return ( | |
<div ref={this.refCallback} className={this.props.chartClass}></div> | |
) | |
} | |
} | |
export default VegaChart; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export default class VegaSignalListener { | |
constructor(name, handler) { | |
this.name = name; | |
this.handler = handler; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment