Rotate and Translate Points with d3 in a TopoJSON Map

Transforms in a map svg can engage and delight users. With a little extra effort, you can give your customer something unexpected that they enjoy using. However, transforms can be complicated to get right. The syntax, sequence, and math can give anyone a headache when they are trying to transform a vision into code.

The goal:

See the Pen React Rotate + Translate by Jon (@Middaugh) on CodePen.

We’ll start with this pen. A TopoJSON map and d3 have been imported, and there is a single transform to translate the latitude and longitude of the state capitals to the appropriate points on the map.

We have three steps to get to our final rotate + translate for each point:

  • Start at a single point in the center
  • Rotate out from that point in an evenly spaced pattern
  • Translate to the appropriate coordinates within the map

Set the Center Point of the TopoJSON Map

First, we need to set the center from which the points will radiate out from. Initially I attempted to translate each group element (<g>) inside the data.map using:

transform={`
    translate(${mapWidth / 2}, ${mapHeight / 2})
`}

However, when I added the rotate later, I had this unexpected result:

Wrong Center

The problem is, the translate of the points is lost when rotating later. Instead, I translated the parent group element using the same transform logic I previously tried on the children.

Rotate

Next, I added the rotate. The rotate is called from useLayoutEffect so that it can be run after the translate of the parent group element. However, the rotate cannot be performed in isolation. A rotate on it’s own will simply rotate the element while keeping it at it’s current x,y coordinates. The rotate must be chained with a translate, like so:

rotate(${360 * (index / array.length)})translate(${200}, ${200})

Order also matters. D3 reads from right to left and incorporates the translate into the rotate. Instead of just rotating the element at a single point, this rotate code actually changes the x,y coordinates with a circular revolution and rotates the element. The bit of math in the rotate function is used to fan out the elements in a regularly spaced circle.

However, we don’t actually want the point rotation, just the revolution. We need one more piece in our chaining:

rotate(${360 * (index / array.length)})translate(${200}, ${200})rotate(${-360 * (index / array.length)})

The rotate at the end counteracts the rotation at the beginning. The rotate at the end of the chain actually calculates first and does not incorporate the translate into its calculations. However, all pieces of the transform execute at the same time, giving us the desired revolution motion without any point rotation.

The most interesting piece of the above code is that the two rotations need to be opposites in order to counteract. In this case, I used -360 and 360.

Translate to the Final Coordinates

Finally, I need to transform the circle of points to their ending destination. I chained another transform named translateLongLatin the useLayoutEffect:

useLayoutEffect(() => {
    d3.selectAll('#parentG > g')
    .transition().attr('transform', rotate).duration(2000)
    .transition().duration(2000).attr('transform', translateLongLat)
}, [d3]);

This transform simply takes the points and places them at the specified x,y. It does not incorporate the x,y location the points were at after the rotatetransform.

That’s all there is to it. Understanding the additive effects of transforms will greatly help in developing incredible user interfaces.

This code builds on this TopoJSON mapData files are here.

Share this post:

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.