Skip to content

Instantly share code, notes, and snippets.

@mgold
Last active August 29, 2015 14:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mgold/f61420a6f02adb618a70 to your computer and use it in GitHub Desktop.
Save mgold/f61420a6f02adb618a70 to your computer and use it in GitHub Desktop.
Zoom Buttons III

This example shows how to implement zoom in and zoom out buttons on a map using D3 and SVG transforms. Once the target scale and translation are computed, the map transitions over 100ms. Then it checks to see if the button is still held down, and if so continues zooming. Thus, zooming may continue for almost 100ms after the button is released; consider this "scroll with inertia". (It's not a bug, it's a feature!) Control logic is in place to make the buttons feel natural and avoid snapping the map to a new translation instantly.

This is a fork Mike Bostock's Map Pan & Zoom I; you can diff the two to see my changes. The scaling math (which is quite tricky!) is derived from Wil Linssen.

If zooming by a hard-coded factor would push the scale beyond the scale extent, it is clipped. Importantly, this clipping also affects the translation. If you scale by 1.047 but translate as if you had scaled by 1.2, bad things happen.

See also

<!DOCTYPE html>
<meta charset="utf-8">
<style>
svg {
background: #eee;
}
.sphere {
fill: #fff;
}
.land {
fill: #000;
}
.boundary {
fill: none;
stroke: #fff;
stroke-linejoin: round;
stroke-linecap: round;
vector-effect: non-scaling-stroke;
}
.overlay {
fill: none;
pointer-events: all;
}
</style>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<body>
<script>
var width = 960,
height = 960,
center = [width / 2, height / 2];
var projection = d3.geo.mercator()
.translate([width / 2, height / 2])
.scale((width - 1) / 2 / Math.PI);
var zoom = d3.behavior.zoom()
.scaleExtent([1, 8])
.on("zoom", zoomed);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g");
var g = svg.append("g");
svg.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height);
svg
.call(zoom)
.call(zoom.event);
d3.json("/mbostock/raw/4090846/world-50m.json", function(error, world) {
g.append("path")
.datum({type: "Sphere"})
.attr("class", "sphere")
.attr("d", path);
g.append("path")
.datum(topojson.merge(world, world.objects.countries.geometries))
.attr("class", "land")
.attr("d", path);
g.append("path")
.datum(topojson.mesh(world, world.objects.countries, function(a, b) { return a !== b; }))
.attr("class", "boundary")
.attr("d", path);
});
function zoomed() {
g.attr("transform", "translate(" + zoom.translate() + ")scale(" + zoom.scale() + ")");
}
d3.select(self.frameElement).style("height", height + "px");
// Simplest possible buttons
svg.selectAll(".button")
.data(['zoom_in', 'zoom_out'])
.enter()
.append("rect")
.attr("x", function(d,i){return 10 + 50*i})
.attr({y: 10, width: 40, height: 20, class: "button"})
.attr("id", function(d){return d})
.style("fill", function(d,i){ return i ? "red" : "green"})
// Control logic to zoom when buttons are pressed, keep zooming while they are
// pressed, stop zooming when released or moved off of, not snap-pan when
// moving off buttons, and restore pan on mouseup.
var pressed = false;
d3.selectAll('.button').on('mousedown', function(){
pressed = true;
disableZoom();
zoomButton(this.id === 'zoom_in')
}).on('mouseup', function(){
pressed = false;
}).on('mouseout', function(){
pressed = false;
})
svg.on("mouseup", function(){svg.call(zoom)});
function disableZoom(){
svg.on("mousedown.zoom", null)
.on("touchstart.zoom", null)
.on("touchmove.zoom", null)
.on("touchend.zoom", null);
}
function zoomButton(zoom_in){
var scale = zoom.scale(),
extent = zoom.scaleExtent(),
translate = zoom.translate(),
x = translate[0], y = translate[1],
factor = zoom_in ? 1.3 : 1/1.3,
target_scale = scale * factor;
// If we're already at an extent, done
if (target_scale === extent[0] || target_scale === extent[1]) { return false; }
// If the factor is too much, scale it down to reach the extent exactly
var clamped_target_scale = Math.max(extent[0], Math.min(extent[1], target_scale));
if (clamped_target_scale != target_scale){
target_scale = clamped_target_scale;
factor = target_scale / scale;
}
// Center each vector, stretch, then put back
x = (x - center[0]) * factor + center[0];
y = (y - center[1]) * factor + center[1];
// Transition to the new view over 100ms
d3.transition().duration(100).tween("zoom", function () {
var interpolate_scale = d3.interpolate(scale, target_scale),
interpolate_trans = d3.interpolate(translate, [x,y]);
return function (t) {
zoom.scale(interpolate_scale(t))
.translate(interpolate_trans(t));
zoomed();
};
}).each("end", function(){
if (pressed) zoomButton(zoom_in);
});
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment