React and Vega: An Alternative Visualization Example

In this post, I’ll go over an example of how to render a couple of charts by creating React components that encapsulate Vega visualizations. The final code has a line chart and a radial heatmap that are synchronized so that when you hover over one, the same item in each of the charts is highlighted.

Code: https://github.com/pbeshai/linked-highlighting-react-vega-redux

Demo: http://pbeshai.github.io/linked-highlighting-react-vega-redux/


Several months ago I wrote a post demonstrating one way of accomplishing linked highlighting using React, D3, and Reflux. Since then, I was curious to check out how I could accomplish essentially the same thing using the visualization grammar Vega, created by Jeff Heer’s group. I was impressed by their talk at OpenVis and reminded of the potential of their work during their presentations at IEEE VIS in October 2015. Since then, I took a couple of days to put this example together and see what Vega was all about.

Disclaimer: This is my first time using Vega and Redux, and I haven’t seen any example of integrating Vega with React, so the code I show here may not be the optimal way of combining these technologies, but I hope they provide a place to get started.


The End Result

The code used here will create two different react components, a line chart and a radial heat map using Vega, and use Redux to enable linked highlighting between the two charts when interacting with a single one, as demonstrated below:

Why Vega?

The cool thing about Vega is that when you’re done making a visualization with it, you’ve got a portable description of your vis that is essentially just a big JSON object. You can then use this same description in various different environments (e.g., on the web or some server-side application). This appealed to me since I wanted to be able to re-use visualizations in the web and in PDFs.

It’s also really fast! It claims to be 2–10x faster than SVG (used by D3). The Vega team has put together a wiki page comparing Vega and D3 that’s worth a read.

Getting Started

Anyone who has worked with modern web projects knows it’s kind of a pain in the ass to get started, but once your dev environment is configured, it’s really fun.

To ease my way into starting this project, I went to the Reactiflux chat channel and asked what people typically do to get started with React and Redux. Someone there pointed me to the Redux examples source code and suggested I just copy one of the examples and edit as needed to get started.

This resulted in a project structure as follows:

  • actions/ The Redux actions directory
  • components/ The React components directory where the charts are found
  • constants/ Where the Redux Action Types are stored
  • containers/ Where the main React app container lives
  • dist/ The built code after running webpack
  • reducers/ The Redux reducers directory
  • store/ Where the Redux store is configured
  • index.html The main html file
  • index.js The main js file to be imported into the html
  • server.js The express web server

I’m not really going to focus on how Redux played its part in this mini project. It is only used to handle the managing the state for linked highlighting in a similar way to how I used Reflux in my previous post. If you don’t want to use Redux, you can really easily get by just using callbacks on the chart components that setState on the App component.

Installing Vega

I’m sad to say it was a real challenge to get vega working on my computer since having it as an npm dependency means you need to get all the dependencies necessary for server-side rendering. This was surprisingly difficult and required installing xquartz and cairo via Brew and ensuring my pkgconfig path was configured properly. The problem was with getting node-canvas to install.

You might see something like this:

Package cairo was not found in the pkg-config search path.
Perhaps you should add the directory containing `cairo.pc’
to the PKG_CONFIG_PATH environment variable
No package ‘cairo’ found
gyp: Call to ‘./util/has_cairo_freetype.sh’ returned exit status 0. while trying to load binding.gyp

Note: I just tried this and got that error… but it seems like Vega still installed and the web app is still working, so maybe you can just ignore it.

Babel

I should note that this code uses Babel to transpile to ES5 Javascript, my apologies to those that haven’t drank the koolaid yet. It’s a beautiful world out here.

Beware! Copies of Data!

Another point I want to make sure people keep in mind is that when data is passed into Vega, it creates copies with an internal _id property added to them. This means you need to reference your highlighted point by some kind of identifier — a === comparison will not work.

Creating the Line Chart

I used the new Class syntax for creating these components in this project. I still don’t really like it though. The convenience of getting mixins and not having to do .bind(this) on all of the member functions makes React.createClass() still the winner in my books.

Here’s the full code for a basic line chart component using React and Vega

It’s a relatively simple component, but let’s unpack the Vega specific parts.

We’ll start by looking at the main chunk of Vega code — the specification, found in the _spec() function. The spec here is just a simple JSON object, so it doesn’t really make sense to have it returned from a function, since it never changes. However, since we specify the width and height as part of the spec, and those may change depending on the properties of our component (not shown in this example), it makes sense generally to use a function to generate the spec.

// the vega spec for the chart
_spec() {
return {
'width': 400,
'height': 400,
'padding': { 'top': 10, 'left': 50, 'bottom': 50, right: 10 },
'data': [{ 'name': 'points' }],
'scales': [
{
'name': 'x',
'type': 'linear',
'domain': { 'data': 'points', 'field': 'distance' },
'range': 'width'
},
{
'name': 'y',
'type': 'linear',
'domain': { 'data': 'points', 'field': 'value' },
'range': 'height',
'nice': true
}
],
'axes': [
{
'type': 'x',
'scale': 'x',
'offset': 5,
'ticks': 5,
'title': 'Distance',
'layer': 'back'
},
{
'type': 'y',
'scale': 'y',
'offset': 5,
'ticks': 5,
'title': 'Value',
'layer': 'back'
}
],
'marks': [
{
'type': 'line',
'from': { 'data': 'points' },
'properties': {
'enter': {
'x': { 'scale': 'x', 'field': 'distance' },
'y': { 'scale': 'y', 'field': 'value' },
'stroke': { 'value': '#5357a1' },
'strokeWidth': { 'value': 2 }
}
}
}
]
};
}

(My apologies for the rampant quotation marks in the above code. Vega specs can be tested with their online Vega Editor, which takes strict JSON data, meaning properties need to be wrapped in double quotations. My eslint config hates double quotations, so I replaced them with single quotes, making everyone equally unhappy.)

Specifying the Data

data: [{ name: 'points' }]

By specifying the data simply by a name, Vega will look for the data provided via its .data() function, seen in componentDidMount and componentDidUpdate as:

vis.data('points').insert(data)

Setting the Scales

scales: [
{
name: 'x',
type: 'linear',
domain: { data: 'points', field: 'distance' },
range: 'width'
},
{
name: 'y',
type: 'linear',
domain: { data: 'points', field: 'value' },
range: 'height',
nice: true
}
]

These read pretty clearly. Two scales are created named x and y, that get their domains from the data (the properties distance for x and value for y on the objects in the dataset named points) and map their ranges to the width and height respectively. The nice attribute on the y scale ensures the domain is set using “human-friendly” numbers (e.g., 7 instead of 6.96).

Add in Axes

axes: [
{
type: 'x',
scale: 'x',
offset: 5,
ticks: 5,
title: 'Distance',
layer: 'back'
}, {
type: 'y',
scale: 'y',
offset: 5,
ticks: 5,
title: 'Value',
layer: 'back'
}
]

Adding in axes was pretty straightforward, so I figured I might as well do it. Nothing really complicated to mention here. Note that the scales map to those defined above by their name attribute, and the layer attribute determines whether the axes are in front (“front”) of the data points or behind them (“back”).

Specifying How The Data Is Rendered

marks: [
{
type: 'line',
from: { data: 'points' },
properties: {
enter: {
x: { scale: 'x', field: 'distance' },
y: { scale: 'y', field: 'value' },
stroke: { value: '#5357a1' },
strokeWidth: { value: 2 }
}
}
}
]

Vega supports a number of different types of data marks to be used for rendering. I’m using line here, the obvious choice for a line chart. Similar to D3, we specify how the data is rendered with the properties enter, update, and exit. The properties that are valid to be set within enter are determined by the type of mark being used. Since I’m using a line mark, I specify the x and y scales and a couple of stroke properties.

x: { scale: 'x', field: 'distance' },
y: { scale: 'y', field: 'value' },

Here we specify that the x property of the line mark should use the scale named x (defined elsewhere in the spec, as we saw above) with the property distance for each of the data points in the dataset named (aptly) points. Similarly, the y property of the line mark comes from the value field in the data in points.

You’ll note that I am explicitly specifying how to color the stroke directly in the chart specification. This is because Vega wants to completely describe a chart with the specification so it can be reproduced identically by different programs. For those that yearn for styles to be separate from the vis code, I believe the Vega team is working on theming support.

Getting React to Render the Vega Component

So now that we understand the basic Vega spec, how do we hook it up to React? It’s just like hooking up any other third party rendering library, we use a ref to one of the elements in our render() function and tell Vega to mount the component there in componentDidMount, then to update it in componentDidUpdate.

// On initial load, generate the initial vis
componentDidMount() {
const { data } = this.props;
const spec = this._spec();
  // parse the vega spec and create the vis
vg.parse.spec(spec, chart => {
const vis = chart({ el: this.refs.chartContainer });
    // set the initial data
vis.data(‘points’).insert(data);
    // render the vis
vis.update();
    // store the vis object in state to be used on later updates
this.setState({ vis });
});
}
// updates mean that the data changed
componentDidUpdate() {
const { vis } = this.state;
const { data } = this.props;
  if (vis) {
// update data in case it changed
vis.data('points').remove(() => true).insert(data);
    vis.update();
}
}

The vg.parse.spec() takes the JSON Vega specification for our vis and provides a function to create the chart itself based on the spec. Note that it’s very important to call vis.update() after supplying the spec with data, otherwise you’ll be left staring at a blank screen questioning your existence.

Adding Hover Behaviour

There are many ways of adding hover behaviour to a line chart, and I’ll discuss how to highlight the nearest point to the mouse by using a Voronoi transformation of the data.

The red lines show a Voronoi transformation of the line chart data. The areas enclosed by the red lines are the “cells.”

I’ve also written code for an alternative method that simply uses mouse move to find the nearest point by X value.

Here’s the code for the Voronoi version:

The notable changes include: adding a Voronoi transformation of the data based on the x and y scales, a Vega signal that listens for mouse movements over the Voronoi cells, and a handler to that signal that fires off a callback to indicate that a cell should be highlighted. If we didn’t care about having linked highlighting, we wouldn’t need to handle our highlightPoint externally or have the _handleHover callback, we could just have vega reference the highlight point from the signal directly. See my example here: http://jsbin.com/layoxex/5/edit?html,output

Adding in the highlight point data

data: [{ name: 'points' }, { name: 'highlightedPoint' }],
...
marks: [
...
{
type: 'symbol',
from: { data: 'highlightedPoint' },
interactive: false, // <-- essentially pointer-events: none
properties: {
enter: {
x: { scale: 'x', field: 'distance' },
y: { scale: 'y', field: 'value' },
fill: { value: '#fa7f9f' },
stroke: { value: '#891836' },
strokeWidth: { value: 1 },
size: { value: 64 }
}
}
}
]

These parts of the spec simply add in a new dataset (highlightedPoint) that is rendered as a symbol mark. An important new property has been added: interactive. It is set to false to make sure when the user hovers over the highlighted point it doesn’t fire a mouseout event for the voronoi cell.

Since we’ve specified a new dataset, we need to hook in the actual data for it in our componentDidMount and componentDidUpdate functions:

if (highlightedPoint) {
vis.data('highlightedPoint').insert([highlightedPoint]);
}

Creating a Voronoi Transform to Find the Nearest Point

An easy way to get the nearest point to the mouse is to see which cell of a voronoi diagram based on the positions of the points in the dataset the mouse is currently in. To accomplish this, we add in a new mark that is invisible to the user, but will be used for mouseover and mouseout events, as follows:

marks: [
...
{
type: 'path',
name: 'cell',
from: {
mark: 'points',
transform: [
{ type: 'voronoi', x: 'x', y: 'y' }
]
},
...
]

And just like that, we’ve got voronoi cells. Now, my understanding of this is that we are taking the x and y output from the marks named points, which makes use of our x and y scales to transform the distance and value properties of our data points into pixel positions on screen. We take those as input to the voronoi transform which creates cells that can be used for getting mouse events.

However, upon reviewing this code, I realized that I don’t even have a set of marks named points and I can, in fact, put any word I want as the value of the mark property and I get the desired result. So, I think the right way to do it is to have a set of marks that are just the points being transformed by the scales x and y and feeding those into the voronoi transform as described above, but it seems to work without the points explicitly defined as well. Not sure why.

Adding in Mouse Listeners via Signals

signals: [
{
name: 'hover', init: null,
streams: [
{ type: '@cell:mouseover', expr: 'datum' },
{ type: '@cell:mouseout', expr: 'null' }
]
}
]

Signals are Vega’s way of handling interactivity. The above configuration specifies that when the user mouses over or mouses out of the mark named cell (i.e., the voronoi cells described above), the datum associated with that cell is passed as the “hover” signal’s value. Then, when we mount our vis, we add a listener for the hover signal to call our onHighlight callback with the highlighted datum:

componentDidMount() {
...
vg.parse.spec(spec, chart => {
const vis = chart({ el: this.refs.chartContainer })
.onSignal('hover', this._handleHover);
...
});
}

Creating the Radial Heatmap

The Radial Heatmap is created in a very similar fashion. We just specify different scales and different mark types in our vega specification.

Linked Highlighting

So, how does the linked highlighting work? Since the LineChart and RadialHeatmap are designed to take a highlightPoint as a prop, they just need to be given the same highlightPoint at the same time to give us the linked highlighting effect. We accomplish this by triggering a callback on mouse events in both the LineChart and RadialHeatmap that update the state of the highlight point in our main store. When this state changes, the main app container gets the new value and passes it as an updated prop to both the LineChart and RadialHeatmap.

With this design, neither chart is aware that linked highlighting is taking place, and it enables code external to the charts to control which points are highlighted in each.

Issues With Vega

While trying to put this example together, I ran into a number of issues trying to understand how to use Vega.

1. Documentation and Examples. While there is a fair amount of documentation on the wiki pages for Vega , there are not many examples in the wild of how to do things. In particular, there seemed to be a dearth of examples of how to use Vega outside of just creating specs and passing them to the Vega-Editor. There are a number of unique terms to Vega, such as predicates and signals that are challenging to figure out how to combine properly to produce the desired results.

2. You are expected to have one big Vega instance with multiple components within. While this approach works nicely for full screen visualizations, it is contrary to the more component based approach used in web development frameworks like React or Angular. As I’ve shown in the example in this post, it is possible to create components that interact with each other, but you do pay some overhead in that copies of the input data are made for each of the Vega instances you create, which is unfortunate.

3. Setting the size of a circle is confusing as all hell. It took me far longer than I expected to get the radial heatmap to look right. In my head, and in previous implementations, it was simple: all I had to do was map distance to the radius of the circles. However, there is no concept of “radius” of a circle when using symbol::circle marks in Vega. I had to dig into the source code to figure out how circles were rendered, where I discovered the size parameter was input into a formula that mapped to the area of the circle, not the radius. I’m sure they have their reasons for this, but it was pretty frustrating for me.

I will say, however, that both the mailing list and the GitHub repositories are very active, and the contributors responded to issues I made and questions I posed very quickly.

What’s Next?

There’s clearly a fair amount of overlap between the two components LineChart and RadialHeatmap, that make sense to abstract out for reuse.

I didn’t explore how to deal with updating the spec in componentDidUpdate, which would come in handy for when the width or height of the charts or the number of circles to draw in the RadialHeatmap changes.

Well, that’s it. Hope this helps somebody figure out how to create some interactive charts with Vega and React.



Thanks for reading! If you have any questions, you can find me on Twitter @pbesh.