Skip to content

Instantly share code, notes, and snippets.

@mbostock
Last active October 24, 2023 08:48
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save mbostock/6123708 to your computer and use it in GitHub Desktop.
Save mbostock/6123708 to your computer and use it in GitHub Desktop.
Drag + Zoom
license: gpl-3.0
redirect: https://observablehq.com/@d3/drag-zoom

An example of how to combine d3.behavior.drag and d3.behavior.zoom, using stopPropagation to allow the drag behavior to take precedence over panning. Use the mouse to pan by clicking on the background, or drag by clicking on individual dots; you may also use the mousewheel to zoom.

Note: combining these two behaviors means that gesture interpretation is ambiguous and highly sensitive to position. A click on a circle is interpreted as dragging that circle, whereas a click one pixel away could be interpreted as panning the background. A more robust method of combining these behaviors is to employ modality. For example, if the user holds down the SPACE key, clicking and dragging is interpreted as panning, rather than dragging, regardless of the click location. This approach is commonly employed in commercial software such as Adobe Photoshop.

This example was created in response to a Stack Overflow question.

x y
100 80
80 69
130 75
90 88
110 83
140 99
60 72
40 42
120 108
70 48
50 56
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.dot circle {
fill: lightsteelblue;
stroke: steelblue;
stroke-width: 1.5px;
}
.dot circle.dragging {
fill: red;
stroke: brown;
}
.axis line {
fill: none;
stroke: #ddd;
shape-rendering: crispEdges;
vector-effect: non-scaling-stroke;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
var margin = {top: -5, right: -5, bottom: -5, left: -5},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var zoom = d3.behavior.zoom()
.scaleExtent([1, 10])
.on("zoom", zoomed);
var drag = d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.right + ")")
.call(zoom);
var rect = svg.append("rect")
.attr("width", width)
.attr("height", height)
.style("fill", "none")
.style("pointer-events", "all");
var container = svg.append("g");
container.append("g")
.attr("class", "x axis")
.selectAll("line")
.data(d3.range(0, width, 10))
.enter().append("line")
.attr("x1", function(d) { return d; })
.attr("y1", 0)
.attr("x2", function(d) { return d; })
.attr("y2", height);
container.append("g")
.attr("class", "y axis")
.selectAll("line")
.data(d3.range(0, height, 10))
.enter().append("line")
.attr("x1", 0)
.attr("y1", function(d) { return d; })
.attr("x2", width)
.attr("y2", function(d) { return d; });
d3.tsv("dots.tsv", dottype, function(error, dots) {
dot = container.append("g")
.attr("class", "dot")
.selectAll("circle")
.data(dots)
.enter().append("circle")
.attr("r", 5)
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.call(drag);
});
function dottype(d) {
d.x = +d.x;
d.y = +d.y;
return d;
}
function zoomed() {
container.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
d3.select(this).classed("dragging", true);
}
function dragged(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
}
function dragended(d) {
d3.select(this).classed("dragging", false);
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment