Animation and interaction - D3 on AngularJS: Create Dynamic Visualizations with AngularJS (2014)

D3 on AngularJS: Create Dynamic Visualizations with AngularJS (2014)

Animation and interaction

We’ll cover the basics of animation and other interactive concepts in this chapter. These techniques lay the foundation for things like live updating, real-time data visualizations.

Transitions

At the heart of almost all animated data visualizations built with D3 our transitions. Transitions make it effortless for us to apply a smooth, animated, transition over a collection of selected elements. It’s easier to show how this works so lets dive right in with a simple example. Reload the page if the animation wasn’t noticed the first time.

1 <!DOCTYPE html>

2 <html>

3 <body>

4 <script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>

5 <script>

6 d3.select('body').append('svg')

7 .append('circle').attr({r: 10, cx: 100, cy: 100})

8 .transition().attr('cx', 300);

9 </script>

10 </body>

11 </html>

Wow, how cool is that? We took a circle and animated with the addition of only a single line of code! Let’s break down what’s happening here. Selectors have an additional method called transition() which will return another selection, specifically, a special “transition selection”, that will gradually update all the DOM element attributes and styles in the first selection with their new values, in the second selection. This all occurs over a duration of 250 milliseconds (1/4 of a second), the default duration for a transition.

hello transition

hello transition

As a reminder, the object returned from calling transition() on a selector, it itself a selector, so we can easily use this same technique for selectors with multiple elements. If a we don’t change a particular property or attribute on both selections, (the selector before the transition and the one after), that property will not be transitioned.

We can adjust how long a transition takes by using the .duration() method that exists only on transition selectors. Working from the above example, we can adjust the animation to take 4 seconds (4000ms), instead of 250ms, by using transition().duration(4000).

1 d3.select('body').append('svg')

2 .append('circle').attr({r: 10, cx: 100, cy: 100})

3 .transition().duration(4000).attr('cx', 300);

Another useful feature of transitions is the ability to specify an easing type. Easing lets us fiddle with how the transition should accelerate. For example, if we drop a feather 5 feet, it will fall at about a constant rate (unless we’re on the moon were there’s no atmosphere to slow down the feather.) This would be a "linear" easing type. If, instead, we drop a hammer, it will accelerate down, going faster and faster. This would be an "exp" easing type. By default, D3 uses "cubic-in-out" which is how most physical objects move in the real world. Most objects don’t instantaneously start moving at a constant speed. They first speed up, then, when they approach their target, they slow down, like a car speeding up after a stop sign and then slowing down as it approaches a red light at an intersection. This is the same transition illustrated in the above example. Here’s an example of how we can specify a different easing type with a duration of 4 seconds.

1 d3.select('body').append('svg')

2 .append('circle').attr({r: 10, cx: 100, cy: 100})

3 .transition().duration(4000).ease('bounce').attr('cx', 300);

Here’s a visualization of other easing types.

Easing types

Easing types

Notice how all the transitions above gain speed accept for the last, "cubic-in-out". "cubic-in-out" is actually the same as "cubic" except it has its easing applied equally to both ends (the in and out portions.) By default, all easing types are in so just cubic is the same as cubic-in and we can fiddle with the easing type by appending either -out or -in-out. Here’s an example of the 3 different poly easing variations.

Events

Events allow our visualizations to be interactive. Using them, we specify what should happen when a user performs an action, like clicking, or moving the mouse, or pressing the keyboard. HTML on its own has a variety of different interactive events we can opt to be notified of using callbacks. D3 simply makes it easier to set them up using selections instead of having to deal with individual DOM elements directly. We can do all this just using the on() selection method, passing it the name of the event we want to listen for and a callback function which should be called when that event occurs. Lets have a look at a simple ‘hello world’ style event example that compares the native HTML way of setting up an event listener and the D3 way.

1 <!DOCTYPE html>

2 <html>

3 <body>

4 <script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>

5 <select>

6 <option value="hello">hello</option>

7 <option value="world">world</option>

8 </select>

9 <script>

10 // // the non-D3 way

11 document.getElementsByTagName('select')[0]

12 .addEventListener('change', function(){ alert(this.value); });

13 // the D3 way

14 d3.select('select').on('change', function(){ alert(this.value); });

15 </script>

16 </body>

17 </html>

Notice how the callback gets called with the select element as the this variable. Using on instead of addEventListener might sound like a trivial change but there’s more going on here than just that. Different event listeners are being applied to all the elements in the selection. Just like most other selection methods, our callback will get called with the first two arguments set to the clicked elements datum and index in the selection, respectively. Event listener will only be setup once per element, event if we re-setup the listener. If we copied and pasted the non-D3 version, we’d get two alerts.

1 document.getElementsByTagName('select')[0]

2 .addEventListener('change', function(){ alert(this.value); });

3 document.getElementsByTagName('select')[0]

4 .addEventListener('change', function(){ alert(this.value); });

5 // the second event listener is added after the first

If we did the same with the D3 version, we’d get only one.

1 d3.select('select').on('change', function(){ alert(this.value); });

2 d3.select('select').on('change', function(){ alert(this.value); });

3 // the second event listener overrides the first

This is convenient since most of the time, we’ll want to simply re-apply our event listeners for all selected elements instead of having to explicitly remove our old listeners. If, for some reason, we want to remove all the event listeners for a selection, we can call on() with null as the second argument.

1 d3.select('select').on('change', function(){ alert(this.value); });

2 d3.select('select').on('change', null);

3 // nothing is alerted!

Some other common events we’ll be interested in are the mouse events click, mousedown, mousemove, and mouseup. A click events are fired when a user presses and then releases the mouse button. It’s a combination of mousedown and mouseup. mouse move is fired everytime the mouse moves.

Putting together all our knowledge of transitions and events, let’s build a simple video game. The objective is to avoid moving asteroids (circles) from colliding with our spaceship (mouse.) We’ll start off by defining our current level, and the width and height of our game, which will take up the entire window.

1 var level = 0, width = window.innerWidth, height = window.innerHeight;

Now lets create the svg that will hold our game objects.

1 var svg = d3.select('body').append('svg');

Lets add some asteroids. 20 sounds like a good number. Lets make the pretty big, with a radius of 20

1 var circles = svg.selectAll('circle').data(d3.range(20)).enter().append('circle')

2 .attr('r', 50)

We’ll randomly position all the asteroids across the screen at every level and transition all the asteroids as go between levels so lets create an update method we can call periodically using setInterval.

1 function update(){

2 circles.transition().duration(2000).attr({

3 cx: function(){ return Math.random() * width },

4 cy: function(){ return Math.random() * height } });

5 level++;

6 }

So there’s something on the screen when the user first visits our game, lets call update() right off the bat. Then we’ll setup our interval to call update() every 2 seconds.

1 update();

2 setInterval(update, 2000);

When an asteroid (circle) collides with our spaceship (mouse), game over! Alert the user that they lost and reset the level counter.

1 circles.on('mouseover', function(){

2 alert('Nice! You made it to level ' + level);

3 level = 0;

4 });

And we’re done! We just made a video game in 15 lines of JavaScript. Here’s what the final version looks like.

Asteroids, D3 style

Asteroids, D3 style

1 <!DOCTYPE html>

2 <html>

3 <body>

4 <script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>

5 <script>

6 var level = 0, width = window.innerWidth, height = window.innerHeight;

7 var svg = d3.select('body').append('svg');

8 var circles = svg.selectAll('circle').data(d3.range(20)).enter()

9 .append('circle').attr('r', 50)

10 .on('mouseover', function(){

11 alert('Nice! You made it to level ' + level);

12 level = 0;

13 });

14 function update(){

15 circles.transition().duration(2000).attr({

16 cx: function(){ return Math.random() * width },

17 cy: function(){ return Math.random() * height } });

18 level++;

19 }

20 update();

21 setInterval(update, 2000);

22 </script>

23 </body>

24 </html>