Hexbin map

This map shows approximately 3,000 locations of Walmart stores. The hexagon area represents the number of stores in the vicinity, while the color represents the median age of these stores. Older stores are red, and newer stores are blue.

import * as Hexbin from "npm:d3-hexbin";

// Specify the map dimensions.
const width = 960;
const height = 600;

// Specify the map projection.
const projection = d3.geoAlbersUsa()
    .scale(4 / 3 * width)
    .translate([width / 2, height / 2]);

// Create the container SVG.
const svg = d3.create("svg")
    .attr("viewBox", [0, 0, width, height])
    .attr("width", width)
    .attr("height", height)
    .attr("style", "max-width: 100%; height: auto;");

// Create the bins.
const hexbin = Hexbin.hexbin()
    .extent([[0, 0], [width, height]])
    .radius(10)
    .x((d) => d.xy[0])
    .y((d) => d.xy[1]);

const bins = hexbin(walmarts.map((d) => ({xy: projection([d.longitude, d.latitude]), date: d.date})))
   .map((d) => (d.date = new Date(d3.median(d, (d) => d.date)), d))
   .sort((a, b) => b.length - a.length)

// Create the color scale.
const color = d3.scaleSequential()
  .domain(d3.extent(bins, (d) => d.date))
  .interpolator(d3.interpolateSpectral);

// Create the radius scale.
const radius = d3.scaleSqrt()
  .domain([0, d3.max(bins, (d) => d.length)])
  .range([0, hexbin.radius() * Math.SQRT2]);

// Append the color legend.
svg.append("g")
    .attr("transform", "translate(580, 20)")
    .append(() => Plot.legend({
      color: {
        domain: color.domain(),
        interpolate: color.interpolator(),
      },
      label: "Median opening year",
      width: 260,
      ticks: d3.utcYear.every(5).range(...color.domain()),
      tickFormat: d3.utcFormat("%Y")
    }));

// Append the state mesh.
svg.append("path")
    .datum(topojson.mesh(us, us.objects.states))
    .attr("fill", "none")
    .attr("stroke", "#777")
    .attr("stroke-width", 0.5)
    .attr("stroke-linejoin", "round")
    .attr("d", d3.geoPath(projection));

// Append the hexagons.
svg.append("g")
  .selectAll("path")
  .data(bins)
  .join("path")
    .attr("transform", (d) => `translate(${d.x},${d.y})`)
    .attr("d", (d) => hexbin.hexagon(radius(d.length)))
    .attr("fill", (d) => color(d.date))
    .attr("stroke", (d) => d3.lab(color(d.date)).darker())
  .append("title")
    .text((d) => `${d.length.toLocaleString()} stores\n${d.date.getFullYear()} median opening`);

display(svg.node());
const parseDate = d3.utcParse("%m/%d/%Y");

const walmarts = FileAttachment("data/walmart.tsv")
  .tsv()
  .then((data) => data.map((d) => ({
    longitude: +d[0],
    latitude: +d[1],
    date: parseDate(d.date)
  })))
  .then(display);
const us = FileAttachment("data/us-counties-10m.json").json().then(display);
✎ Suggest changes to this page