Leveling Up D3, Events and Refactorings
Last published on February 25, 2016 by Marcos Iglesias
This article was originally posted on the Eventbrite Engineering Blog.
This is the third and final post in the Leveling Up D3 series. In the first post, we talked about the Reusable Chart API and its advantages. And in the second article, we test-drove a basic bar char. In this post, we will carry out some refactoring and optimizations made possible by our new, shiny, and high-quality code.
Personal Note
I love refactoring and evolving my code, as I greatly value being proud of the code I produce. In general terms, in the Software Development field, it is pretty common to introduce bugs when new features are added to a product. This obviously isn’t ideal, but people have gotten used to it. However, when we talk about improving a piece of code that is already on production, organizations do not like adding bugs when pushing those changes. And that’s why the previous steps were so important. Without the tests generated in the last post, we wouldn’t have the necessary confidence to refactor our charts code once it had been shipped.
Accessors Refactoring
The first refactor I want to show you deals with the accessor functions for our public interface. Consider this code:
/**
* Gets or Sets the margin of the chart
* @param {object} _x Margin object to get/set
* @return { margin | module} Current margin or Bar Chart module to chain calls
* @public
*/
exports.margin = function (_x) {
if (!arguments.length) return margin;
margin = _x;
return this;
};
/**
* Gets or Sets the width of the chart
* @param {number} _x Desired width for the graph
* @return { width | module} Current width or Bar Chart module to chain calls
* @public
*/
exports.width = function (_x) {
if (!arguments.length) return width;
width = _x;
return this;
};
/**
* Gets or Sets the height of the chart
* @param {number} _x Desired width for the graph
* @return { height | module} Current height or Bar Char module to chain calls
* @public
*/
exports.height = function (_x) {
if (!arguments.length) return height;
height = _x;
return this;
};
We can easily spot a repetition pattern here, right? These methods do almost the same thing but for different parameters. Additionally, as the public API of the bar chart grows, the repetition will be even more flagrant. There is something else we should take a look at. Check this code section at the top of the file:
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 960,
height = 500,
gap = 2,
data,
chartWidth, chartHeight,
xScale, yScale,
xAxis, yAxis,
svg,
//...
It is not clear which ones of these variables are public (and therefore accessible via accessor functions) and which ones are private. Let’s fix it! The idea is to have two lists of attributes: public and private. We will loop over the public attributes list and dynamically generate an accessor function for each item in that list. The first step is to pick an attribute and unify its interface inside our chart. For that, we need to swap the mentions of the attribute with a call to its accessor (instead of accessing height
, we call exports.height()
). If we chose margin, this is how it would look like in code:
_selection.each(function (_data) {
chartWidth = width - exports.margin().left - exports.margin().right;
chartHeight = height - exports.margin().top - exports.margin().bottom;
data = _data;
buildScales();
buildAxis();
buildSVG(this);
drawBars();
drawAxis();
});
We will do this for each public attribute (height, width, and margin), checking that our unit tests still work. But this call to exports
still look a bit weird, doesn’t it? So let’s also change the exports
name and move it to chart
, resulting in the following code:
_selection.each(function (_data) {
chartWidth = chart.width() - chart.margin().left - chart.margin().right;
chartHeight = chart.height() - chart.margin().top - chart.margin().bottom;
data = _data;
buildScales();
buildAxis();
buildSVG(this);
drawBars();
drawAxis();
});
That looks much better! Now let’s think about our attribute getter/settter code:
/**
* Gets or Sets the margin of the chart
* @param {object} _x Margin object to get/set
* @return { margin | module} Current margin or Bar Chart module to chain calls
* @public
*/
exports.margin = function (_x) {
if (!arguments.length) return margin;
margin = _x;
return this;
};
If we assume we are going to keep our public attributes tidy within an object called publicAttributes
, a possible form of generating the equivalent of the previous snippet could be:
/**
* Create an accessor function for the given attribute
* @param {string} attr Public attribute name
* @return {func} Accessor function
*/
function generateAccessor(attr) {
/**
* Gets or Sets the public attribute of the chart
* @param {object} value Attribute object to get/set
* @return { attr | chart} Current attribute value or Chart module to chain calls
*/
function accessor(value) {
if (!arguments.length) {
return publicAttributes[attr];
}
publicAttributes[attr] = value;
return chart;
}
return accessor;
}
This generator function returns a function that will be an accessor for the attribute passed to the generator function. Let’s define publicAttributes
as:
// Attributes that will be configurable from the outside
var publicAttributes = {
margin: {
top: 20,
right: 20,
...
},
width: 960,
height: 500
};
Now, we can apply our generator to our public attributes with:
/**
* Generate accessors for each element in attributes
* We are going to check if it's an own property and if the accessor
* wasn't already created
*/
for (var attr in publicAttributes) {
if (!chart[attr] && publicAttributes.hasOwnProperty(attr)) {
chart[attr] = generateAccessor(attr);
}
}
And that’s it! We have generated accessors in our chart! A nice advantage of this code is that we highlight which attributes are public and which are private. We can also change this segregation by just moving attributes from one group to the other, super easy!
Adding Events
Now that we have a fully refactored bar chart, we can start thinking about communicating our chart with other components. Maybe we want to implement a fully-fledged tooltip, or maybe just an interactive legend or some other functionality that would react to a ‘hover’ event on one of the bars of our chart. In order to do this, we would need to make our chart trigger events when these actions happen. But first, we are going to write the test we want to pass. It looks like this:
describe("on hovering a bar", function () {
beforeEach(function () {
this.callbackSpy = jasmine.createSpy("callback");
barChart.on("customHover", this.callbackSpy);
});
it("should trigger a callback", function () {
var bars = containerFixture.selectAll(".bar");
bars[0][0].__onmouseover();
// arguments: data, index, ?(always 0)
expect(this.callbackSpy).toHaveBeenCalledWith(dataset[0], 0, 0);
});
});
I have highlighted two lines in the previous code. The first is the API that we want for our bar chart events. This might feel really familiar as it looks exactly the same as what we would get using jQuery or other general purpose JavaScript libraries. The second emphasized line shows how we can use an specific property of d3 which appends the blinded events of an element directly onto the DOM with the prefix ‘__’, to trigger an event on the DOM element.
NOTE: Do you know why the third argument of the hover callback is 0? I couldn’t find it anywhere! In order to make this test pass, the first thing we need to do is to declare the events that our chart will trigger, and for that, we will use d3’s dispatch object. Consider the following:
// Dispatcher object to broadcast the 'customHover' event
dispatch = d3.dispatch('customHover'),
Next, we will use this dispatcher object to emit events when necessary -- in this case, when the user mouse goes over one of the bars:
// Enter
bars.enter()
.append("rect")
.classed("bar", true)
.attr({
width: barW,
x: chartWidth, // Initially drawing the bars at the end of Y axis
y: function (d) {
return yScale(d.frequency);
},
height: function (d) {
return chartHeight - yScale(d.frequency);
},
})
// This is basically saying: when mouse hovers a bar,
// trigger the customHover event in the dispatch object
.on("mouseover", dispatch.customHover);
Now for the event to be accessible from the outside, it needs to be bound to the chart. Since it is an object, we could use a method like $.extend
(on jQuery) or _.extend
(in underscore), but it happens that there is a d3 method that does something similar: d3.rebind. This is how it looks in our code:
// Copies the method "on" from dispatch to exports,
// making it accessible from outside
d3.rebind(chart, dispatch, "on");
And those are all the modifications necessary inside our chart. In order to use this, we will do:
barChart.on("customHover", function () {
/* user code ... */
});
Now we can easily send the highlighted element’s information to a tooltip, highlight an element of a legend, or any other interaction that we want to achieve. All this without breaking the encapsulation of our bar chart or coupling it with another object!
Summary
And with this post, we have arrived to the end of this series! If I would have to describe what having tests on my D3 code brings me, that would be “peace of mind.” I could say this whole series is about that: achieving peace when developing D3 charts, and I wanted to share it with you. I hope that you all have learned about other ways of building d3.js charts and, at the very least, that you will contemplate writing some tests for your next implementation. Do you know other ways of doing this? Tell us about your attempts at testing D3 code by leaving a comment or pinging me @golodhros.