Let's start with a simple HTML template.
We're going to load a basic GeoJSON world map.
<html>
<head>
<style></style>
</head>
<body>
<svg></svg>
<script src="https://d3js.org/d3.v7.min.js"></script>
</body>
</html>
More reading: More than you ever wanted to know about GeoJSON
Fun point: Winding and the right-hand rule.
Add in this JS code.
let width = 1000, height = 600;
let svg = d3.select("svg")
.attr("width", width)
.attr("height", height);
// Map and projection
let projection = d3.geoEquirectangular();
let geopath = d3.geoPath().projection(projection);
// Load GeoJSON data
d3.json("https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson").then(data => {
// Draw the map
svg.append("g")
.attr("id", "countries")
.selectAll("path")
.data(data.features)
.enter()
.append("path")
.attr("d", d => geopath(d))
.attr("fill", "#777")
.attr("stroke", "#fff")
.attr("stroke-width", 0.5);
})
Add in a simple HTML tooltip.
<div class="tooltip"></div>
.on("mouseover", (event, d) => {
d3.select(".tooltip")
.text(d.properties.name)
.style("position", "absolute")
.style("background", "#fff")
.style("left", (event.pageX) + "px")
.style("top", (event.pageY) + "px");
})
.on("mouseout", (event, d) => {
d3.select(".tooltip")
.text("");
})
The current projection that we're using is equirectangular.
There are many, many projections, and you can find a list in the D3 projections module.
Try something like switching it to orthographic (i.e. globe) or some other projection.
svg.append("path")
.datum({type: "Sphere"})
.attr("id", "ocean")
.attr("d", geopath)
.attr("fill", "lightBlue");
You can define gradients in SVGs. Typically they are under the defs tag. You can of course do this programatically via JS, but you can add this in the SVG tag.
<defs>
<linearGradient id="oceanGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:rgb(0,0,27);stop-opacity:1" />
<stop offset="100%" style="stop-color:rgb(51,122,183);stop-opacity:1" />
</linearGradient>
</defs>
.attr("fill", "url(#oceanGradient)");
d3.geoEquirectangular()
.center([-0.118092, 51.509865]) // London's longitude / latitude;
.scale(500)
Can you center the map to around Singapore and zoom in?
A graticule is a network of lines representing meridians and parallels, on which a map or plan can be represented.
let graticule = d3.geoGraticule()
.step([10, 10]);
svg.append("g")
.attr("id", "graticules")
.selectAll("path")
.data([graticule()])
.enter()
.append("path")
.attr("d", d => geopath(d))
.attr("fill", "none")
.attr("stroke", "#aaa")
.attr("stroke-width", 0.2);
Recap: D3's projection function takes a longitude, latitude pair and translates it onto your drawing canvas.
let singapore = [103.851959, 1.290270] // longitude = x, latitude = y
svg.append("circle")
.attr("cx", projection(singapore)[0])
.attr("cy", projection(singapore)[1])
.attr("r", 5)
.attr("fill", "yellow");
// List of cities
let cities = [
{name: "Singapore", longitude: 103.851959, latitude: 1.290270},
{name: "London", longitude: -0.118092, latitude: 51.509865},
{name: "Tokyo", longitude: 139.839478, latitude: 35.652832}
]
Given this list of cities, can you add them to the map as small coloured circles?
Create a group called cities, and put them there.
svg.append("g")
.attr("id", "cities")
.selectAll("circle")
.data(cities)
.enter()
.append("circle")
.attr("cx", d => projection([d.longitude, d.latitude])[0])
.attr("cy", d => projection([d.longitude, d.latitude])[1])
.attr("r", 5)
.attr("fill", "yellow");
let time = Date.now();
d3.timer(function() {
let angle = (Date.now() - time) * 0.02;
projection.rotate([angle, 0, 0]);
svg.select("g#countries").selectAll("path")
.attr("d", geopath.projection(projection));
svg.select("g#graticules").selectAll("path")
.attr("d", geopath.projection(projection));
svg.select("g#cities").selectAll("circle")
.attr("cx", d => projection([d.longitude, d.latitude])[0])
.attr("cy", d => projection([d.longitude, d.latitude])[1]);
});
In orthographic projection (globe) you can see the cities even when it goes behind the globe. How do we solve this?
Add a test to see whether the point is visible or not:
svg.select("g#cities").selectAll("circle")
.attr("cx", d => projection([d.longitude, d.latitude])[0])
.attr("cy", d => projection([d.longitude, d.latitude])[1])
.attr("visibility", d => {
var point = {type: 'Point', coordinates: [d.longitude, d.latitude]};
if (geopath(point) == null) {
return "hidden";
} else {
return "visible";
}
});
Chi-Loong | V/R