/ d3.js

Wrapping my head around D3 rotation transitions

Although the majority of my posts are about more technical things, tips and tricks, and things like Docker containers, I am also interested in visualization. When I was working at Deloitte, I worked together with Nadieh Bremer on a small analytics demo that allowed us to compare persons attending the Deloitte Ladies Open 2015 to any of the dutch golf pros based on hitting a golf ball 5 times. Ever since this small (but extremely fun) project I was amazed with the flexibility and the power of D3.js (especially in the hands of a pro like Nadieh, see her website for some really cool stuff!)

Recently, I had the need for creating a visualization and decided to brush up on my D3 knowledge again :) One of the main things I needed in my visualization was an animation that both translated and rotated a shape defined by some path, where the rotating happened around the center point of the shape.

Drawing the shape

For sake of simplicity, I will not use the complex shape, but just a simple triangle with the three corners (60,100), (30,180), and (90,180) to to demonstrate my journey with the rotations:

<svg height="200" width="280">
  <path id="triangle" d="M60 100 L30 180 L90 180 Z"  fill="#888"/>
</svg>

The above code will draw the following triangle:

To make the visualization a bit more clear for the next steps, I will also add some additional grid lines and a center point by changing the code into the following:

<svg height="200" width="280">
  <line x1="0" y1="0" x2="300" y2="0" stroke="black" />
  <line x1="0" y1="100" x2="300" y2="100" stroke="black" />
  <line x1="0" y1="200" x2="300" y2="200" stroke="black" />

  <line x1="0" y1="50" x2="300" y2="50" stroke="#222" stroke-dasharray="3 2"  stroke-width="1" />
  <line x1="0" y1="150" x2="300" y2="150" stroke="#222" stroke-dasharray="3 2"  stroke-width="1" />

  <g id="triangle_and_centerpoint">
    <path id="triangle" d="M60 100 L30 180 L90 180 Z"  fill="#888"/>
    <circle cx="60" cy="140" stroke="3" fill="red" />
  </g>
</svg>

which now results in the following triangle with the red dot at (140,60) being the center point:

Rotating the shape

What I now want to do is using a D3 transition to rotate the triangle -180 degrees around its center point (i.e. (140,60) ). Since I want to make use of D3, the first thing I will need to do is include the D3.js library, which is accomplished by including the following code in your HTML:

<script src="//d3js.org/d3.v4.min.js"></script>

With the following javascript code and the above SVG, we can accomplish this as follows:

var svg = d3.select("svg")
var triangle_cx = 60
var triangle_cy = 140

function animateTriangle(){
    svg.select("#triangle")
        .transition()
        .duration(2500)
        .attr('transform' , 'rotate(-180, '+triangle_cx+',' +triangle_cy +') ')
        
        .transition() //And rotate back again
        .duration(2500)
        .attr('transform' , 'rotate(0, '+triangle_cx+',' +triangle_cy +') ')
        
        .on("end", animateTriangle) ;  //at end, call it again to create infinite loop
}

//And just call the animateTriangle function now
animateTriangle()

The above javascript code, combined with the SVG element will result in the following animation

So while the end result is still correct (i.e. the triangle is rotated -180 degrees around the red center point), the transition itself is a bit of a disappointment...

D3.js interpolators

So let's see what is actually causing this. It turns out that the when you use the transition functionality and use it on the transform attribute as we are doing above, D3 will use the function d3.interpolateTransformSvg to determine the intermediate steps in the animation. Since our rotation is around a different center point than (0,0), the animation will actually consider the translation to (40,160) also in the animation for the rotate.

We can use the d3.interpolateTransformSvg function ourselves to show the way our center point is translated during the animation.

First we store the interpolate function between the start and end state in a new variable and then use this new interpolate function to calculate 25 intermediate steps (the interpolate function takes a value between 0 and 1, where 0 represents the start state and 1 represents the end state):

var interpol_transform = d3.interpolateTransformSvg( "rotate(0,60,140)", "rotate(-180,60,140)" )

for (i=0 ; i<25 ; i++){
  svg.append("circle")
      .attr("cx", 60)
      .attr("cy", 140)
      .attr("r", 4)
      .style("fill", "red")
      .attr('transform', interpol_transform(i/25))
  console.log("Transformation attribute value: " + interpol_transform(i/25))
}

Adding the above code to our original javascript, we end up with the following SVG where the path of the triangle center point is shown:

If you look in your developer console, you should see also logging statements indicating the 25 intermediate steps similar to the ones below:

Transformation attribute value: translate(0, 0) rotate(0)
Transformation attribute value: translate(4.8, 11.200000000000001) rotate(-7.2)
Transformation attribute value: translate(9.6, 22.400000000000002) rotate(-14.4)

Although there is a perfectly good explanation for it, for me, this behavior brings back some MS Excel frustrations whenever Excel (or Clippy ) tries to do something automatically where you don't want it to, e.g. automatically convert some string fields into dates because they appear like dates....

Solving the rotation issues

Fortunately, there are some simple solutions also:

  • Include the triangle within a svg g node, where you ensure the centerpoint of the triangle is at (0,0) of the group and just rotate the group
  • Define our own interpolate function based on interpolateString and using the attrTween function to tell the transition function how to calculate the in-between frames of the animation

Using custom attrTween function

To use this approach, we just have to define a interpolate function based on the interpolateString function, similar to the interpol_transform we defined earlier to show the path of the center point.

By changing the javascript code as follows:

var svg = d3.select("#svg")
var triangle_cx = 60
var triangle_cy = 140

//Define the two interpolations. Note that the interpolateString will 
//do a pairwise interpolate for the numbers that are found and the 
//rotation point is the same for both (i.e. the (60,140) )
var interpol_rotate = d3.interpolateString( "rotate(0,60,140)", "rotate(-180,60,140)" )
var interpol_rotate_back = d3.interpolateString( "rotate(-180,60,140)", "rotate(0,60,140)" )



function animateTriangle(){
    svg.select("#triangle")
        .transition()
        .duration(2500)
        .attrTween('transform' , function(d,i,a){ return interpol_rotate } )
        
        .transition() //And rotate back again
        .duration(2500)
        .attrTween('transform' ,  function(d,i,a){ return interpol_rotate_back })
        
        .on("end", animateTriangle) ;  //at end, call it again to create infinite loop
}

//And just call the animateTriangle function now
animateTriangle()

We end up with exactly the result I had in mind when I decided I needed to rotate a path around a given center point:

Using additional group nodes to translate around origin

The alternative to the above is to wrap the path you want to rotate in three separate group nodes:

  1. One group node to translate the center point of the path to the origin of this group
  2. One group that envelops group node 1 and does the actual rotation
  3. One group note that envelops group node 2 that cancels out the translation from group node 1

In the SVG code, this means we have to have the following definition:

<svg height="200" width="280">
  <line x1="0" y1="0" x2="300" y2="0" stroke="black" />
  <line x1="0" y1="100" x2="300" y2="100" stroke="black" />
  <line x1="0" y1="200" x2="300" y2="200" stroke="black" />

  <line x1="0" y1="50" x2="300" y2="50" stroke="#222" stroke-dasharray="3 2"  stroke-width="1" />
  <line x1="0" y1="150" x2="300" y2="150" stroke="#222" stroke-dasharray="3 2"  stroke-width="1" />

  <g id="g_translate_1" transform="translate(60, 140)">
    <g id="g_rotate">
      <g id="g_translate_2" transform="translate(-60,-140)">
        <path id="triangle" d="M60 100 L30 180 L90 180 Z"  fill="#888"/>
      </g>
    </g>
  </g>
  <circle cx="60" cy="140" r="3" fill="red" />
</svg>

And in the Javascript code, you just have to apply the rotation to the group node with id g_rotate by using the following code:

function animateTriangle(){
    svg.select("#g_rotate")
        .transition()
        .duration(2500)
        .attr('transform' , rotate(-180))
        
        .transition() //And rotate back again
        .duration(2500)
        .attr('transform' , rotate(-180))        
        .on("end", animateTriangle) ;  //at end, call it again to create infinite loop
}

//And just call the animateTriangle function now
animateTriangle()

And this way, you also end up with the same result:

Conclusions

At the moment not 100% sure which of these approaches is the best way. Personally, I would probably stick with the more Javascript solution (i.e. using the attrTween and define my own interpolation), but that might be influenced by the fact I am more of a programmer than a designer :)

Let me know if you have any other ideas, curious to hear other potential solutions. Also, if you good reasons why one of the above solutions is better than the other, please also share them!

Guido Diepen

Guido Diepen

Senior Data Scientist at T-Mobile NL | Fascinated with new technologies and tools | Trying to automate the boring bits of life | Love to travel

Read More