16 August 2023

Choropleth Maps with React & D3

  1. Zoomable with MouseOver effects
  2. A Tooltip!

I will be building upon this example which I found too bare bones. It was also in a script tag for you to glue into a HTML document, who even does that.

Here is how I did it with React and it is zoomable and toggles opacity upon a mouse event. A cheeky bonus; a tooltip.

Zoomable with MouseOver effects

import * as React from 'react';
import * as d3 from 'd3';

interface Props { }

const ChoroplethMap: React.FC<Props> = props => {

  const svgRef = React.useRef(null);
  const [width, height] = [1600, 1000];
  const countries = ['canada', 'russia', 'usa']
  const availableCountry = (d: any): boolean => {
    return countries.indexOf(d?.properties?.name?.toLowerCase()) > -1;
  }

  const countryColor = (d: any): string => {
    return availableCountry(d) ? "#B5D3E7" : "grey";
  }

  React.useEffect(() => {
    const svg = d3.select(svgRef.current)
      .attr("viewBox", `0 0 ${width} ${height}`)

    const everything = svg.selectAll('*');
    everything.remove();


    const projection = d3.geoNaturalEarth1()
      .scale(320)
      .translate([width / 2, height / 2])

    d3.json("https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson").then((data: any) => {

      svg.append("g")
        .selectAll("path")
        .data(data.features)
        .join("path")
        .attr("fill", (d) => countryColor(d))
        .attr("d", d3.geoPath().projection(projection))
        .style("stroke", "#fff")
        .style("opacity", .8)
        .on("mouseover", (d) => d3.select(this).attr('fill', 'black').transition().duration(200))
        .on("mouseleave", (d) => d3.select(this).attr('fill', countryColor(d)).transition().duration(200))


      const zoom = d3.zoom<SVGSVGElement, unknown>()
        .scaleExtent([1, 4])
        .translateExtent([[0, 0], [width, height]]) // Restricts panning
        .on('zoom', (event) => {
          svg.select("g").attr('transform', event.transform)
        });
      svg.call(zoom)

    })
  }, [])

const divStyle: React.CSSProperties = {
	display: 'inline-block',
	position: 'relative',
	width: '100%',
	verticalAlign: 'top',
	overflow: 'hidden'
};


  return (
    <div id="world-map" className="world-map" style={divStyle}>
      <svg
        id="world-map-svg"
        ref={svgRef}
        xmlns="http://www.w3.org/2000/svg"
      />
    </div>
  );
};

export default ChoroplethMap;

You can largely ignore my stylings, EXCEPT FOR RELATIVE POSITIONING if you want tooltips. The most important bits would...

  • GeoJSON dataset that has the coordinates of the vertices of the country polygon
  • The projection function that converts these coordinates to cartesian coordinates, though you can experiment with other wild projections.
  • Appending the group elements in the svg element and creating path elements in one of the groups.
  • Calling your zoom function that restricts panning to be within the viewbox and transforms your coordinates in a sensible manner. You would want your map to zoom in on where your cursor is. You can just copy the zoom function I have written.

Experiment with the code, figure out what each attribute does. Your HTML and CSS knowledge do not carry over that much with SVGs unfortunately. You CANNOT render a text element within an svg element, do not try to embed a text box within each path element like I did.

A Tooltip!

If you want a tooltip, call this function on the mouseover event. Also add that div element as a sibling to your svg element. Text elements will not render as you expect if embedded within an SVG element.

const mapMouseOver = (event: MouseEvent, d: GeoJSON.Feature) => {
  if (availableCountry(availableCountries, d)) {
    d3.select("#projectmap-tooltip")
      .style("opacity", 1)
      .style('left', event.offsetX + 10 + 'px')
      .style('top', event.offsetY - 10 + 'px')
      .text(renderTooltipText(mapData, d))
      // Write your own rendertooltipText function, d.properties.name will get you the country name.
  }
}

// Append this div as a sibling to your svg-element, note that world-map is a div itself that contains the svg.
d3.select("#world-map")
  .append("div")
  .attr("id", "projectmap-tooltip")
  .style("position", "absolute")
  .style("opacity", 0)

I have used opacity in this case but you can also use the visibility attribute, toggle it between hidden and visible. Same effect as opacity 0 - 1 really.

Tags: D3 Typescript React