Skip to content

Instantly share code, notes, and snippets.

@bwswedberg
Last active April 18, 2020 16:05
Show Gist options
  • Save bwswedberg/da180a7f413d9fa47495482e99a56b9b to your computer and use it in GitHub Desktop.
Save bwswedberg/da180a7f413d9fa47495482e99a56b9b to your computer and use it in GitHub Desktop.
World Wrapping Map w/ Click-to-Zoom

D3 map with infinite pan while maintaining click-to-zoom for leaflet like capability. Hence, map seamlessly world wraps and behaves as you would expect. Click-to-zoom hooks into d3-zoom.

Click-to-zoom is tricky with inifinite panning because the pan offset must be accounted for--on top of the necessary map zoom level and map projection transfroms. On this map for example, if you pan past Africa several times and click China, the map will zoom and pan so that China is properly censtered. If the offsets were not properly handled, the map would auto zoom and pan over 360 degrees longitude back to the initial map AND then zoom to China.

This examples uses canvas, but can easily use svg instead.

<!DOCTYPE html>
<meta charset="utf-8">
<canvas></canvas>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://unpkg.com/topojson-client@3"></script>
<script>
var width = 960;
var height = 500;
var countries;
var innerlines;
var selected;
var yaw = d3.scaleLinear()
.domain([0, width])
.range([0, 360]);
var rootProjection = d3.geoEquirectangular();
var projection = d3.geoEquirectangular();
var path = d3.geoPath()
.projection(projection);
var zoom = d3.zoom()
.scaleExtent([1, 5])
.extent([[0, 0],[width, height]])
.translateExtent([[-Infinity, 0],[Infinity, height]])
.on('zoom', zoomed);
var canvas = d3.select('canvas')
.attr('width', width)
.attr('height', height)
.style('pointer-events', 'all')
.on('click', function() {
var pos = projection.invert(d3.mouse(this));
handleClick(pos);
})
.call(zoom);
var ctx = canvas.node().getContext('2d');
path.context(ctx);
d3.json('https://unpkg.com/world-atlas@1/world/110m.json').then(init);
function init(world) {
countries = topojson.feature(world, world.objects.countries);
innerlines = topojson.mesh(world, world.objects.countries, function(a, b) {
return a != b;
});
rootProjection.fitSize([width, height], countries);
projection.scale(rootProjection.scale())
.translate(rootProjection.translate())
.rotate(rootProjection.rotate());
draw();
}
function draw() {
ctx.clearRect(0, 0, width, height);
// countries
ctx.beginPath();
path(countries);
ctx.fillStyle = '#333';
ctx.fill();
if (selected) {
ctx.beginPath();
path(selected);
ctx.fillStyle = '#daa520';
ctx.fill();
}
ctx.beginPath();
path(innerlines);
ctx.strokeStyle = '#fff';
ctx.stroke();
}
function handleClick(pos) {
var lastSelected = selected;
selected = null;
for (var i = 0; i < countries.features.length; i++) {
if (d3.geoContains(countries.features[i], pos)) {
selected = lastSelected === countries.features[i] ? null : countries.features[i];
break;
}
}
selected ? zoomIn(selected): zoomOut();
}
function zoomIn(feature) {
var pos = projection(d3.geoCentroid(feature));
var cur = d3.zoomTransform(canvas.node());
var k = selected ? getK(feature) : zoom.scaleExtent()[0];
var x0 = -cur.invertX(pos[0]) * k + width / 2;
var y0 = -cur.invertY(pos[1]) * k + height / 2;
var t = d3.zoomIdentity.translate(x0, y0).scale(k);
canvas.transition().duration(2000).call(zoom.transform, t);
}
function zoomOut() {
canvas.transition().duration(2000).call(zoom.scaleTo, 1);
}
function zoomed() {
var scale = rootProjection.scale();
var translate = rootProjection.translate();
var t = d3.event.transform;
var tx = translate[0] - t.invertX(translate[0]);
var ty = translate[1] * t.k + t.y;
projection.scale(t.k * scale)
.rotate([yaw(tx), 0, 0])
.translate([translate[0], ty]);
draw();
}
function getK(feature) {
var k = 0.8;
var bounds = d3.geoBounds(feature);
var bbox = [rootProjection(bounds[0]), rootProjection(bounds[1])];
var w = Math.abs(bbox[1][0] - bbox[0][0]);
var h = Math.abs(bbox[1][1] - bbox[0][1]);
return d3.min([width / w * k, height / h * k, zoom.scaleExtent()[1]]);
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment