Responsive D3 Angular Directives

At Collaborative Care Initiative, I work in the mental health space, specifically collecting patient reported data and transforming the raw information into measure-based guidance for care providers and their patients.

Optimizing data visualization for stakeholders with different needs and varying levels of engagement is a major challenge. Helping both doctors and patients understand and potentially act on the information requires an iterative process melding sophisticated analytics and fine control of displays. AngularJS and D3 provide a powerful set of tools to setup and manage visualizations.

There are lots of articles and tutorials on AngularJS and D3, including several on working with them together. However most of those focus on initial setup and integrating the two. In this article I’ll be assuming those are solved problems and focus on how to manage multiple D3 visualizations in an established project. We’ll also have them be responsive to layout changes and abstract away some D3 boilerplate using ES6 classes and inheritance.

At the time of writing I am using the latest D3 version 4, and Angular 1.5. The examples and code in this article also assume an ES6 compatible setup (I’m using Babel).

In setting up this tutorial, I am going work backwards from the controller and template to the directive and finally to the classes used to organize our D3 logic. When all is said and done, we’ll be able to add multiple directives to generate different types of charts using an inherited implementation, thereby minimizing the amount of time you, the developer spend setting up visualizations!

All of the code shown in this write up is available as a fully working Angular app at: https://github.com/bakedbean/responsive-angular-d3

Let’s start with our main view and controller. For the purposes of this exercise, the controller is only responsible for providing some data for D3 to act on. We’ll use some hard coded JSON data to keep things simple:

This is pretty self explanatory. We’re using ES6 module syntax to export a function to be used as an Angular controller. We define a simple array of date and value objects on the controller’s this scope.

Our view is equally straightforward:

We’re using Angular’s controller as syntax which allows us to reference values defined on the controller’s this scope in the view. We then render the following custom directive and assign barData to its’ isolate scope:

<div bar-chart data="ctrl.barData"></div>

The directive is responsible for instantiating the chart class as well as handling responsive updates. Let’s take a look and then break it down:

There’s a bit going on here, but at the end of the day it’s pretty simple stuff. As directives go, this one is basic, no controller just a linker function. First we import the entire D3 library. It should be noted that in a real application destined for a production environment, importing the entire D3 library is heavy handed. One of the benefits of working with D3 v4 is the ability to only import the bits you are actually using.

We then import the BarChart class, which we’ll be exploring in more depth later. Next we export an ES6 module to represent the directive. The only dependency this directive needs is the $window wrapper that Angular provides for the standard window object.

The next important bit is at line 10:

angular.element(document).ready()

This delays execution until the document is fully loaded. This is important because we size the chart dynamically based on the size of the containing element of the directive. We need that element to be rendered to the DOM to know it’s final width.

We then declare a new instance of our BarChart class and call it’s render method. I’ll talk more about the BarChart constructor and methods in a bit. Finally we have this line:

angular.element($window).bind('resize', () => chart.clean().size().render());

This line is what handles the responsive aspect of the implementation. We bind to the window resize event, and fire off the methods required to re-render our chart each time. As I said, I’ll go into more details on those rendering methods shortly. Lastly is the actual directive configuration:

return {
restrict: 'EA',
replace: true,
link: linker,
scope: {
data: '='
}
}

This is standard Angular directive stuff, but I’ll go through it for the sake of clarity. EA restricts the directive to only being used as an element or an attribute. In the examples shown here I’m only using the attribute option, but it’s nice to have the element option as well. replace means simply that we replace the directive with the compiled results. Finally the scope declares our isolate scope which is just the data we want the chart to act on. That’s it. Now onto the chart.

I have found setting up D3 charts to be a very verbose and “lines of code” intensive undertaking. It’s somewhat the nature of the beast, but if you’re dealing with a lot of charts, like I do, then it can lead to A LOT of code to have to pick through, especially for non-trivial visualizations.

The easiest area to create some reusable components is when it comes to setting up the svg element and sizing it. Having flexible components to size the svg and child elements is also critical to allowing for a responsive chart. Let’s take a look at what a typical D3 bar chart implementation looks like:

This is taken right off Mike Bostock’s examples (http://bl.ocks.org/mbostock/3885304). As bar charts go, this is probably one of the most basic ones you’ll find. Looking at this implementation we can start to isolate the bits that can be reused for any responsive chart. Anything having to do with sizing the chart is what we want to break out.

var svg = d3.select(“svg”), 
margin = {top: 20, right: 20, bottom: 30, left: 40},
width = +svg.attr(“width”) — margin.left — margin.right,
height = +svg.attr(“height”) — margin.top — margin.bottom;
var g = svg.append(“g”).attr(“transform”, “translate(“ + margin.left + “,” + margin.top + “)”);

These lines are the main elements that determine the size of the svg and the placement of a g container that then holds the various chart elements. If we focus on these bits, we can start to cobble together a base class for creating responsive charts. Let’s take a look and then break it down:

ok, so there’s a bit here to discuss. Let’s start with:

const BREAKPOINT = 400;

This is used to determine the width in pixels that we want to switch from a desktop layout to a mobile layout. For the purposes of this article, I am only setting one breakpoint, but you could conceivably have multiple for different device targets, and act accordingly on them.

We then get to the class constructor. Here we define three properties: id, d3 and element. id is a string used to uniquely identify the svg in the DOM. d3 is our D3 object that we setup in the directive. I’ve chosen to pass it into the class like this since it affords more flexibility than referencing it globally on window or something like that. This way you can load D3 any way you wish, and then pass through the final implementation. Lastly we pass in the DOM element from the directive.

The next method is margins. This method takes a configuration object of margin sizes and sets up a property on the class: this.margin. This can be used when setting up the svg. The configuration object can be any of the following properties:

{
top: INT,
bottom: INT,
left: { sm: INT, lg: INT },
right: { sm: INT, lg: INT }
}

The left and right properties accept objects that allow for defining margins for small and large views. These are then toggled based on the BREAKPOINT value. We then setup the margins with defaults of zero if no corresponding values are provided. If you look back at the directive implementation, you’ll notice that we declare a const SIZE to set the margins.

Next up is our size method:

This accepts one argument which is the size configuration object. We then set the width for the svg:

this.width = this.element[0].offsetWidth — this.margin.left — this.margin.right;

Note how we’re using the offsetWidth from the directive’s element. As long as the directive blocks the instantiation of the chart class until the DOM is loaded, this guarantees properly sized charts. We then create the svg and assign it the id, width and height class properties that were defined in the margins method.

The last method on the base class is clean. This method simply removes the element with the corresponding id set by the constructor. Note that both size and clean return this. This is so we can chain off these methods, which we’ve seen an example of in the directive when handling responsive updates. That’s it for the base class. We can now setup chart classes that extend this base class and can take advantage of these methods. Let’s take a look at a chart class that extends the base class:

Most of what’s going on in this class is a simple restructuring of the same elements we saw in the generic bar chart example. First off we extend the BarChart class with our BaseChart class. The rest of the interesting bits are in the constructor and render methods. In the constructor we call:

super('bar-chart-' + id || 'bar-chart', d3, element);

For those unfamiliar with ES6 classes, calling super first in the constructor allows us to reference the parent class methods on the this scope. We pass in the custom id (if defined) the d3 and element objects. I’m appending bar-chart- to the id just in case there’s some other element on the DOM with the same id.

We then call this.size and define our custom sizing configuration (if necessary). Since this.size returns self, we can then chain the configure method onto it. In this case, I use the configure method to do any custom data munging necessary for D3 to use the data. I then define methods for the x and y axis and then a block of renderers. Much of how this class is organized is matter of opinion. If you don’t agree, or have different aesthetics, feel free to take it in a different direction!

The last piece of the puzzle is the render method. Here we add our g container onto the svg and position it, using the size values determined by our base class. We then call each of the renderers.

So given all of that, let’s take another look at our directive:

Hopefully now, this is making a bit more sense, and the power of having our D3 functionality represented in a class is more clear. When the directive detects a resize event, we can see now that it first calls clean which removes the svg by its’ id, then thanks to returning self, we can chain size which sets up our newly sized svg and margins, and finally re-render all the renderers.

We could easily whip up a new directive for a line chart, that line chart could extend our BaseChart class, then all we’re concerned with is building the visual elements of that chart. We can take that new chart directive, stick it into a layout div on our template and rest assured that it will size properly and respond to device size changes independent of any other chart directives in the same template.

If you’ve made it this far, I am impressed. Remember that all of the code shown here, plus some additional stuff is available in a fully functioning development app here: https://github.com/bakedbean/responsive-angular-d3. The app ties it all together with angular modules, some basic routing and multiple charts in one view. It also uses a basic webpack configuration to run the app in development.

Want to learn more about what we’re doing at Collaborative Care Initiative? Click here to explore the benefits of engaged care through measure based data.

Show your support

Clapping shows how much you appreciated Eben Goodman’s story.