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.

  1. Replacing (most of) d3.js with pure SVG + AngularJS
  2. Screw D3.js, Bar Charts in AngularJS

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 Bars 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.