days
-1
-2
hours
-1
-6
minutes
-3
-9
seconds
-2
-7
search
Avoid the aggravation with these tips!

D3.js for the busy Angular developer

Stephan Rauh
D3.js
© Shutterstock / SFIO CRACHO

The awesome ngx-charts library is based on D3.js, so it is possible to use D3.js with Angular. In this article, Stephan Rauh of Beyond Java explains how to use D3.js with Angular correctly without driving yourself nuts.

When a problem is hard, you’re probably tackling it from the wrong angle. Using D3.js in an Angular application is such a problem. 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. Your boss will be impressed.

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 developer’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. I’ve stripped down the code as much as possible, assuming that 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 at first glance, 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 container 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:

<circle *ngFor="let d of data" [radius]="d.r" [x]="d.x" [y]="d.y" [color]="red"></circle>

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:

<circle *ngFor="let d of data" [radius]="d.r" [x]="d.x" [y]="d.y" (click)="d.r = d.r * 2" ></circle>}

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:

<svg class="bubbles">
    <g *ngFor="let d of data" [attr.transform]="'translate('+d.x+','+d.y+')'" (click)="onBubbleSelected(d)">
        <circle [attr.r]="d.r" [attr.fill]="'red'"></circle>
    </g>
</svg>    

Now it’s starting to look like an Angular component. 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 over time. 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

There’s still a lot to discover. We’ve managed to use the change detection of Angular to redraw the chart when the data changes. But it’s an immediate redraw, not a smooth transition to the new state. My co-worker Christoph Kaden tells us in the third part of the series how to do that. The second part of the series shows how to use the math engine of D3 to draw more complex charts.

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.

We’ll see in the next part of the series that it’s not always that easy, but that’s the key idea to integrate D3 charts in an Angular application.

This article was originally published on Stephan Rauh’s blog, Beyond Java

Author

Stephan Rauh

Stephan Rauh is a Java programmer dreaming about programming other languages: Scala, Groovy, Dart, Ceylon, Kotlin. Why don’t you have a look at Beyond Java?


Leave a Reply

Be the First to Comment!

avatar
400
  Subscribe  
Notify of