Skip to content

Commit 61d3d89

Browse files
committed
Rewrite of d3.behavior.zoom.
The extent functionality is simplified, such that the zoom behavior only supports an extent on the scale factor. Furthermore, the extent is expressed as a scale factor rather than a zoom level, for consistency. An extent on translate is no longer supported; this isn't possible unless the zoom behavior also knows the size of the canvas. The scale transform functionality is also simplified; simply register an x or y scale, and the zoom behavior will automatically update the domain before a zoom event is dispatched. If you change the scale's domain programmatically, you can simply reassign the scale to the zoom behavior. Both of these changes are strictly backwards-incompatible. However, since this class is undocumented, I'll probably let this slide with a minor version bump rather than waiting until v3. This commit also reduces the scroll speed slightly, since the previous implementation seemed a little too fast for my liking. It would be easy to make the scroll speed configurable as a future enhancement. Implementation note: rather than using global event listeners on the window, and needing to copy local state into globals on interaction start, the zoom behavior now uses closures that are temporarily bound (e.g., mousemove and mouseup are bound on mousedown, and removed on mouseup). Fixes d3#484, allowing scale and translate to be get or set externally. Fixes d3#485, providing an example of integration with d3.geo.projection. Fixes d3#487, using a scale extent rather than a zoom-level extent.
1 parent 76400ac commit 61d3d89

6 files changed

Lines changed: 434 additions & 538 deletions

File tree

d3.js

Lines changed: 137 additions & 182 deletions
Original file line numberDiff line numberDiff line change
@@ -4517,102 +4517,175 @@ function d3_behavior_dragClick() {
45174517
d3_behavior_dragMoved = 0;
45184518
}
45194519
}
4520-
// TODO unbind zoom behavior?
45214520
d3.behavior.zoom = function() {
4522-
var xyz = [0, 0, 0],
4521+
var translate = [0, 0],
4522+
translate0, // translate when we started zooming (to avoid drift)
4523+
scale = 1,
4524+
scale0, // scale when we started touching
4525+
scaleExtent = d3_behavior_zoomInfinity,
45234526
event = d3.dispatch("zoom"),
4524-
extent = d3_behavior_zoomInfiniteExtent;
4527+
x0,
4528+
x1,
4529+
y0,
4530+
y1,
4531+
touchtime; // time of last touchstart (to detect double-tap)
45254532

45264533
function zoom() {
45274534
this
45284535
.on("mousedown.zoom", mousedown)
45294536
.on("mousewheel.zoom", mousewheel)
4537+
.on("mousemove.zoom", mousemove)
45304538
.on("DOMMouseScroll.zoom", mousewheel)
45314539
.on("dblclick.zoom", dblclick)
4532-
.on("touchstart.zoom", touchstart);
4540+
.on("touchstart.zoom", touchstart)
4541+
.on("touchmove.zoom", touchmove)
4542+
.on("touchend.zoom", touchstart);
4543+
}
45334544

4534-
d3.select(window)
4535-
.on("mousemove.zoom", d3_behavior_zoomMousemove)
4536-
.on("mouseup.zoom", d3_behavior_zoomMouseup)
4537-
.on("touchmove.zoom", d3_behavior_zoomTouchmove)
4538-
.on("touchend.zoom", d3_behavior_zoomTouchup)
4539-
.on("click.zoom", d3_behavior_zoomClick, true);
4545+
zoom.translate = function(x) {
4546+
if (!arguments.length) return translate;
4547+
translate = x.map(Number);
4548+
return zoom;
4549+
};
4550+
4551+
zoom.scale = function(x) {
4552+
if (!arguments.length) return scale;
4553+
scale = +x;
4554+
return zoom;
4555+
};
4556+
4557+
zoom.scaleExtent = function(x) {
4558+
if (!arguments.length) return scaleExtent;
4559+
scaleExtent = x == null ? d3_behavior_zoomInfinity : x.map(Number);
4560+
return zoom;
4561+
};
4562+
4563+
zoom.x = function(z) {
4564+
if (!arguments.length) return x1;
4565+
x1 = z;
4566+
x0 = z.copy();
4567+
return zoom;
4568+
};
4569+
4570+
zoom.y = function(z) {
4571+
if (!arguments.length) return y1;
4572+
y1 = z;
4573+
y0 = z.copy();
4574+
return zoom;
4575+
};
4576+
4577+
function location(p) {
4578+
return [(p[0] - translate[0]) / scale, (p[1] - translate[1]) / scale];
45404579
}
45414580

4542-
// snapshot the local context for subsequent dispatch
4543-
function start() {
4544-
d3_behavior_zoomXyz = xyz;
4545-
d3_behavior_zoomExtent = extent;
4546-
d3_behavior_zoomDispatch = event.zoom;
4547-
d3_behavior_zoomEventTarget = d3.event.target;
4548-
d3_behavior_zoomTarget = this;
4549-
d3_behavior_zoomArguments = arguments;
4581+
function point(l) {
4582+
return [l[0] * scale + translate[0], l[1] * scale + translate[1]];
4583+
}
4584+
4585+
function scaleTo(s) {
4586+
scale = Math.max(scaleExtent[0], Math.min(scaleExtent[1], s));
4587+
}
4588+
4589+
function translateTo(p, l) {
4590+
l = point(l);
4591+
translate[0] += p[0] - l[0];
4592+
translate[1] += p[1] - l[1];
4593+
}
4594+
4595+
function dispatch(argumentz) {
4596+
var o = d3.event; // events can by reentrant (e.g., focus);
4597+
d3.event = {scale: scale, translate: translate, target: zoom};
4598+
if (x1) x1.domain(x0.range().map(function(x) { return (x - translate[0]) / scale; }).map(x0.invert));
4599+
if (y1) y1.domain(y0.range().map(function(y) { return (y - translate[1]) / scale; }).map(y0.invert));
4600+
try { event.zoom.apply(zoom, argumentz); }
4601+
finally { d3.event = o; }
4602+
o.preventDefault();
45504603
}
45514604

45524605
function mousedown() {
4553-
start.apply(this, arguments);
4554-
d3_behavior_zoomPanning = d3_behavior_zoomLocation(d3.svg.mouse(d3_behavior_zoomTarget));
4555-
d3_behavior_zoomMoved = 0;
4556-
d3.event.preventDefault();
4606+
var moved = 0,
4607+
that = this,
4608+
argumentz = arguments,
4609+
target = d3.event.target,
4610+
w = d3.select(window).on("mousemove.zoom", mousemove).on("mouseup.zoom", mouseup),
4611+
l = location(d3.svg.mouse(that));
4612+
45574613
window.focus();
4614+
d3_eventCancel();
4615+
4616+
function mousemove() {
4617+
moved = 1;
4618+
translateTo(d3.svg.mouse(that), l);
4619+
dispatch(argumentz);
4620+
}
4621+
4622+
function mouseup() {
4623+
if (moved) d3_eventCancel();
4624+
w.on("mousemove.zoom", null).on("mouseup.zoom", null).on("click.zoom", moved && d3.event.target === target ? click : null);
4625+
}
4626+
4627+
function click() {
4628+
d3_eventCancel();
4629+
w.on("click.zoom", null);
4630+
}
45584631
}
45594632

4560-
// store starting mouse location
45614633
function mousewheel() {
4562-
start.apply(this, arguments);
4563-
if (!d3_behavior_zoomZooming) d3_behavior_zoomZooming = d3_behavior_zoomLocation(d3.svg.mouse(d3_behavior_zoomTarget));
4564-
d3_behavior_zoomTo(d3_behavior_zoomDelta() + xyz[2], d3.svg.mouse(d3_behavior_zoomTarget), d3_behavior_zoomZooming);
4634+
if (!translate0) translate0 = location(d3.svg.mouse(this));
4635+
scaleTo(Math.pow(2, d3_behavior_zoomDelta() * .002) * scale);
4636+
translateTo(d3.svg.mouse(this), translate0);
4637+
dispatch(arguments);
4638+
}
4639+
4640+
function mousemove() {
4641+
translate0 = null;
45654642
}
45664643

45674644
function dblclick() {
4568-
start.apply(this, arguments);
4569-
var mouse = d3.svg.mouse(d3_behavior_zoomTarget);
4570-
d3_behavior_zoomTo(d3.event.shiftKey ? Math.ceil(xyz[2] - 1) : Math.floor(xyz[2] + 1), mouse, d3_behavior_zoomLocation(mouse));
4645+
var p = d3.svg.mouse(this), l = location(p);
4646+
scaleTo(d3.event.shiftKey ? scale / 2 : scale * 2);
4647+
translateTo(p, l);
4648+
dispatch(arguments);
45714649
}
45724650

4573-
// doubletap detection
45744651
function touchstart() {
4575-
start.apply(this, arguments);
4576-
var touches = d3_behavior_zoomTouchup(),
4577-
touch,
4652+
var touches = d3.svg.touches(this),
45784653
now = Date.now();
4579-
if ((touches.length === 1) && (now - d3_behavior_zoomLast < 300)) {
4580-
d3_behavior_zoomTo(1 + Math.floor(xyz[2]), touch = touches[0], d3_behavior_zoomLocations[touch.identifier]);
4654+
4655+
scale0 = scale;
4656+
translate0 = {};
4657+
touches.forEach(function(t) { translate0[t.identifier] = location(t); });
4658+
d3_eventCancel();
4659+
4660+
if ((touches.length === 1) && (now - touchtime < 500)) { // dbltap
4661+
var p = touches[0], l = location(touches[0]);
4662+
scaleTo(scale * 2);
4663+
translateTo(p, l);
4664+
dispatch(arguments);
45814665
}
4582-
d3_behavior_zoomLast = now;
4666+
touchtime = now;
4667+
}
4668+
4669+
function touchmove() {
4670+
var touches = d3.svg.touches(this),
4671+
p0 = touches[0],
4672+
l0 = translate0[p0.identifier];
4673+
if (p1 = touches[1]) {
4674+
var p1, l1 = translate0[p1.identifier];
4675+
p0 = [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2];
4676+
l0 = [(l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2];
4677+
scaleTo(d3.event.scale * scale0);
4678+
}
4679+
translateTo(p0, l0);
4680+
dispatch(arguments);
45834681
}
45844682

4585-
zoom.extent = function(x) {
4586-
if (!arguments.length) return extent;
4587-
extent = x == null ? d3_behavior_zoomInfiniteExtent : x;
4588-
return zoom;
4589-
};
4590-
45914683
return d3.rebind(zoom, event, "on");
45924684
};
45934685

4594-
var d3_behavior_zoomDiv,
4595-
d3_behavior_zoomPanning,
4596-
d3_behavior_zoomZooming,
4597-
d3_behavior_zoomLocations = {}, // identifier -> location
4598-
d3_behavior_zoomLast = 0,
4599-
d3_behavior_zoomXyz,
4600-
d3_behavior_zoomExtent,
4601-
d3_behavior_zoomDispatch,
4602-
d3_behavior_zoomEventTarget,
4603-
d3_behavior_zoomTarget,
4604-
d3_behavior_zoomArguments,
4605-
d3_behavior_zoomMoved;
4606-
4607-
function d3_behavior_zoomLocation(point) {
4608-
return [
4609-
point[0] - d3_behavior_zoomXyz[0],
4610-
point[1] - d3_behavior_zoomXyz[1],
4611-
d3_behavior_zoomXyz[2]
4612-
];
4613-
}
4686+
var d3_behavior_zoomDiv, // for interpreting mousewheel events
4687+
d3_behavior_zoomInfinity = [0, Infinity]; // default scale extent
46144688

4615-
// detect the pixels that would be scrolled by this wheel event
46164689
function d3_behavior_zoomDelta() {
46174690

46184691
// mousewheel events are totally broken!
@@ -4639,124 +4712,6 @@ function d3_behavior_zoomDelta() {
46394712
delta = e.wheelDelta || (-e.detail * 5);
46404713
}
46414714

4642-
return delta * .005;
4643-
}
4644-
4645-
// Note: Since we don't rotate, it's possible for the touches to become
4646-
// slightly detached from their original positions. Thus, we recompute the
4647-
// touch points on touchend as well as touchstart!
4648-
function d3_behavior_zoomTouchup() {
4649-
var touches = d3.svg.touches(d3_behavior_zoomTarget),
4650-
i = -1,
4651-
n = touches.length,
4652-
touch;
4653-
while (++i < n) d3_behavior_zoomLocations[(touch = touches[i]).identifier] = d3_behavior_zoomLocation(touch);
4654-
return touches;
4655-
}
4656-
4657-
function d3_behavior_zoomTouchmove() {
4658-
var touches = d3.svg.touches(d3_behavior_zoomTarget);
4659-
switch (touches.length) {
4660-
4661-
// single-touch pan
4662-
case 1: {
4663-
var touch = touches[0];
4664-
d3_behavior_zoomTo(d3_behavior_zoomXyz[2], touch, d3_behavior_zoomLocations[touch.identifier]);
4665-
break;
4666-
}
4667-
4668-
// double-touch pan + zoom
4669-
case 2: {
4670-
var p0 = touches[0],
4671-
p1 = touches[1],
4672-
p2 = [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2],
4673-
l0 = d3_behavior_zoomLocations[p0.identifier],
4674-
l1 = d3_behavior_zoomLocations[p1.identifier],
4675-
l2 = [(l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2, l0[2]];
4676-
d3_behavior_zoomTo(Math.log(d3.event.scale) / Math.LN2 + l0[2], p2, l2);
4677-
break;
4678-
}
4679-
}
4680-
}
4681-
4682-
function d3_behavior_zoomMousemove() {
4683-
d3_behavior_zoomZooming = null;
4684-
if (d3_behavior_zoomPanning) {
4685-
d3_behavior_zoomMoved = 1;
4686-
d3_behavior_zoomTo(d3_behavior_zoomXyz[2], d3.svg.mouse(d3_behavior_zoomTarget), d3_behavior_zoomPanning);
4687-
}
4688-
}
4689-
4690-
function d3_behavior_zoomMouseup() {
4691-
if (d3_behavior_zoomPanning) {
4692-
if (d3_behavior_zoomMoved) {
4693-
d3_eventCancel();
4694-
d3_behavior_zoomMoved = d3_behavior_zoomEventTarget === d3.event.target;
4695-
}
4696-
4697-
d3_behavior_zoomXyz =
4698-
d3_behavior_zoomExtent =
4699-
d3_behavior_zoomDispatch =
4700-
d3_behavior_zoomEventTarget =
4701-
d3_behavior_zoomTarget =
4702-
d3_behavior_zoomArguments =
4703-
d3_behavior_zoomPanning = null;
4704-
}
4705-
}
4706-
4707-
function d3_behavior_zoomClick() {
4708-
if (d3_behavior_zoomMoved) {
4709-
d3_eventCancel();
4710-
d3_behavior_zoomMoved = 0;
4711-
}
4712-
}
4713-
4714-
function d3_behavior_zoomTo(z, x0, x1) {
4715-
z = d3_behavior_zoomExtentClamp(z, 2);
4716-
var j = Math.pow(2, d3_behavior_zoomXyz[2]),
4717-
k = Math.pow(2, z),
4718-
K = Math.pow(2, (d3_behavior_zoomXyz[2] = z) - x1[2]),
4719-
x_ = d3_behavior_zoomXyz[0],
4720-
y_ = d3_behavior_zoomXyz[1],
4721-
x = d3_behavior_zoomXyz[0] = d3_behavior_zoomExtentClamp((x0[0] - x1[0] * K), 0, k),
4722-
y = d3_behavior_zoomXyz[1] = d3_behavior_zoomExtentClamp((x0[1] - x1[1] * K), 1, k),
4723-
o = d3.event; // Events can be reentrant (e.g., focus).
4724-
4725-
d3.event = {
4726-
scale: k,
4727-
translate: [x, y],
4728-
transform: function(sx, sy) {
4729-
if (sx) transform(sx, x_, x);
4730-
if (sy) transform(sy, y_, y);
4731-
}
4732-
};
4733-
4734-
function transform(scale, a, b) {
4735-
scale.domain(scale.range().map(function(v) { return scale.invert(((v - b) * j) / k + a); }));
4736-
}
4737-
4738-
try {
4739-
d3_behavior_zoomDispatch.apply(d3_behavior_zoomTarget, d3_behavior_zoomArguments);
4740-
} finally {
4741-
d3.event = o;
4742-
}
4743-
4744-
o.preventDefault();
4745-
}
4746-
4747-
var d3_behavior_zoomInfiniteExtent = [
4748-
[-Infinity, Infinity],
4749-
[-Infinity, Infinity],
4750-
[-Infinity, Infinity]
4751-
];
4752-
4753-
function d3_behavior_zoomExtentClamp(x, i, k) {
4754-
var range = d3_behavior_zoomExtent[i],
4755-
r0 = range[0],
4756-
r1 = range[1];
4757-
return arguments.length === 3
4758-
? Math.max(r1 * (r1 === Infinity ? -Infinity : 1 / k - 1),
4759-
Math.min(r0 === -Infinity ? Infinity : r0, x / k)) * k
4760-
: Math.max(r0, Math.min(r1, x));
4715+
return delta;
47614716
}
47624717
})();

d3.min.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)