Integrating D3 with React

09 Jun 2016

For a dashboard I was making using React, I needed to generate some graphics and I thought of using D3 since I found an example which was almost exactly what I was looking for.

I have to say that I’m not an expert in D3 nor React, in the case of D3 all the graphs I have done have been using examples from the web (it’s amazing what you can do with it). And React, well it’s my last love.

Searching around I found different ways of integrating these two technologies. The first one that appears us using either (X or Y). These projects create components around different chart components, like axis, legends, lines etc. Which in turn are transformed into D3 elements. The problem with this approach was that it would make it harder to port the example I had to React and also it would mean adding a new library to the codebase (I take this very seriously).

Reusing most of the code of the sample was a priority for me, so the next option I thought was to use all of the D3 code and inject it on the componentRender method. But something didn’t felt right about this approach, as I won’t be leveraging in the power of React.

So, I end up creating a mix between the two. Using D3 to calculate all the coordinates for the graph (the heavy math) and then use React to build the graph with JSX. I like this approach a lot because it gives you a good understanding of the general structure of the resulting graph’s SVG code.

But let’s see some code. I’ll only focus on the code of the render method as everything else is pretty standard.

What I like about this approach, is that you can define very well the structure of the resulting SVG graph. And leverage the power o React to do the rendering. I have to do some tests on the performance of this method compared to using D3 directly, but it worked very well for my needs.

varAreaChart=React.createClass({propTypes:{data:React.PropTypes.any,startDate:React.PropTypes.date,endDate:React.PropTypes.date,},getDefaultProps:function(){return{width:600,height:300}},getInitialState:function(){return{varNames:Object.keys(this.props.data),series:this._parseDataSeries(this.props.data)}},componentWillReceiveProps(newProps){this.setState({varNames:Object.keys(newProps.data),series:this._parseDataSeries(newProps.data)});},render(){varseriesArr=this.state.series;varvarNames=this.state.varNames;varmargin={top:20,right:55,bottom:30,left:80},width=1000-margin.left-margin.right,height=500-margin.top-margin.bottom;varx=d3.time.scale.utc().domain([this.props.startDate,this.props.endDate]).range([0,width]);vary=d3.scale.linear().rangeRound([height,0]);varstack=d3.layout.stack().offset("zero").values((d)=>d.values).x((d)=>x(d.date)).y((d)=>d.revenue);vararea=d3.svg.area().interpolate("monotone").x((d)=>x(d.date))// dates come as strings from server.y0((d)=>y(d.y0)).y1((d)=>y(d.y0+d.y));varcolor=d3.scale.category20().domain(varNames);// adds y and y0 propertiesstack(seriesArr);// calculate the domain of the Y after injecting the area valuesy.domain([0,d3.max(seriesArr,function(c){returnd3.max(c.values,function(d){returnd.y0+d.y;});})]);varxAxis=d3.svg.axis().scale(x).orient("bottom");varyAxis=d3.svg.axis().scale(y).orient("left").tickFormat((d)=>'$'+Math.round(d/100));return(<ChartclassName={this.props.className}width={width+margin.left+margin.right}height={height+margin.top+margin.bottom}><gtransform={`translate(${margin.left}, ${margin.top})`}>{seriesArr.map((d)=><g><pathd={area(d.values)}style={{fill:color(d.name),stroke:'grey'}}></path>
</g>
)}<gclassName="axis"ref={(g)=>d3.select(g).call(yAxis)}><texttransform="rotate(-90)"y={6}dy=".71em"style=>Revenue</text>
</g>
<gclassName="axis"ref={(g)=>d3.select(g).call(xAxis)}transform={`translate(0, ${height})`}/>
{varNames.slice().reverse().map((name,i)=><gclassName="legend"transform={`translate(55, ${i*20})`}><rectx={width-10}width={10}height={10}style={{fill:color(name),stroke:'grey'}}/>
<textx={width-12}y={6}dy=".35em"style=>{name}</text>
</g>
)}</g>
</Chart>
);},_parseDataSeries:function(data){varvarNames=Object.keys(data);varseriesArr=[];varNames.forEach((name)=>{data[name].forEach((x)=>x.date=newDate(x.date));// convert all date objects to datesseriesArr.push({name:name,values:this._normalizeSerie(data[name])});});returnseriesArr;},_normalizeSerie:function(serie){varresult=[];// ensures there's at least one observation for each day in the rangevarstart=moment(this.props.startDate);while(start.isBefore(this.props.endDate)){letobservation=serie.filter((x)=>start.isSame(x.date,'day'));result.push({date:start.toDate(),revenue:(observation&&observation.length)?observation[0].revenue:0,})start=moment(start).add({days:1});}returnresult;}});