Marrying Backbone.js and D3.js, a Follow-up

Shirley Wu
6 min readJul 13, 2015

--

This article was originally posted to Quora on July 2014.

More than a year ago, I struggled with putting Backbone and D3 together. It wasn’t a particularly hard problem, merely that the two libraries had a lot of overlapping functionality, and it was an interesting internal struggle deciding where to use what. I documented my first approach, where I directly injected D3.js into Backbone.js, here: Marrying Backbone.js and D3.js.

Since then, I’ve learned quite a bit from the (amazing) D3 community, and explored a few more approaches. It’s far from perfect, but at a good enough point where I feel ready to document it again.

Reusable charts.

A few days after I put up my first blog post, I attended a meetup at the Bay Area d3 User Group, talking specifically about combining D3 with popular MV* frameworks.

One of the core concepts I came away with was Reusable D3, based on Mike Bostock’s article, Towards Reusable Charts. It talked about wrapping common D3 charts into functions, so they could be called over and over again without needing to be rewritten.

For example, to draw the above bar chart, we have to first setup the canvas:

var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 960 — margin.left — margin.right,
height = 500 — margin.top — margin.bottom;
var svg = d3.select(“body”).append(“svg”)
.attr(“width”, width + margin.left + margin.right)
.attr(“height”, height + margin.top + margin.bottom)
.append(“g”)
.attr(“transform”, “translate(“ + margin.left + “,” + margin.top + “)”);
/* set up x and y scales, and x and y axis */

Then load in the data and render.

d3.tsv(“data.tsv”, type, function(error, data) {
// draw the bars
svg.selectAll(“.bar”)
.data(data)
.enter().append(“rect”)
.attr(“class”, “bar”)
.attr(“x”, function(d) { return x(d.letter); })
.attr(“width”, x.rangeBand())
.attr(“y”, function(d) { return y(d.frequency); })
.attr(“height”, function(d) { return height — y(d.frequency); });
/* draw in x and y scales */
});

(Full example code by Mike Bostock here: Bar Chart)

So you can imagine that even if we were to need just one more chart (perhaps a side-by-side comparison of a chart #1 and a chart #2), the lines of code would increase rather quickly.

If we were instead to wrap the rendering code in a function, and pass in configuration with getter-setter methods:

function BarChart() {
var width = 800,
height = 800;
function Chart(selection) {
selection.selectAll(“.bar”)
.data(function(d) {return d})
.enter().append(“rect”)
.attr(“class”, “bar”)
.attr(“x”, function(d) { return x(d.letter); })
.attr(“width”, x.rangeBand())
.attr(“y”, function(d) { return y(d.frequency); })
.attr(“height”, function(d) { return height — y(d.frequency); });
/* set up and render the x and y-axis. */
}
Chart.width = function(value) {
if (!arguments.length) return width;
width = value;
return Chart;
};
Chart.height = function(value) {
if (!arguments.length) return height;
height = value;
return Chart;
};
return Chart;
}

We can then create a second chart right by the first one with only a couple more lines; using D3's selection.call() method, we can call the BarChart to render in two different group elements, with two different datasets bound to them.

d3.tsv(“data1.tsv”, type, function(error, data1) {
var chart1 = BarChart().width(200).height(200);
svg.append(“g”)
.attr(“transform”, “translate(0, 0)”)
.data(data1).call(chart1);
});
d3.tsv(“data2.tsv”, type, function(error, data2) {
var chart2 = BarChart().width(200).height(200);
svg.append(“g”)
.attr(“transform”, “translate(200, 0)”)
.data(data2).call(chart2);
});

A slight optimization.

It took me a couple tries to arrive at a pattern I was a fan of (an earlier example here: sxywu/80k), but one of the things I noticed was that my rendering functions were very verbose. And when I also included an update function on the chart, I was copy and pasting the rendering code and tweaking just a little bit, so that my Reusable Chart became doubly verbose.

What really helped was when I started to split up my Reusable Chart into parts.

Take, for example, the bar graph from above. Currently, it only renders. If we want the bar graph to also update, d3 says we would have to do three things: enter new bars, update the existing bars, and exit any bars no longer in the data set.

// enter new bars
svg.selectAll(“.bar”)
.data(data).enter().append(“rect”)
.attr(“class”, “bar”)
.attr(“width”, x.rangeBand())
.attr(“x”, function(d) { return x(d.letter); })
.attr(“y”, function(d) { return y(d.frequency); })
.attr(“height”, function(d) { return height — y(d.frequency); });
// update only the attributes affected by data change
svg.selectAll(“.bar”)
.data(data)
.attr(“x”, function(d) { return x(d.letter); })
.attr(“y”, function(d) { return y(d.frequency); })
.attr(“height”, function(d) { return height — y(d.frequency); });
// exit
svg.selectAll(“.bar”)
.data(data).exit().remove();

We can see that the code for entering the new bars is the same as the initial code for rendering all the bars. Thus, when we plug the update code into our reusable chart, we should create common functions for entering, updating, and exiting, so that we reduce code repetition:

function BarChart() {
var bars; // d3 selection of all rectangle bar elements
function Chart(selection) {
bars = selection.selectAll(“.bar”)
.data(function(d) {return d;})
.call(enterBars);
/* set up and render the x and y-axis. */
}
Chart.update = function() {
bars.data(function(d) {return d;})
.call(exitBars)
.call(updateBars)
.call(enterBars);
}
function enterBars(selection) {
selection.enter().append(“rect”)
.attr(“class”, “bar”)
.attr(“width”, x.rangeBand());
updateBars(selection);
}
function updateBars(selection) {
selection.attr(“x”, function(d) { return x(d.letter); })
.attr(“y”, function(d) { return y(d.frequency); })
.attr(“height”, function(d) { return height — y(d.frequency); });
}
function exitBars(selection) {
selection.exit().remove();
}
return Chart;
}

One thing I want to point out is the order of the Chart’s update function. We want to remove the old bars first, so that when we call the function to update the bars, we only update the intersection of the old and new data before entering the new bars.

One of the best things about these reusable charts, and the decoupling of functionality that it provides, is that our external call to the chart hasn’t changed:

d3.tsv(“data.tsv”, type, function(error, data) {
var chart = BarChart();
var g = svg.append(“g”)
.attr(“transform”, “translate(0, 0)”)
.data(data).call(chart);
// and if we want to update the chart
g.data(someUpdatedData);
chart.update();
});

Talking with Backbone.js

As you may have guessed, Backbone has a lot less of a role this time around. All it has to do is call the reusable chart’s render and update functions when appropriate:

var MyView = Backbone.View.extend(function() {
initialize: function() {
this.data = this.options.data || [];
this.chart = BarChart();
},
render: function() {
this.g = d3.select(‘svg’).append(‘g’)
.attr(“transform”, “translate(0, 0)”)
.data(this.data).call(this.chart);
return this;
},
events: {
“click .update”: “update”
},
update: function() {
this.g.data(someUpdatedData);
this.chart.update();
}
});
d3.tsv(“data.tsv”, type, function(error, data) {
var myView = new MyView({data: data});
myView.render();
});

I love this, because I don’t need to worry about my Backbone view if my bar graph’s implementation needs to change, and I don’t need to worry about my d3 code if I decide to use some other MV* framework.

More importantly, it establishes the roles Backbone and D3 play. In this pattern, the reusable D3 chart is the view, and takes care of the rendering and event handling. The Backbone models keep track of the data, which is especially useful within a complex app where other parts of the interface are manipulating the data that the chart displays. Finally, the Backbone views act as the controller, translating the model attributes to something the d3 chart can handle and vice versa. To me, Backbone’s biggest advantage is the way it tracks changes in the models, and notifies its subscribed views of that change.

In that sense, I very rarely use Backbone these days. I find that for most of my less-involved side projects, D3 can handle everything. It’s only when the side project involves constantly changing data via user interactions, that I also ask Backbone for help.

In conclusion?

It’s been a nice ride. I feel like I can now talk about this topic for hours (though probably not days). I’m ready to put it to rest and talk about other things (like complex visualizations on mobile, for one).

Sometime in the future though, I intend to take a look at the The Miso Project. This post is greatly inspired by Irene Ros’s keynote at d3.unconfback in March, where she talked about what it meant for a chart to be reusable. Two of the concepts she mentioned — extensibility and combinability — are things I have yet to explore, and I fully intend to down the line.

Irene Ros’s keynote at d3.unconf 2014

Responses, especially corrections, are very welcome.

--

--

Shirley Wu

I code too much & don’t draw enough. @ucberkeley alum, 1/2 @datasketches, @d3bayarea & @d3unconf co-organizer ✨ currently freelancing → http://sxywu.com