Angular / d3.js / charts / animations / transitions

Animations with D3.js and Angular

Posted on by Christoph Kaden
Guest author's biography
Christoph Kaden is working as a Software Developer at OPITZ CONSULTING Deutschland GmbH. He’s been developing web applications, JavaScript-based frameworks and even an application server for several years.
Let's choose a simple example to explain how it's done. The full-blown pie chart component is much more complicated because of many mathematic calculations. If you're interested in all the gory details, have a look at the source code of the full pie chart component, which is available at our corporate GitHub repository https://github.com/opitzconsulting/ngx-d3.

What’s the problem?

Basically, developing animations with D3.js is easy. It takes only a few lines of code because of the functional programming:

svg
  .selectAll('rect')
  .attr('x', 10)
  .transition()
  .duration(1000)
  .attr('x', 100);

What‘s going on here? Inside the SVG element each rect element will be moved to the x coordinate 10. After this, an animation is defined by using the transition method. Then during 1000 milliseconds the movement of the elements to the x coordinate 100 will be done with a smooth animation. This is almost trivial, don't you think?

Remains the challenge how to translate that to Angular.

Suppose we've got an array rects in an Angular component with coordinates and dimensions for each rectangle. The HTML file for this component may look like this:

<svg>
  <rect *ngFor="let rect of rects"
    [attr.x]="rect.x" [attr.width]="rect.width"
    [attr.y]="rect.y" [attr.height]="rect.height"
    />
</svg>

Thing is, an animation following this approach results in inconsistent data. The animation would work, but the x coordinates will be adjusted directly on the individual rect elements and not within our rects array. A change of the x coordinate within our array will no longer affect our rect elements.

In other words: using this approach we will lose the property binding between HTML template and TypeScript logic – one of the core features of Angular.

Let me add an essential hint at this point. Within the method in which we create the array elements, we cannot access them directly when using the D3.js methods select or selectAll. But after the next rendering cycle, when Angular has created these elements, these methods can be used. If necessary, you can use a little trick. Accessing the elements with the D3.js methods must occur in a setTimeout closure.

this.rects = [...];
setTimeout(() => {
  svg
    .selectAll('rect')
    ...
},0);

Is there an alternative approach?

D3.js provides the attrTween method. With this method, we can modify individual attributes during a running animation.

Let us assume that we have already initialized our entries in the rects array, so suffice it to show the code dealing with the animation. An implementation of the desired functionality using the attrTween method might look like so:

public moveRectsTo(x: number): void {
  svg
    .selectAll('rect')
    .transition()
    .duration(1000)
    .attrTween('x', (data, idx, nodeList) => {
      const i = d3.interpolate(this.rects[idx].x, x);
      return (t) => {
        return i(t).toString();
      };
    });
};

What’s happening? In principle, it's pretty much the same as in our first animation. The x coordinate which should be reached is passed to the moveRectsTo method as a parameter. Starting the animation, the attrTween method will be fired once for each rect element. Two things will happen. At first, by calling the D3.js method interpolate, we will create our own interpolation function. This function expects the initial state as the first parameter and the target state as the second parameter. Then this function can be called by passing a number parameter between 0 and 1, and it will return the correspondent state. Secondly, we will return a so-called factory function. This function will be continuously triggered during the animation, and it calculates and returns the current x coordinate by using the current animation progress in percent (i.e., 0-1).

In our example you could easily write this interpolation function yourself. It might look like this:

const interpolate = (from: number, to: number): ((t: number) => string) => {
  return (t: number): string => {
    return ( from + ( ( to - from ) * t ) ).toString();
  };
};

The D3.js method interpolate can process hexadecimal color values or even entire objects. It is much more flexible and quite simpler to use. I would always prefer using it – why we should reinvent the wheel? But we didn’t solve the real problem with this approach. In principle, our new algorithm exactly does the same thing as the first one. It’s just more verbose and more complicated.

And what’s a possible solution?

Those familiar with JavaScript probably have an idea: „We can solve it with only a simple change. Let's store the calculated value in the rects array before returning it.“:

public moveRectsTo(x: number): void {
  svg
    .selectAll('rect')
    .transition()
    .duration(1000)
    .attrTween('x', (data, idx, nodeList) => {
      const i = d3.interpolate(this.rects[idx].x, x);
      return (t) => {
        this.rects[idx].x = i(t);
        return this.rects[idx].x.toString();
      };
    });
};

That’s right. The x coordinates can be accessed at any time during the animation via the rects array. Our data is now consistent. Changing the x coordinate in the rects array directly will now also affect the representation.

I don’t know why Angular keeps property binding in this case, even though the attrTween method will overwrite the attribute value for x. I assume if the attribute value to be assigned via D3.js is already equal to the attribute value assigned via Angular, no further assignment is executed. The Angular property binding will be retained. However, I don’t want to trust that fact…

In my mind, a clear solution consists of only one more simple change. During the animation, the representation and our data in the background will be consistent. We have to keep Angulars property binding alive. That can be done easily. The attrTween method expects the name of the HTML attribute as the first parameter – in our case „x“. When we change this attribute name to a non-existing one, for example, „transition-x“, we have solved this problem.

For a possible solution the source code might look like this:

public moveRectsTo(x: number): void {
  svg
    .selectAll('rect')
    .transition()
    .duration(1000)
    .attrTween('transition-x', (data, idx, nodeList) => {
      const i = d3.interpolate(this.rects[idx].x, x);
      return (t) => {
        this.rects[idx].x = i(t);
        return this.rects[idx].x.toString();
      };
    });
};

How can we change multiple values for one animation?

For example, if we want to change the y coordinate or the dimension and not only the x coordinate, we can do this within the existing attrTween method. Since there is further no attribute mapping by D3.js, it isn’t necessary to use multiple attrTween methods – of course, this could make sense in your context.

public moveRectsTo(x: number, y: number): void {
  svg
    .selectAll('rect')
    .transition()
    .duration(1000)
    .attrTween('transition-dummy', (data, idx, nodeList) => {
      const iX = d3.interpolate(this.rects[idx].x, x);
      const iY = d3.interpolate(this.rects[idx].y, y);
      return (t) => {
        this.rects[idx].x = iX(t);
        this.rects[idx].y = iY(t);
        return '';
      };
    });
};

Returning an empty string within the factory function is only necessary if we use the D3.js type library (@types/d3), because of HTML attributes are always of type string.

What about property changes during a running animation?

During the pie chart development, I stumbled upon this problem. Because of user actions during a running animation, the property values could be overwritten. This leads to bizarre effects in presentation and partly to inconsistent data. Again, I have tried to find a pragmatic approach. If we would hold our data twice, one array describing the current state and another array describing the end state. With this, we can solve the problem skillfully. We could take the rects array describing the end state und define an additional array rectsState, which we initialize with data from our rects array. Every time we change the rects array, we will trigger our animation. Our animation method might look now as following:

public animateRects(): void {
  svg
    .selectAll('rect')
    .transition()
    .duration(1000)
    .attrTween('transition-dummy', (data, idx, nodeList) => {
      const iX = d3.interpolate(this.rectsState[idx].x, this.rects[idx].x);
      const iY = d3.interpolate(this.rectsState[idx].y, this.rects[idx].y);
      return (t) => {
        this.rectsState[idx].x = iX(t);
        this.rectsState[idx].y = iY(t);
        return '';
      };
    });
};

When we trigger a new animation via the transition method, possible active animations will be stopped automatically by the D3.js routines. Unfortunately, animations are executed asynchrony. That means the last cycle will be still executed for every element independently of a new animation. This can lead to a jumping animation when inserting new entries or deleting entries. We can work around this behavior by calling the D3.js method interrupt at the affected elements before starting a new animation. To stop a running animation safely, let us insert a few more lines of source code at the beginning of our animateRects method:

public animateRects(): void {
  svg
    .selectAll('rect')
    .interrupt();
  ...
};

How do I respond to property changes automatically?

During pie chart development I also came across this problem. How can I avoid the animation method must manually be triggered each time data changes? For an Angular component, you can respond to property changes using the ngOnChanges method. The problem using this method is that ngOnChanges only fires when properties are changed from the outside. Plus, the corresponding properties have to be marked with the @Input decorator. Furthermore, changes within arrays or objects are not detected automatically.

When Angular itself checks if property values have changed, in other words on each test cycle, the Angular component method ngDoCheck is triggered. At this point, we can implement a check and can analyze whether values relevant to the animation have changed.

Because of constantly changing values in our rects array during running animations, we cannot use this. We will need another array which contains the last detected final state to be reached. In our example I defined an array rectsLast and initialized it with data from our rects array. The ngDoCheck method might look like this:

ngDoCheck(){
  let changed = (this.rects.length !== this.rectsLast.length);
  if(!changed){
    for(let i=0; i < this.rects.length; ++i){
      const a = this.rects[i];
      const b = this.rectsLast[i];
      changed = changed || (a.x !== b.x) || (a.y !== b.y) 
                || (a.width !== b.width) || (a.height !== b.height);
      if(changed) break;
    }
  }
  if(changed){
    this.animateRects();
  }
};

Within our animateRects method we always first duplicate the final state from the rects array into the array rectsLast. We have to create a true copy, not just a new array of existing object references. In the example, I used the JSON methods stringify and parse to achieve this.

public animateRects(): void {
    this.rectsLast = JSON.parse(JSON.stringify(this.rects));
    ...
};

Et voilá: Each time the coordinates in our array rects change, in our representation these changes are going to be displayed with smooth animations. We don’t have to worry about whether an animation is running, nor do we need any additional method calls to detect data changes.