I recently picked up a copy of Interactive Data Visualization for the Web (link) by Scott Murray to learn a bit about how to use D3.js. About the same time I started messing around with both Angular and Coffeescript. In this post, I'll show you how to reproduce one of the examples in the book using D3, Angular, and Coffeescript together.

Here's where we are going -- a bar chart that updates with a new random value every time you click. Although basic, this example demonstrates some of the important features of D3 and Angular:

  • how to bind data to elements
  • selecting elements that are entering, exiting, or updating in the chart
  • using transitions to generate nice looking updates
  • implementing custom directives and two-way data-binding in Angular to create a chart that auto-updates when the data changes

I'll assume you have some working knowledge of D3 and Coffeescript and are more interested in how to get it to work within Angular. The code for this example is hosted on Github.

Setting up Angular routes and our data model

To start, let's set up Angular with a route, controller, and template for the view that contains our chart.

In App.coffee, we start by defining our app as d3testApp and declare dependencies d3testApp.controllers and d3testApp.directives. We then configure a route to render a template and bind it to our data model in the controller MainCtrl.

In our controller Main.coffee we set up our data model and methods.

Our data model is defined in $scope.data and consists of a list of hashes, each with a time and data field. In D3, we will render the data components as the height of the bar and use the time as a key value that will help to properly bind our data to elements over successive updates. The function $scope.updateData pushes a new random datapoint onto the array up to a maximum of $scope.dataLength elements.

Using Angular directives to render and update the graph

Here's where the start of the magic happens. We'll set up a directive to bind data changes to a function that will update the graph. In Directives.coffee we'll create a directive called scVisualization.

Let's step through this in detail. We declare an angular module named d3testApp.directives. The directive is named scVisualization. The return statement sets up the data-binding that we want and declares the actions we want the directive to take when changes happen.

The first, restrict: E restricts this directive to use as a tag in the document markup, rather than as an attribute somewhere else. scope: { val: '=' } sets up a two-way data-binding between our controller and this directive. We define the binding in our view by using this new directive and setting up a relationship between val and the data property in our controller. The link statement declares the function to be evaluated when the bound data changes.

In this callback, we first call a helper function that will append an SVG element when the page renders with the first data point. We then set up a $watch on val. The second argument is a function updateGraph that is called with the old and new values of val any time the data changes. The last argument, set to true, is very important for the way our data model is implemented. By default, $watch only evaluates updateGraph on changes to the object reference indicated by val. Setting the third argument to true ensures that we do an object comparison between the old and new values of val which will fire the graph update any time we push a new value onto the array.

Now, let's look at the createSVG and updateGraph functions.

In createSVG I check for the existence of a svg on the scope. If not, I create an SVG element and bind it to the scope. Important variables like width and height are also properties of the scope so that they can be easily passed to the updateGraph call.

Why do we put these properties on the scope? Angular's $watch calls updateGraph with only three arguments: newVal, oldVal, and scope. We separate the two functions so that we can pass the element reference to add the SVG to the DOM in the proper location. Afterwards, the SVG element persists on the scope and we will use it to grab references to the RECT elements in updateGraph.

Here's updateGraph:

I won't go through this in detail, but let me point out a few important things. Updating elements in this type of chart follows a D3 design pattern:

  1. Bind your data to the SVG elements, in this case a bunch of RECTs, using svg.selectAll("rect").data(...).
  2. Since we are pushing new data one point at a time, we also specify a key function when we bind our data using .data(newVal, (d) -> d.time). This keeps the data bound to the appropriate SVG element as elements enter and exit the chart.
  3. Update the existing elements using a transition, in this case bars.transition().
  4. Create new SVG elements for data that is entering the chart, here using bars.enter(). You can add transition effects, delay, etc.
  5. Select and remove elements that are leaving the chart with bars.exit().remove().
  6. Finally, we apply the attributes to all the elements to start the transition and move them to the updated location.

To glue it all together, let's take a look at main.html the view that will bind our data, graph updates, and handle user interaction to create new random data points.

This one is pretty simple. We create an element, in this case a link and bind it via an Angular click event to our function updateData. This will add a new random data point on each user click. The directive is called using the element sc-Visualization. The binding between our data and the directive is accomplished by linking val="data".

That's it. If you have any improvements, please leave me a note in the comments. And if you've read this far and find it useful, follow me on twitter for future posts.