Replacing (most of) d3.js with pure SVG + AngularJS

Over the last few months, I've been feeling increasingly unsatisfied with the options we have for integration between d3.js and AngularJS. In my earlier projects, I used Brian Ford's approach of creating a directive for the visualisation I need, and adding the d3 code in the link function of the directive. This is simple and works, but it feels like a very superficial integration. Effectively the Angular directive creates a hole in the DOM processing cycle within which d3 can work.

The good guys at BayesHive came out with an alternative approach called Radian, that felt closer to what I wanted. They rely heavily on small, pre-existing Angular directives to declaratively create d3 visualisations. This means that a lot of the information about how to configure the graph lives in the HTML, as it should.

Thinking about it further though, what Radian is doing is using directives (masterfully) to trigger d3, to create SVG tags, so that the browser can finally render a visualisation. But if the browser already contains a declarative language that allows specifying visualisations in SVG, why do we have to jump through so many hoops?

Dividing responsibilities

Perhaps in order to understand the note of discord that's been bothering me, it may be best to take a step back and play a game. Out of D3 and Angular, try to guess which of the two has the following quote on its homepage:

"[ProjectX] allows you to bind arbitrary data to a Document Object Model (DOM), and then apply data-driven transformations to the document."

And which one has this:

"For example, you can use [ProjectX] to generate an HTML table from an array of numbers."

Or this:

"Create an interactive SVG bar chart with smooth transitions and interaction."

Educated guesses in hand, let's look at the answers.

The third one is easy. It's from D3's homepage. Angular can't create SVG charts. (or can it?) What's surprising is that the other two quotes are also from the D3 homepage.

It seems d3 sees itself as kind of a data binding library, that happens to be good at visualisations. This usually confuses newbies to no end, but makes sense once you get the hang of d3. But bindings are also a core pillar of Angular's way of working. So using both leads us to having two duelling libraries, both trying to control the DOM. In order to get what we want, we need to understand both and use both. This doesn't feel right. So before figuring out how to do the integration, it may be best to first decide who will do what.

Let's start without D3 for a moment

Perhaps the best way to continue this investigation is to try and instrument an SVG visualisation purely with AngularJS, and see what we could be missing. Afterall, SVG is markup that works in HTML5, and Angular is good at manipulating HTML5. Once this way of looking at visualisations sinks in, you might even wonder if we will need D3 at all.

Let's start by adding some data to our controller:

$scope.graph = {'width': 265, 'height': 134}

Then, we'll need an SVG element in our template. An Angular coder would probably write something like this:

<svg height="{graph.height}" width="{graph.width}"></svg>  

Simple, yes? Not so fast! This is the first pitfall you'd find if you go this route. Apparently SVG elements are more sensitive than other HTML5 elements, and don't accept invalid attribute values. This means that the SVG parser will throw errors before Angular has time to come in and replace the interpolated values. For this reason, AngularJS 1.1.4 added an excellent feature, ng-attr. If our template looks like this instead, it will work fine:

<svg ng-attr-height="{{graph.height}}" ng-attr-width="{{graph.width}}"></svg>  

That's because ng-attr-height does not trip up the SVG parser, not being a proper SVG attribute to begin with, and when Angular parses it it will convert it to the height attribute set to the correct value. As such, the SVG parser is never presented with an attribute that has an unparseable value, and never throws an error.

Let's now add some visual to our -isation. Perhaps something basic such as circles?

<svg ng-attr-height="{{graph.height}}" ng-attr-width="{{graph.width}}">  
    <circle ng-repeat="circle in circles" 
        ng-attr-r= "{{circle.r}}">

In our controller, we'll need to add the circles property. Let's seed it with some source data:

$scope.circles = [
    {'x': 15, 'y': 20, 'r':30},
    {'x': 35, 'y': 60, 'r':20},
    {'x': 55, 'y': 10, 'r':40},

This is what the end result looks like. Click on the "Code" button to review the full source of the running example below.

* Update: I have now made a much more substantial example of using plain AngularJS and SVG which you can see here*

Allright, this is starting to look like something. An SVG visualisation that can be generated (and updated!) directly from a JSON object. The code is quite terse and fully declarative. The bindings also work just fine using angular's fine ng-repeat machinery, so you can do fancy work just by modifying the $scope.circles object at runtime with regular JavaScript, as long as you work with Angular's digest cycle. To my eye, the HTML also looks much more approachable and modifiable than the equivalent D3 incantations.

In fact, if you want to add animation to your visualisation, AngularJS has a homegrown ngAnimate library, obviating the D3 .enter() and .exit() song-and-dance. Though one could make an argument that the enter/exit metaphor is still there in the CSS. If you want to know more about how to add animations to an AngularJS visualisation, you can do worse than this excellent tutorial by yearOfMoo, the primary coder behind ngAnimate. Adding animation to the above example is left as an exercise to the interested reader.

Or maybe we need D3 afterall?

So far so good, Angular can cope. But when trying to do the most famous of visualisations, the line graph, our approach hits a wall. Line graphs use svg paths, specified in a peculiar mini-language, which I presume hails from the depths of SVG's ancestry, possibly PostScript. It looks something like this:

<path d="M150 0 L75 200 L225 200 Z" />

This is where Angular falls down. What we need is the ability to convert a sequence of points to this path string, but Angular has no built-in machinery for this. We could perhaps implement a function to convert a series of points to an SVG path, but that would be a lot of code and thankfully there's no reason for that anyway. D3 has d3.svg.line() that generates just the right kind of string. It can even do some pretty sophisticated interpolations. What's more, due to d3's excellent modular design, we don't have to use the rest of d3 if we don't want to.

Say we have a series of points we want to convert into a line:

$scope.points = [
    {'x': 3,  'y': 7 },
    {'x': 5,  'y': 15},
    {'x': 7,  'y': 8 },
    {'x': 11, 'y': 17},
    {'x': 13, 'y': 13},
    {'x': 17, 'y': 23}

We could do that using d3's functions as follows:

x = d3.time.scale().range([0, $scope.graph.width]);  
y = d3.scale.linear().range([$scope.graph.height, 0]);

x.domain(d3.extent($scope.points, function(d) {return d.x}));  
y.domain(d3.extent($scope.points, function(d) {return d.y}));

$scope.line = d3.svg.line()
  .x(function(d) {return x(d.x);})
  .y(function(d) {return y(d.y);});

And this is what that looks like:

Here we've used d3's functions to generate the path for the line, but d3 itself never touches the DOM. That is left to Angular, keeping a clean division of responsibilities: Angular for binding, and d3 for visualisation domain magic.

You may be wondering how we'll go about adding Axes to our graph, but here be dragons, so I'll leave that aside for the moment.

And now for something a bit more complex

But what about all those beautiful diagrams d3 is known for? Like the force-directed graph for instance. How would we recreate that in Angular/SVG? It turns out that the force layout is heavily dependent on the d3.layout.force() engine. Thankfully this engine is more-or-less decoupled from the DOM so we can pair it with Angular quite nicely:

In the embedded window above, pressing 'Code', selecting a file, and then pressing 'Preview' again will show you the animation from the start.

The original D3 example this was based on can be found here for comparison. Dragging nodes doesn't work, but can be added by tacking on a 'draggable' directive, similar to this to the graph's nodes. Also, depending on your system, you may notice that the Angular version is a little slower than the pure-d3 version. This is an artefact of how d3's engines expect to be used. This will not be solved until either the force layout is re-implemented to respect Angular's digest cycle, or Angular implements object.observe(), (which Team Angular are working on), making the digest cycle a transparent internal detail.

Early days for an integrated approach

Overall, it seems that AngularJS implements the declarative, data-driven documents vision Mike Bostock had for d3. It's just that d3.js isn't a big part of the final picture when it comes to data binding. Angular, in the process of completing a disruption of the jQuery paradigm, has implemented many of d3's best ideas. d3.js was a pragmatic way of bringing these ideas to the fore by working in the visualisation space without upsetting the status quo of managing the DOM, but the ideas are so much more powerful when used everywhere, uniformly.

My own experience in doing visualisations using Angular/SVG first and judiciously adding d3 helper functions (or the occasional directive) when needed, is that I finally 'get' SVG a lot better, and feel much more empowered to do what I want, than when trying to manipulate D3's abstraction over SVG. But this only applies for the kinds of visualisations that can currently be done with the Angular/SVG combo. On the other hand, D3's nicely modular computation functions are an excellent API that mostly needs no change.

As of right now, anyone wanting to do Angular/SVG visualisations has no helpers or examples at their disposal, and will have to reinvent a few wheels. We're in early proof-of-concept days, with an architectural insight as the only guide. I wouldn't advise taking this anywhere near production, unless you know exactly what you're getting into, but if you would like to experiment with an avant-garde approach to visualisation, this is for you.

As people experiment more, I expect this state of affairs will change. Many of d3's functions can be used as-is. Others mix DOM updates with computation, so that pattern clashes with Angular.

D3 took us way forward in web visualisation. With AngularJS rapidly maturing, there's (theoretically) no longer a reason to use two separate ways of doing the same thing. Same as Bootstrap got integrated into Angular first with wrapper directives and then by a native re-implementation, I expect d3 to be first wrapped and then reimplemented in an Angular-compatible way.

Enter Triangular.js

I've already done some work in that direction with a library I call triangular.js (get it?). I've made what I have available on GitHub to use as you please. It is more of a code example than an actual codebase at the moment, mostly consisting of the pieces necessary to make a linechart that animates as the source data changes (another exercise for the abnormally interested reader!). For those of you brave enough, please do give it a look and send pull requests so we can build it into something more useful. As I said we're in the early days, so most of the philosophy around the library is open to rough consensus and running code. It's initial approach is to try and wrap the necessary parts of D3 into Angular directives, but inevitably, if this project is to be completed appropriately, it will need to replace parts of D3 with code that respects Angular's digest cycle.

The future is not actually about Angular

I may have tricked you into thinking that this post was about Angular with my writing so far. I did actually write it in the title, so you're not to blame. In fact, I doubt any non-Angular users have read this far. And that's too bad because this post isn't really about Angular, all things considered. In fact, Angular may just be a temporary step in having this sort of functionality built into every browser out there.

With Web Components and Template Binding coming down the standards pipeline, that day may not even be very far. We as web developers will have to learn to code in this style, and D3 will have to evolve for a world where browsers can deal with data-binding natively. If it continues to live as a framework in a world where its ideas about binding have dominated, it will have to focus on providing the additional computational functionality needed for beautiful visualisations. Same as AngularJS, d3's ultimate success may be to be no longer needed, when the Web can natively do what each library's authors thought the web should do.

Update: Check out Chris Garvis' adaptation of a bar chart if you're looking for some more sample code in the same style.

Update: @Rich_Harris pointed me towards RactiveJS which has bindings and strong SVG support. And it looks very pretty. Hopefully we'll get AngularJS to be that smooth too, soon enough.

I look forward to your comments, here or on Twitter.

Alexandros Marinos

I love to take the web to new places. Currently working on to bring web programming in contact with the physical world, so you can 'git push' JS to your devices.

comments powered by Disqus