Skip to content

Instantly share code, notes, and snippets.

@jasondavies
Last active October 6, 2018 08:54
Show Gist options
  • Save jasondavies/3689931 to your computer and use it in GitHub Desktop.
Save jasondavies/3689931 to your computer and use it in GitHub Desktop.
Zoom by Rectangle

An example of setting x- and y-scale domains (zooming) by selecting a rectangular region using the mouse. The original question required that it work in tandem with d3.behavior.zoom for panning, hence the checkbox for switching on the “zoom by rectangle” mode.

<!DOCTYPE html>
<meta charset="utf-8">
<title>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: 10px sans-serif;
shape-rendering: crispEdges;
}
rect {
fill: #ddd;
}
rect.zoom {
stroke: steelblue;
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: 12, left: 36},
width = 960 - margin.left - margin.right,
height = 430 - margin.top - margin.bottom;
var x = d3.scale.linear()
.domain([-width / 2, width / 2])
.range([0, width]);
var y = d3.scale.linear()
.domain([-height / 2, height / 2])
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickSize(-height);
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5)
.tickSize(-width);
var zoom = d3.behavior.zoom().x(x).y(y).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()))
.y(y.domain([origin[1], m[1]].map(y.invert).sort()));
}
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);
function refresh() {
svg.select(".x.axis").call(xAxis);
svg.select(".y.axis").call(yAxis);
}
</script>
Copyright 2012 Jason Davies https://www.jasondavies.com
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
@usamec
Copy link

usamec commented Jun 10, 2014

This line is wrong:

zoom.x(x.domain([origin[0], m[0]].map(x.invert).sort()))

when the input domain is something like (90, 100), it gets sorted as (100, 90) (js sort arrays as strings).

Should be:

zoom.x(x.domain([origin[0], m[0]].map(x.invert).sort(function(a,b) {return a-b;})))

(and the same for y axis).

@noscript
Copy link

noscript commented Jun 1, 2015

@usamec Thanks for the fix!

@akshayKhot
Copy link

akshayKhot commented Jun 7, 2016

Thank you for this example.
What does the following code means?
.on("mousemove.zoomRect", function() {

I am not aware of this syntax. I tried the docs but not sure what to look for. zoomRect is a boolean variable, what does it mean when we say mousemove.zoomRect ?

Any help is appreciated. Thanks.

@buenjybar
Copy link

@akshayKhot,
the following command will add an event listener mousemove with a naming/tag zoomRect which is a single string used to differentiate other mousemove events. We use it here to unsubscribe it at the mouseup event !

you can change the zoomRect with foo as long as you are consistent with the unsubscribe call.

.on("mousemove.foo", function() { ... }) // add event listener
.on("mousemove.foo", null) // remove event listener

with regards,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment