Transition Chaining with React and d3 in TopoJSON Map

My favorite JavaScript map library is TopoJSON with d3 because it is simple to configure and there are fantastic examples to get a project up and running quickly.  In this project, I have a TopoJSON map of the United States with latitude/longitude points for each capital city.  I have multiple transitions chained together on the latitude/longitude points. This gist is a good starting point.

There are three items in particular that we’ll discuss in depth: the jsx (html), the useLayoutEffect, and the transition chaining. This gist is the starting point.

The D3 Code For the Map:

import React, { FC, useLayoutEffect, useCallback } from 'react';
import * as d3 from 'd3';
import * as topojson from 'topojson';
import * as statesMap from '../data/states-albers-10m.json';
import * as cities from '../data/us-cities.json';

export const USMap: FC = () => {

  const projection = d3.geoAlbersUsa().scale(1300).translate([487.5, 305])
  const path = d3.geoPath();
  //@ts-ignore
  const us = statesMap.default;
  //@ts-ignore
  const data = cities.default;

  const mapWidth = 975;
  const mapHeight = 610;

  const translateDiagonal = useCallback((option1, option2, option3) => {
    return `translate(${mapWidth * (option2 / option3.length)} ${mapHeight * (option2 / option3.length)})`
  }, [mapWidth, mapHeight]);

  const translateLongLat = useCallback((option1, option2, option3) => {
    return `translate(${option3[option2].getAttribute('originalx')}, 
    ${option3[option2].getAttribute('originaly')})`
  }, []);

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

  const map = <svg viewBox={`0 0 ${mapWidth} ${mapHeight}`}>
    <path fill='#ddd' d={`${path(topojson.feature(us, us.objects.nation))}`}></path>
    <path fill='none' stroke='#fff' strokeLinejoin='round' strokeLinecap='round' d={`${path(topojson.mesh(us, us.objects.states, (a, b) => a !== b))}`}></path>
    <g id='parentG' textAnchor='middle' fontFamily='sans-serif' fontSize='10' style={{ position: 'relative' }}>
      {
        //@ts-ignore
        data.map(({ capital, lat, long }, index, array) => {
          const projected = projection([long, lat]);
          const projectedX = projected ? projected[0] : 0;
          const projectedY = projected ? projected[1] : 0;

          return (
            projected ? <g
              //@ts-ignore
              originalx={projectedX}  //Custom attribute
              originaly={projectedY}  //Custom attribute
            >
              <defs>
                <radialGradient id='grad1' >  
                  <stop offset='10%' stopColor='#209cee' stopOpacity={.1} />  //inner color
                  <stop offset='90%' stopColor='#050' stopOpacity={.8} />  //outer color
                </radialGradient>
              </defs>
              <circle r='4' fill='url(#grad1)' ></circle>  //points to the radial gradient
              <text y='-6'>{capital}</text>
            </g>
              : null
          )
        }
        )
      }
    </g>
  </svg>

  return <div style={{ width: '100%', height: '100%' }}>{map}</div>
}

JSX

Notice the custom attributes I set on the <g> elements created when looping through the capital data. I use this later to be able to transition to the correct point on the map.

The radial gradient is used to set the inner and outer colors on the points. The circle element has a url attribute set to the id of the radial gradient to point it to the correct radial gradient to use.

The rest of the JSX comes from a demo that can be found here.

useLayoutEffect

Why did I choose useLayoutEffect instead of useEffect? useLayoutEffect runs before useEffect and makes changes to DOM nodes before the browser has a chance to paint.

Transition Chaining – Multiple Synchronous Transitions on the SVG

This was the most challenging part even though the final code does not look complex.

First, I select only the <g> tags that need to be transitioned. Otherwise, <g> elements outside the scope of the map might be manipulated.

Second, I call two sequential series of transition-transform-translate. d3 syntax uses chaining of transition calls to call synchronous transitions. Be careful not to get tripped up by this syntax. Other valid syntax includes translate(...)translate(...) . However, the translate is calculated differently with this syntax.

The gist with this code can be found here. The data files with lat/long can be found here.

Maps transitions can create a great first impression on users. With just a little bit of effort, you get a lot of reward in UI presentation.

Share this post:

Leave a Comment

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