A globe with pan & zoom showing circles for each city of over 100,000 inhabitants. Data from Geonames, preprocessed. The same data is used in World City Explorer.
Uses world-110m
and world-50m
geographic shapes from TOPOJSON World Atlas.
Inspired by
forked from curran's block: Orthographic Zoom III
forked from curran's block: Cities on the Globe
forked from Sazerland's block: Cities on the Globe
xxxxxxxxxx
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<script src="https://unpkg.com/d3-tip@0.7.1/index.js"></script>
<style>
.d3-tip {
font-family: sans-serif;
font-size: 1.5em;
line-height: 1;
padding: 7px;
background: black;
color: lightgray;
border-radius: 20px;
}
circle:hover {
stroke: white;
stroke-width: 0.5px;
fill-opacity: 1;
}
</style>
</head>
<body>
<svg width="960" height="500"></svg>
<script>
const svg = d3.select('svg').style('background-color', '#222');
const path = svg.append('path').attr('stroke', 'gray');
const citiesG = svg.append('g');
const projection = d3.geoOrthographic();
const initialScale = projection.scale();
const geoPath = d3.geoPath().projection(projection);
let moving = false;
const rValue = d => d.population;
const rScale = d3.scaleSqrt().range([0, 20]);
var commaFormat = d3.format(',');
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(d => `${d.name}: ${commaFormat(d.population)}`);
svg.call(tip);
d3.queue()
.defer(d3.json, 'https://unpkg.com/world-atlas@1/world/110m.json')
.defer(d3.json, 'https://unpkg.com/world-atlas@1/world/50m.json')
.defer(d3.csv, 'geonames_cities100000.csv')
.await((error, world110m, world50m, cities) => {
const countries110m = topojson
.feature(world110m, world110m.objects.countries);
const countries50m = topojson
.feature(world50m, world50m.objects.countries);
cities.forEach(d => {
d.latitude = +d.latitude;
d.longitude = +d.longitude;
d.population = +d.population;
});
rScale.domain([0, d3.max(cities, rValue)]);
cities.forEach(d => {
d.radius = rScale(rValue(d));
});
const render = () => {
// Render low resolution boundaries when moving,
// render high resolution boundaries when stopped.
path.attr('d', geoPath(moving ? countries110m : countries50m));
const point = {
type: 'Point',
coordinates: [0, 0]
};
cities.forEach(d => {
point.coordinates[0] = d.longitude;
point.coordinates[1] = d.latitude;
d.projected = geoPath(point) ? projection(point.coordinates) : null;
});
const k = Math.sqrt(projection.scale() / 200);
const circles = citiesG.selectAll('circle')
.data(cities.filter(d => d.projected));
circles.enter().append('circle')
.merge(circles)
.attr('cx', d => d.projected[0])
.attr('cy', d => d.projected[1])
.attr('fill', 'steelblue')
.attr('fill-opacity', 0.4)
.attr('r', d => d.radius * k)
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
circles.exit().remove();
};
render();
let rotate0, coords0;
const coords = () => projection.rotate(rotate0)
.invert([d3.event.x, d3.event.y]);
svg
.call(d3.drag()
.on('start', () => {
rotate0 = projection.rotate();
coords0 = coords();
moving = true;
})
.on('drag', () => {
const coords1 = coords();
projection.rotate([
rotate0[0] + coords1[0] - coords0[0],
rotate0[1] + coords1[1] - coords0[1],
])
render();
})
.on('end', () => {
moving = false;
render();
})
// Goal: let zoom handle pinch gestures (not working correctly).
.filter(() => !(d3.event.touches && d3.event.touches.length === 2))
)
.call(d3.zoom()
.on('zoom', () => {
projection.scale(initialScale * d3.event.transform.k);
render();
})
.on('start', () => {
moving = true;
})
.on('end', () => {
moving = false;
render();
})
)
});
</script>
</body>
https://d3js.org/d3.v4.min.js
https://d3js.org/topojson.v1.min.js
https://unpkg.com/d3-tip@0.7.1/index.js