D3 and React - the future of charting components?
Using D3.js with React.js for reusable charts.
JavaScript React D3
Data-Driven Documents (D3.js) is currently my favoured charting library for producing interactive SVG visualisations. Typically, I chose to use it in combination with NVD3. It provides a higher level library of re-usable charts and chart components for D3.
Another JavaScript library that has taken my interest recently is React by Facebook. It aims to be just the View, or V in MVC, for building rich web application user interfaces.
Why React?
Two reasons that I find it of interest are firstly due to the implementation of a virtual DOM allowing very efficient and high performance browser DOM manipulation. This also allows a React JavaScript application to be entirely rendered on the server when using Node.js for the initial page load or SEO benefits. Secondly, React comes with an XML-like syntax called JSX that allows you to write React components and then compose them within your view code as if they were HTML tags. Much as web components - and libraries like Polymer - are aiming for. Except that you can use JSX with React today.
The example taken from React’s homepage illustrates this point, with the <HelloMessage />
component being appended to a DOM node.
/** @jsx React.DOM */
var HelloMessage = React.createClass({
render: function() {
return <div>Hello {this.props.name}</div>;
}
});
React.renderComponent(<HelloMessage name="John" />, mountNode);
The JSX compiler converts the XML syntax to vanilla JavaScript function calls. You can use React without JSX, but I think it’s a really good feature. The JSX compilation can be configured as part of a Grunt build or watch task or by referencing the JSXTransformer.js
script for runtime compilation during development.
Reusable charts with React components
Inspiration for combining React with D3 came from reading the following two blog posts where AngularJS was used as a part replacement for D3.
In these articles, AngularJS was used to manipulate the SVG elements, rather than D3.js. It had been relegated to providing the axis scales, calculating shape dimensions and path coordinates.
I was convinced that React could take Angular’s place. Bringing with it the benefits of the web component style charting tags that the JSX syntax would allow me to create.
This would allow me to specify my chart component in the domain and terms of charting: Chart
, Bar
, Line
and DataSeries
. Rather than the actual implementation detail of SVG elements such as g
, path
, rect
.
Baby steps to creating a reusable Bar chart
Getting started, I created a static HTML file and added a reference to React and its JSX compiler, D3.js and Underscore.js in the <head />
of the page.
<script src="http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.5.2/underscore.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/react/0.8.0/react.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/react/0.8.0/JSXTransformer.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/d3/3.3.13/d3.js"></script>
The body of the document simply contained the <div />
that would act as the target DOM node for React to use as the mount point.
<body>
<div id="container"></div>
</body>
Then I created a number of React components that would encapsulate the different elements needed to build a bar chart. These components were wrapped inside <script />
tag with the type text/jsx
so that the JSX compiler would compile them on-the-fly and convert them into vanilla JavaScript function calls.
<script type="text/jsx">
/** @jsx React.DOM */
// ... React components inside here
</script>
This began with a Chart
component to render a container svg
element with a given size.
Chart
var Chart = React.createClass({
render: function() {
return (
<svg width={this.props.width} height={this.props.height}>{this.props.children}</svg>
);
}
});
Next up, a component for each individual Bar
to render an SVG rect
element.
Bar
var Bar = React.createClass({
getDefaultProps: function() {
return {
width: 0,
height: 0,
offset: 0
}
},
render: function() {
return (
<rect fill={this.props.color}
width={this.props.width} height={this.props.height}
x={this.props.offset} y={this.props.availableHeight - this.props.height} />
);
}
});
Then came a DataSeries
component to take an array of values and create a collection of Bar
s for the chart.
Here I used D3 to create the scales for both x and y axis. This allowed mapping the range of the input domain (values from the data array) to the output range (the available chart area).
Data Series
var DataSeries = React.createClass({
getDefaultProps: function() {
return {
title: '',
data: []
}
},
render: function() {
var props = this.props;
var yScale = d3.scale.linear()
.domain([0, d3.max(this.props.data)])
.range([0, this.props.height]);
var xScale = d3.scale.ordinal()
.domain(d3.range(this.props.data.length))
.rangeRoundBands([0, this.props.width], 0.05);
var bars = _.map(this.props.data, function(point, i) {
return (
<Bar height={yScale(point)} width={xScale.rangeBand()} offset={xScale(i)} availableHeight={props.height} color={props.color} key={i} />
)
});
return (
<g>{bars}</g>
);
}
});
Finally, a BarChart
component was created and used to build the Chart
and its child DataSeries
with the data array and bar colouring. This was then rendered using React and added to the DOM via the getElementById
selector.
var BarChart = React.createClass({
render: function() {
return (
<Chart width={this.props.width} height={this.props.height}>
<DataSeries data={[ 30, 10, 5, 8, 15, 10 ]} width={this.props.width} height={this.props.height} color="cornflowerblue" />
</Chart>
);
}
});
React.renderComponent(
<BarChart width={600} height={300} />,
document.getElementById('container')
);
Bar chart component rendered by React
This gave me a taster of what could be acheived using React with D3. The end goal was to create a set of reusable charting components that could be composed to create a bar chart.
<Chart width={this.props.width} height={this.props.height}>
<DataSeries data={[ 30, 10, 5, 8, 15, 10 ]} width={this.props.width} height={this.props.height} color="cornflowerblue" />
</Chart>
More importantly, the JSX within the Chart component actually looked like it was describing a bar chart. Rather than dealing with an SVG and shapes - the mechanics of the implementation. Those are details that should be hidden away from the consumer.
Stacked bar chart
Next up I was interested in create a stacked bar chart, composed of a number of data series. I was able to reuse the Chart
and Bar
components, along with a reworked DataSeries
that handled multiple series.
Data Series
var DataSeries = React.createClass({
getDefaultProps: function() {
return {
title: '',
data: []
}
},
render: function() {
var self = this,
props = this.props,
refs = props.__owner__.refs
size = props.size,
width = size.width,
height = size.height;
var yScale = props.yScale;
var xScale = d3.scale.ordinal()
.domain(d3.range(props.data.length))
.rangeRoundBands([0, width], 0.05);
var otherSeries = _.chain(refs)
.values()
.reject(function(component) { return component === self; })
.value();
var bars = _.map(props.data, function(point, i) {
var yOffset = _.reduce(otherSeries, function(memo, series) {
return memo + series.props.data[i];
}, 0);
return (
<Bar height={yScale(point)} width={xScale.rangeBand()} x={xScale(i)} y={size.height - yScale(yOffset) - yScale(point)} color={props.color} key={i} />
)
});
return (
<g>{bars}</g>
);
}
});
Stacked Bar Chart
var StackedBarChart = React.createClass({
getDefaultProps: function() {
return {
width: 600,
height: 300
}
},
render: function() {
var data = this.props.data,
size = { width: this.props.width, height: this.props.height };
var zipped = _.zip(data.series1, data.series2, data.series3);
var totals = _.map(zipped, function(values) {
return _.reduce(values, function(memo, value) { return memo + value; }, 0);
});
var yScale = d3.scale.linear()
.domain([0, d3.max(totals)])
.range([0, this.props.height]);
return (
<Chart width={this.props.width} height={this.props.height}>
<DataSeries data={data.series1} size={size} yScale={yScale} ref="series1" color="cornflowerblue" />
<DataSeries data={data.series2} size={size} yScale={yScale} ref="series2" color="red" />
<DataSeries data={data.series3} size={size} yScale={yScale} ref="series3" color="green" />
</Chart>
);
}
});
var data = {
series1: [ 30, 10, 5, 8, 15, 10 ],
series2: [ 5, 20, 12, 4, 6, 2 ],
series3: [ 5, 8, 2, 4, 6, 2 ]
};
React.renderComponent(
<StackedBarChart data={data} />,
document.getElementById('container')
);
Stacked Bar chart component rendered by React
Line chart
Finally, I attempted a third chart, a multi-series line chart. Here I created a new Line
component, in place of the existing Bar
, to create an SVG path
element.
D3 was used to create the path coordinates for a given data series set of (x,y) coordinates.
Line
var Line = React.createClass({
getDefaultProps: function() {
return {
path: '',
color: 'blue',
width: 2
}
},
render: function() {
return (
<path d={this.props.path} stroke={this.props.color} strokeWidth={this.props.width} fill="none" />
);
}
});
Data Series
var DataSeries = React.createClass({
getDefaultProps: function() {
return {
data: [],
interpolate: 'linear'
}
},
render: function() {
var self = this,
props = this.props,
yScale = props.yScale,
xScale = props.xScale;
var path = d3.svg.line()
.x(function(d) { return xScale(d.x); })
.y(function(d) { return yScale(d.y); })
.interpolate(this.props.interpolate);
return (
<Line path={path(this.props.data)} color={this.props.color} />
)
}
});
The resultant LineChart
component with multiple DataSeries
and the corresponding data series values.
Line Chart
var LineChart = React.createClass({
getDefaultProps: function() {
return {
width: 600,
height: 300
}
},
render: function() {
var data = this.props.data,
size = { width: this.props.width, height: this.props.height };
var max = _.chain(data.series1, data.series2, data.series3)
.zip()
.map(function(values) {
return _.reduce(values, function(memo, value) { return Math.max(memo, value.y); }, 0);
})
.max()
.value();
var xScale = d3.scale.linear()
.domain([0, 6])
.range([0, this.props.width]);
var yScale = d3.scale.linear()
.domain([0, max])
.range([this.props.height, 0]);
return (
<Chart width={this.props.width} height={this.props.height}>
<DataSeries data={data.series1} size={size} xScale={xScale} yScale={yScale} ref="series1" color="cornflowerblue" />
<DataSeries data={data.series2} size={size} xScale={xScale} yScale={yScale} ref="series2" color="red" />
<DataSeries data={data.series3} size={size} xScale={xScale} yScale={yScale} ref="series3" color="green" />
</Chart>
);
}
});
var data = {
series1: [ { x: 0, y: 20 }, { x: 1, y: 30 }, { x: 2, y: 10 }, { x: 3, y: 5 }, { x: 4, y: 8 }, { x: 5, y: 15 }, { x: 6, y: 10 } ],
series2: [ { x: 0, y: 8 }, { x: 1, y: 5 }, { x: 2, y: 20 }, { x: 3, y: 12 }, { x: 4, y: 4 }, { x: 5, y: 6 }, { x: 6, y: 2 } ],
series3: [ { x: 0, y: 0 }, { x: 1, y: 5 }, { x: 2, y: 8 }, { x: 3, y: 2 }, { x: 4, y: 6 }, { x: 5, y: 4 }, { x: 6, y: 2 } ]
};
React.renderComponent(
<LineChart data={data} />,
document.getElementById('container')
);
Multi series Line chart component rendered by React
Was it worth the journey?
If web components are the future of web application development then React provides a reasonable stepping stone towards that future.
From this journey I saw a glimpse of the power of reusable components that can be built. An approach similar to these snippets could be taken forward and used to create the next generation, component-based and easy to use charting library.