Skip to content

Instantly share code, notes, and snippets.

@mbostock
Last active December 10, 2019 17:13
Show Gist options
  • Save mbostock/2b534b091d80a8de39219dd076b316cd to your computer and use it in GitHub Desktop.
Save mbostock/2b534b091d80a8de39219dd076b316cd to your computer and use it in GitHub Desktop.
Drag & Zoom
license: gpl-3.0

This example shows how to combine d3-drag and d3-zoom to allow dragging of individual circles within a zoomable canvas. If you click and drag on the background, the view pans; if you click and drag on a circle, it moves.

The tricky part of this example is the need to distinguish between two coordinate spaces: the world coordinates used to position the circles, and the pointer coordinates representing the mouse or touches. The drag behavior doesn’t know the view is being transformed by the zoom behavior, so you must convert between the two coordinate spaces.

Compare this to the simpler SVG implementation.

<!DOCTYPE html>
<meta charset="utf-8">
<canvas width="960" height="500"></canvas>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var canvas = d3.select("canvas"),
context = canvas.node().getContext("2d"),
width = canvas.property("width"),
height = canvas.property("height"),
radius = 2.5,
transform = d3.zoomIdentity;
var points = d3.range(2000).map(phyllotaxis(10));
canvas
.call(d3.drag().subject(dragsubject).on("drag", dragged))
.call(d3.zoom().scaleExtent([1 / 2, 8]).on("zoom", zoomed))
.call(render);
function zoomed() {
transform = d3.event.transform;
render();
}
function dragsubject() {
var i,
x = transform.invertX(d3.event.x),
y = transform.invertY(d3.event.y),
dx,
dy;
for (i = points.length - 1; i >= 0; --i) {
point = points[i];
dx = x - point[0];
dy = y - point[1];
if (dx * dx + dy * dy < radius * radius) {
point.x = transform.applyX(point[0]);
point.y = transform.applyY(point[1]);
return point;
}
}
}
function dragged() {
d3.event.subject[0] = transform.invertX(d3.event.x);
d3.event.subject[1] = transform.invertY(d3.event.y);
render();
}
function render() {
context.save();
context.clearRect(0, 0, width, height);
context.beginPath();
context.translate(transform.x, transform.y);
context.scale(transform.k, transform.k);
points.forEach(drawPoint);
context.fill();
context.restore();
}
function drawPoint(point) {
context.moveTo(point[0] + radius, point[1]);
context.arc(point[0], point[1], radius, 0, 2 * Math.PI);
}
function phyllotaxis(radius) {
var theta = Math.PI * (3 - Math.sqrt(5));
return function(i) {
var r = radius * Math.sqrt(i), a = theta * i;
return [
width / 2 + r * Math.cos(a),
height / 2 + r * Math.sin(a)
];
};
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment