Bill Mill web site logo

Making a US Map for the web with D3

I make maps with d3 sporadically, and I forget how to start pretty much every time, so with this post I'm going to make a simple reminder of how to get started.

Create your HTML

Create an index.html file like the one below in your favorite editor that imports d3 and topojson, and has a div to contain your map.

(For simplicity's sake, I'm just going to be loading d3 and topojson from a cdn. I may write a bit in the future about how to use npm and esbuild to compile it into a single file)

<html>
  <head>
    <script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
    <script src="https://cdn.jsdelivr.net/npm/topojson@3"></script>
    <script src="index.js"></script>
  </head>
  <body>
    <div id="map">
    </div>
  </body>
</html>

Choose your map

I recommend using topojson/us-atlas boundary files, which have been intelligently slimmed down so you're transferring as little data to your viewers as possible.

There are files with county data, state data, and the whole nation's data. Each file is also available unprojected or projected into Albers USA projection, which is a reasonable projection of the continental USA, with Alaska and Hawaii moved below it for good measure.

For our simple map here, let's choose the smallest Albers map that suits our needs - I'm going to pick state-albers-10m.json just because I want to show state boundaries. Right clicking on "Download" and copying the link gives us a URL of https://cdn.jsdelivr.net/npm/us-atlas@3/states-albers-10m.json, which we'll use as our data source.

Set up the map

Create a file named index.js that looks like this:

function map(mapdata) {
  const width=975,
    height=610;

  // Create an svg element to hold our map, and set it to the proper width and
  // height. The viewBox is set to a constant value becase the projection we're
  // using is designed for that viewBox size:
  // https://github.com/topojson/us-atlas#us-atlas-topojson
  const svg = d3.select("#map").append("svg")
      .attr("width", width)
      .attr("height", height)
      .attr("viewBox", [0, 0, 975, 610])
      .attr("style", "width: 100%; height: auto; height: intrinsic;");

  // Create the US boundary
  const usa = svg
    .append('g')
    .append('path')
    .datum(topojson.feature(mapdata, mapdata.objects.nation))
    .attr('d', d3.geoPath())

  // Create the state boundaries. "stroke" and "fill" set the outline and fill
  // colors, respectively.
  const state = svg
    .append('g')
    .attr('stroke', '#444')
    .attr('fill', '#eee')
    .selectAll('path')
    .data(topojson.feature(mapdata, mapdata.objects.states).features)
    .join('path')
    .attr('vector-effect', 'non-scaling-stroke')
    .attr('d', d3.geoPath());
}

window.addEventListener('DOMContentLoaded', async (event) => {
  const res = await fetch(`https://cdn.jsdelivr.net/npm/us-atlas@3/states-albers-10m.json`)
  const mapJson = await res.json()
  map(mapJson)
});

That's it!

If you followed along, you've now got something like this:

I may write future blog posts on how to do things with your map; until then I recommend going to Observable and searching for maps that do similar things to what you want to do, then trying to port it over to your map.