Choropleth Maps with React & D3
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.