Skip to content

Instantly share code, notes, and snippets.

@XavierGimenez
Last active May 6, 2020 10:40
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save XavierGimenez/bef583d04a759c233324c4ee90b3a45c to your computer and use it in GitHub Desktop.
Save XavierGimenez/bef583d04a759c233324c4ee90b3a45c to your computer and use it in GitHub Desktop.
React component for Vega charts
/**
* @fileOverview React component to wrap Vega.js chart rendering.
* @author Xavi Giménez (xavi@xavigimenez.net)
*/
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;
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