D3 is awesome. If you haven’t perused the D3 gallery before, have a look and prepare to nerd-out on some incredibly cool data visualizations. Here’s a simple graph built with D3.
We’ve split up and reformatted the source for clarity, but otherwise this example was copied verbatim from the D3 examples.
The markup is reasonable. Concise even. How about the JavaScript?
Did you get all of that? If you look carefully you’ll find the chart’s behaviour, structure, and appearance are all defined here. As a demonstration of JavaScript mastery, its brilliant. That said, you wouldn’t build a website node by node with jQuery, so why build your visualizations from the ground up with D3?
Separation of Concerns
Fortunately, AngularJS is here to help. Using some custom directives, we can take a more declarative approach to building this chart. Our goal here is modularity and maintainability.
View
We’ll start by creating a simple Angular app. For brevity, we’ve removed most of the boilerplate and only shown the body
element.
It’s really not that different from the pure D3 version. We’ve used three attribute directives that come built into Angular (ng-controller
, ng-model
, and ng-click
) and added a custom chart
directive. You can already see an advantage to this approach – you can tell where your chart will be rendered without reading the JavaScript!
Controller
Here we see more familiar elements from the D3 example. We’ve attached dimensions, scales, and chart data to the controller scope and channelled our inner Uncle Bob into some clearly named sort and map methods.
The most notable absence is the DOM manipulation code previously used to insert the SVG elements. We are able to eliminate it by using the custom chart
directive. Now let’s dig a little deeper to understand how that works.
Chart
This is the structure of our main directive. It sets the dimensions of the chart and includes the axes and bar components.
Again, it’s clear where the chart components will render. The directive definition is as simple as they come.
The only real reason for not including the chart in view template is that it uses the SVG namespace.
X-Axis
D3 will still do most of the heavy lifting in terms of creating and updating the axes. This single-line template is all it takes from us.
You might be wondering why the transform
attribute is prefixed with ng-attr-
. This tells Angular not to include the attribute until {{height}}
is defined. Now onto the directive definition.
We’re starting to see the major differences between these two approaches. The axis-x
directive watches for changes to x.domain()
(which it inherits prototypically from the controller scope) and triggers a transition. Contrast this to the original example where a single event handler updated the scale domain, transitioned the bars, and transitioned the axis.
Y-Axis
To keep things exciting, we set the transclude option in the axis-y
definition. This let’s axis-y
wrap arbitrary content. In this case, we use transclude for the simple purpose of labelling the y-axis, but it is very powerful and worth understanding.
Bar Directive
A bar is just a rect
with some dimensions and position. We purposely don’t set the x
attribute so that D3 can animate it.
In the bar
directive link
sets the initial value of x, and creates an event handler to animate x when the domain changes.
The biggest challenge in writing these directives is understanding the state of scope
when when link
runs. The bar
directive is simple: it is created by ng-repeat
once scope.bars
is defined. The axes are more complicated. They are added to the DOM (and link
runs) before their respective axis domains are specified. We use scope.$watch
to detect domain changes, and render the axis immediately the first time and animate it on subsequent changes..
Is it worth it?
You’ve probably realised that the D3 with AngularJS version of the chart requires a lot more code than the original example, and might be wondering if it’s worth the additional effort, bandwidth, etc. If you are designing a simple web page or infographic, the answer is probably not. This approach really shines when your app starts to scale and you are able to reuse and extend chart components.
What next?
A great place to start building your reusable visualization library is with the chart
element. It’s common to see margins and dimensions defined in the first few lines of D3 examples. A generic chart
directive could perform these calculations and transclude any number of chart components.
Breaking the charts down into directives is a little more work, but ensures that your visualization doesn’t spiral into a 500-line monolithic behemoth. You can find the full source for this post here.