D3 Map Visualization Example
A choropleth is a bar chart wearing country shapes. Here's a live D3 world map with geoPath, Equal Earth, and a hover tooltip.
A D3 map visualization is really just a chart where the “bars” happen to be country shapes. You still have values. You still have a color scale. The one extra piece is a projection that translates longitude and latitude into x and y coordinates that fit on screen.
The live map below loads world-atlas
country outlines, projects them with geoEqualEarth (which
keeps area proportions honest), and colors a dummy index. The API is
exactly the same one you'd use for a GeoJSON export from QGIS or any
other mapping tool. For more on D3 generally, see the D3.js page or the
graph examples.
What you need
- A FeatureCollection. GeoJSON where each feature is a place — a country, a state, a postal code area. That's the geometry you're drawing.
- A projection.
geoMercatoris what most people recognize from Google Maps.geoEqualEarthis a better default for a choropleth because area stays proportional — Africa doesn't shrink and Greenland doesn't balloon. - geoPath. You pass your projection in and it handles writing the SVG
dattribute for every feature. You don't calculate those paths manually. - A value per feature. Population, rate, score, whatever you're encoding. Join this to the features before you draw.
I don't start from shapefiles in the browser. Convert them offline to GeoJSON or TopoJSON first, then ship the output. Shapefiles in the browser are slow and painful.
A D3 map visualization example
Hover a country for the name and a dummy index. Brighter gold is higher. The outlines are Natural Earth 110m. The numbers are fake so you can see the join, the projection, and the scale without a CSV in the way.
Preview
Equal Earth projection · world-atlas countries-110m · hover for the value
The code
fitExtent is the line I forget, then I stare at a tiny smudge
in the corner. It scales the projection so the whole FeatureCollection
fills the SVG. Drop Antarctica or the fit will leave a huge empty south.
Code
const world = await d3.json(
"https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json"
);
const countries = topojson.feature(world, world.objects.countries);
countries.features = countries.features.filter(
(d) => d.properties.name !== "Antarctica"
);
const scores = { "United States of America": 96, China: 88, India: 84 };
countries.features.forEach((d) => {
d.properties.value = scores[d.properties.name] ?? 40;
});
const width = 800;
const height = 440;
const svg = d3.select("#chart").append("svg").attr("viewBox", `0 0 ${width} ${height}`);
const projection = d3.geoEqualEarth()
.fitExtent([[18, 10], [width - 18, height - 52]], countries);
const path = d3.geoPath(projection);
const color = d3.scaleSequential(d3.interpolateYlOrBr).domain([0, 100]);
svg.append("path").datum({ type: "Sphere" }).attr("d", path).attr("fill", "#0e141c");
svg.append("path")
.datum(d3.geoGraticule10())
.attr("d", path)
.attr("fill", "none")
.attr("stroke", "rgba(255,255,255,0.08)");
svg.selectAll("path.country")
.data(countries.features)
.join("path")
.attr("class", "country")
.attr("d", path)
.attr("fill", (d) => color(d.properties.value))
.attr("stroke", "#0a0c10");
Join your own numbers
The preview colors a lookup by properties.name. In a real
chart you load a CSV, Map it on the country name or ISO code,
then color(valueByName.get(d.properties.name)). Names in
Natural Earth are picky: it's
United States of America, not United States.
Need a closer zoom? Use countries-50m.json or a GeoJSON export. If the “map” is really a flow between countries, a D3 sankey chart is usually clearer than drawing lines across the ocean. And if you meant a node-link diagram, that's a force layout, not a choropleth.
Questions I get
How do I make a D3 map visualization?
Start with GeoJSON (or TopoJSON that you convert with topojson.feature()). Pick a projection — geoMercator if you want something familiar, geoEqualEarth if you want area to stay honest. Pass the projection to geoPath, which turns each geographic feature into an SVG path. Then join your data to the features and run the values through a color scale. That's the whole pipeline.
Do I need TopoJSON for a D3 map?
No, GeoJSON is fine. The reason to use TopoJSON is file size — it's significantly smaller because it eliminates redundant shared borders. Libraries like world-atlas and us-atlas ship TopoJSON files; you convert them with topojson.feature() before passing them to D3.
Why does my D3 map look empty?
Most of the time it's one of two things. Either the projection isn't fitted to your data, so the features are rendering somewhere outside the viewport — fix this with fitExtent() or fitSize() on your FeatureCollection. Or the coordinates are lon/lat swapped. GeoJSON specifies [longitude, latitude], not [lat, lon], and that's easy to get backwards.