Skip to content

Instantly share code, notes, and snippets.

@tommct tommct/README.md
Last active Apr 13, 2016

Embed
What would you like to do?
D3 Bounded Zoom

This D3 example demonstrates using the zoom event and limits the bounds of the zooming to a specified domain. It is largely based on http://bl.ocks.org/jasondavies/3689931, but with bounds. Most of this bounding is done in the refresh function. You need to zoom in before you can pan or zoom out.

<!DOCTYPE html>
<meta charset="utf-8">
<title>Constrained Zoom by Rectangle</title>
<script src="http://d3js.org/d3.v2.min.js?2.10.1"></script>
<style>
body {
font-family: sans-serif;
}
.noselect {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
svg {
font: 9pt sans-serif;
shape-rendering: crispEdges;
}
rect {
fill: #ddd;
}
rect.zoom {
stroke: steelblue;
fill: #bbb;
fill-opacity: 0.5;
}
.axis path, .axis line {
fill: none;
stroke: #fff;
}
</style>
<p><label for="zoom-rect"><input type="checkbox" id="zoom-rect"> zoom by rectangle</label>
<script>
var margin = {top: 0, right: 12, bottom: 20, left: 60},
width = 960 - margin.left - margin.right,
height = 430 - margin.top - margin.bottom;
var xmin = 0,
xmax = 500,
ymin = 0,
ymax = 1000;
var x = d3.scale.linear()
.domain([xmin+.5, xmax+.5])
.range([0.5, width+.5]);
var y = d3.scale.linear()
.domain([ymin+.5, ymax+.5])
.range([height+.5, 0.5]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickSize(-height);
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5)
.tickSize(-width); // tickLine == gridline
var refresh = function() {
var reset_s = 0;
if ((x.domain()[1] - x.domain()[0]) >= (xmax - xmin)) {
zoom.x(x.domain([xmin, xmax]));
reset_s = 1;
}
if ((y.domain()[1] - y.domain()[0]) >= (ymax - ymin)) {
zoom.y(y.domain([ymin, ymax]));
reset_s += 1;
}
if (reset_s == 2) { // Both axes are full resolution. Reset.
zoom.scale(1);
zoom.translate([0,0]);
}
else {
if (x.domain()[0] < xmin) {
x.domain([xmin, x.domain()[1] - x.domain()[0] + xmin]);
}
if (x.domain()[1] > xmax) {
var xdom0 = x.domain()[0] - x.domain()[1] + xmax;
x.domain([xdom0, xmax]);
}
if (y.domain()[0] < ymin) {
y.domain([ymin, y.domain()[1] - y.domain()[0] + ymin]);
}
if (y.domain()[1] > ymax) {
var ydom0 = y.domain()[0] - y.domain()[1] + ymax;
y.domain([ydom0, ymax]);
}
}
svg.select(".x.axis").call(xAxis);
svg.select(".y.axis").call(yAxis);
}
var zoom = d3.behavior.zoom().x(x).y(y).scaleExtent([.001, Infinity]).on("zoom", refresh);
var zoomRect = false;
d3.select("#zoom-rect").on("change", function() {
zoomRect = this.checked;
});
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.top + ")")
.call(zoom)
.append("g")
.on("mousedown", function() {
if (!zoomRect) return;
var e = this,
origin = d3.mouse(e),
rect = svg.append("rect").attr("class", "zoom");
d3.select("body").classed("noselect", true);
origin[0] = Math.max(0, Math.min(width, origin[0]));
origin[1] = Math.max(0, Math.min(height, origin[1]));
d3.select(window)
.on("mousemove.zoomRect", function() {
var m = d3.mouse(e);
m[0] = Math.max(0, Math.min(width, m[0]));
m[1] = Math.max(0, Math.min(height, m[1]));
rect.attr("x", Math.min(origin[0], m[0]))
.attr("y", Math.min(origin[1], m[1]))
.attr("width", Math.abs(m[0] - origin[0]))
.attr("height", Math.abs(m[1] - origin[1]));
})
.on("mouseup.zoomRect", function() {
d3.select(window).on("mousemove.zoomRect", null).on("mouseup.zoomRect", null);
d3.select("body").classed("noselect", false);
var m = d3.mouse(e);
m[0] = Math.max(0, Math.min(width, m[0]));
m[1] = Math.max(0, Math.min(height, m[1]));
if (m[0] !== origin[0] && m[1] !== origin[1]) {
zoom.x(x.domain([origin[0], m[0]].map(x.invert).sort(function(a,b) { return a - b;})))
.y(y.domain([origin[1], m[1]].map(y.invert).sort(function(a,b) { return a - b;})));
}
rect.remove();
refresh();
}, true);
d3.event.stopPropagation();
});
svg.append("rect")
.attr("width", width)
.attr("height", height);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
</script>
@pkerpedjiev

This comment has been minimized.

Copy link

commented Apr 13, 2016

Great example, but I think it's missing something. When somebody scrolls off to one side, the domain of x and y scales are changed, but the zoom's translate value doesn't change. This means that if you drag the plot to right (i.e. pan left), then drag right (pan left) some more, when you want to pan back right you have to undo all of the left panning you previously did before the plot actually moves.

To fix this, change the translate values of the zoom right after changing the domain at the edge cases. Here's a diff:

diff --git a/index.html b/index.html
index d0926ac..e103e7b 100644
--- a/index.html
+++ b/index.html
@@ -2,6 +2,7 @@
 <meta charset="utf-8">
 <title>Constrained Zoom by Rectangle</title>
 <script src="http://d3js.org/d3.v2.min.js?2.10.1"></script>
+
 <style>

 body {
@@ -58,6 +59,9 @@ var y = d3.scale.linear()
     .domain([ymin+.5, ymax+.5])
     .range([height+.5, 0.5]);

+var xOrigScale = x.copy();
+var yOrigScale = y.copy();
+
 var xAxis = d3.svg.axis()
     .scale(x)
     .orient("bottom")
@@ -86,17 +90,27 @@ var refresh = function() {
   else {
     if (x.domain()[0] < xmin) {
       x.domain([xmin, x.domain()[1] - x.domain()[0] + xmin]);
+
+        zoom.translate([xOrigScale.range()[0] - xOrigScale(x.domain()[0]) * zoom.scale(),
+                        zoom.translate()[1]])
     }
     if (x.domain()[1] > xmax) {
       var xdom0 = x.domain()[0] - x.domain()[1] + xmax;
       x.domain([xdom0, xmax]);
+
+      zoom.translate([xOrigScale.range()[0] - xOrigScale(x.domain()[0]) * zoom.scale(),
+                      zoom.translate()[1]])
     }
     if (y.domain()[0] < ymin) {
       y.domain([ymin, y.domain()[1] - y.domain()[0] + ymin]);
+
+      zoom.translate([zoom.translate()[0], yOrigScale.range()[0] - yOrigScale(y.domain()[0]) * zoom.scale()])
     }
     if (y.domain()[1] > ymax) {
       var ydom0 = y.domain()[0] - y.domain()[1] + ymax;
       y.domain([ydom0, ymax]);
+
+      zoom.translate([zoom.translate()[0], yOrigScale.range()[0] - yOrigScale(y.domain()[0]) * zoom.scale()])
     }
   }
   svg.select(".x.axis").call(xAxis);
@@ -164,4 +178,4 @@ svg.append("g")
 svg.append("g")
     .attr("class", "y axis")
     .call(yAxis);
-</script>
\ No newline at end of file
+</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.