- 6 minutes read

When a problem is hard, you're probably tackling it from the wrong angle. Some time ago I've contributed a pull request to the awesome ngx-charts library which is based on D3.js. So I knew for sure that it's not that difficult to use D3.js with Angular. But still, each time I find a fancy D3.js chart and ask one of my co-workers to add it to our Angular application, I drive them nuts.

What's the secret of ngx-charts? How to use D3.js with Angular correctly?

I took me some research to find out what we were doing wrong. You'll be surprised how simple it is to do it right.

What's the problem?

Don't get me wrong: D3.js is a great library with great documentation. Granted, it hasn't been written for the newbies, but even the newbies get along after a couple of days. If your goal is to create interactive charts, D3.js is just great.

Things look a bit different if you're an Angular programmer. The API of D3.js is an ill match for the API of Angular. There's no support for the template language. That's not surprising because the average D3.js chart is a JavaScript function without HTML. From the Angular programmer's point of view, that's bad: You can't bind the properties of a D3.js chart to the values of an Angular component or service.

OK, that was a bit abstract. Let's have a look at real source code. A typical D3.js chart looks like the code snippet we've used to draw a bubble chart. Before you ask: as an experienced D3.js developer, you'll probably miss a bit or two. I've stripped down the code as much as possible. In particular, I assume the coordinates and radii of the bubbles have already been calculated and stored in the variable data.

Here we go:

const node = svg .selectAll('.node') .data(data) // <<< .enter() .append('g') .attr('class', 'node') .attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; // <<< }); node .append('circle') .attr('r', function(d) { return d.r; // <<< }) .style('fill', 'red');

If you don't get this algorithm, don't bother. I've marked the essential parts with arrows. The algorithm works on an array of data (pack(root).leaves()). For each array element, it creates a graphical object (append('g')) and puts it at certain co(ordinates on the screen (transform="'translate(' + d.x + ',' + d.y + ')'"). Finally, it draws a circle (append('circle')) with a certain radius (attr('r', d.r)).

However, as an Angular developer, this algorithm puzzles me. I'd expect something along these lines:

D3.js uses function calls to describe the elements of the charts. Angular uses an HTML-based template language.

Linking properties and events to Angular components and services

Now you might argue that this is just another way to describe the same chart. In a way, it is. But there's an important difference. The traditional D3.js chart I've shown above is static. It won't redraw if the data changes. However, that's one of the core ideas of Angular. To change the radius of the bubbles, just modify the corresponding attribute in the Angular component.

Even more important is that you can't call an Angular method from a D3.js event handler. Basically, that's the difference between onclick and (click). Probably it's possible to call a method of an Angular component from an onclick handler, but it's a bad idea because you circumvent the lifecycle and the change detection of the Angular application. If you're unlucky, your method is called, but the HTML page is never updated. I have to admit I didn't try it, but I'm sure you'll encounter all kinds of weird effects.

Cutting a long story short, what we need is something like this:

Why don't we just use the Angular approach?

Here's the catch. Every D3.js documentation I've seen to far describes the algorithmic approach. But that doesn't mean you have to use it. It's just an option. The alternative is to create the SVG graphics using the Angular template language. In this case, the role of D3.js is to calculate the positions and sizes.

So I ran the algorithm I've shown above, inspected the HTML code in the developer tools of the browser, and translated it to the Angular template language. The result looks like so:

That's much better. We've linked the chart with the attributes and methods of the underlying Angular component. As a side effect, we see how D3.js works. Because that's something missing in the documentation. It tells you how to do things, but it offers little in the way of a deeper understanding. I suppose you pick that up with some experience. In our case, things are obvious: the enter() function of D3.js is simply a glorious for loop creating SVG image elements.

The only part we need D3.js for is calculating the coordinates. In our case, we put this algorithm into the ngOnInit() method of our component. Mostly, that's the typical code usually found at the beginning of a D3.js algorithm, so I'll show it without explaining. The bottom line is that we store the D3.js data structure in an attribute of the component (this.data = pack(root).leaves()):

const pack = d3.pack() .size([this.width, this.height]) .padding(1.5); const root = d3.hierarchy({ children: this.getBubbles() }) .sum(function(d) { return d.value; }); this.data = pack(root).leaves();

Limitations of this approach

The Angular change detection updates the chart immediately when you update the data. It doesn't generate smooth transitions. I've managed to find a solution for that, too, but I don't know if I like to, so I'll leave this for a follow-up article.

Wrapping it up

Integrating one of the D3.js charts you find on the internet into the Angular lifecycle boils down to a few simple steps:

  • Inspect the D3.js chart in the browser.
  • Copy the SVG node generated by D3.js and put it into an Angular HTML template.
  • Bind the attribute values to variables of the Angular component.
  • Replace the enter() method of D3.js by an *ngFor loop.
  • Calculate the D3.js data structure on the left-hand side of the enter() method and store it as an attribute of the component.

Comments