Skip to content

Instantly share code, notes, and snippets.

@etpinard
Last active August 29, 2015 14:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save etpinard/7419d900811d14803162 to your computer and use it in GitHub Desktop.
Save etpinard/7419d900811d14803162 to your computer and use it in GitHub Desktop.
d3.geo.zoom without roll
// Copyright 2014, Jason Davies, http://www.jasondavies.com
// See LICENSE.txt for details.
(function() {
var radians = Math.PI / 180,
degrees = 180 / Math.PI;
// TODO make incremental rotate optional
d3.geo.zoom = function() {
var projection,
duration;
var zoomPoint,
zooming = 0,
event = d3_eventDispatch(zoom, "zoomstart", "zoom", "zoomend"),
zoom = d3.behavior.zoom()
.on("zoomstart", function() {
var mouse0 = d3.mouse(this),
rotate0 = projection.rotate(),
translate0 = projection.translate(),
q = quaternionFromEuler(projection.rotate()),
point = position(projection, mouse0);
if (point) zoomPoint = point;
zoomOn.call(zoom, "zoom", function() {
projection.scale(view.k = d3.event.scale);
var mouse1 = d3.mouse(this),
point1 = position(projection, mouse1),
between = rotateBetween(zoomPoint, point1);
var rotateAngles = view.r = eulerFromQuaternion(q = between
? multiply(q, between)
: multiply(bank(projection, mouse0, mouse1), q));
projection.rotate([
rotateAngles[0],
rotate0[1],
rotate0[2]
]);
projection.translate([
translate0[0],
d3.event.translate[1]
]);
mouse0 = mouse1;
zoomed(event.of(this, arguments));
});
zoomstarted(event.of(this, arguments));
})
.on("zoomend", function() {
zoomOn.call(zoom, "zoom", null);
zoomended(event.of(this, arguments));
}),
zoomOn = zoom.on,
view = {r: [0, 0, 0], k: 1};
zoom.rotateTo = function(location) {
var between = rotateBetween(cartesian(location), cartesian([-view.r[0], -view.r[1]]));
return eulerFromQuaternion(multiply(quaternionFromEuler(view.r), between));
};
zoom.projection = function(_) {
if (!arguments.length) return projection;
projection = _;
view = {r: projection.rotate(), k: projection.scale()};
zoom.translate(projection.translate())
return zoom.scale(view.k);
};
zoom.duration = function(_) {
return arguments.length ? (duration = _, zoom) : duration;
};
zoom.event = function(g) {
g.each(function() {
var g = d3.select(this),
dispatch = event.of(this, arguments),
view1 = view,
transition = d3.transition(g);
if (transition !== g) {
transition
.each("start.zoom", function() {
if (this.__chart__) { // pre-transition state
view = this.__chart__;
}
projection.rotate(view.r).scale(view.k);
zoomstarted(dispatch);
})
.tween("zoom:zoom", function() {
var width = zoom.size()[0],
i = interpolateBetween(quaternionFromEuler(view.r), quaternionFromEuler(view1.r)),
d = d3.geo.distance(view.r, view1.r),
smooth = d3.interpolateZoom([0, 0, width / view.k], [d, 0, width / view1.k]);
if (duration) transition.duration(duration(smooth.duration * .001)); // see https://github.com/mbostock/d3/pull/2045
return function(t) {
var uw = smooth(t);
this.__chart__ = view = {r: eulerFromQuaternion(i(uw[0] / d)), k: width / uw[2]};
projection.rotate(view.r).scale(view.k);
zoom.scale(view.k);
zoomed(dispatch);
};
})
.each("end.zoom", function() {
zoomended(dispatch);
});
try { // see https://github.com/mbostock/d3/pull/1983
transition
.each("interrupt.zoom", function() {
zoomended(dispatch);
});
} catch (ignore) { }
} else {
this.__chart__ = view;
zoomstarted(dispatch);
zoomed(dispatch);
zoomended(dispatch);
}
});
};
function zoomstarted(dispatch) {
if (!zooming++) dispatch({type: "zoomstart"});
}
function zoomed(dispatch) {
dispatch({type: "zoom"});
}
function zoomended(dispatch) {
if (!--zooming) dispatch({type: "zoomend"});
}
return d3.rebind(zoom, event, "on");
};
function bank(projection, p0, p1) {
var t = projection.translate(),
angle = Math.atan2(p0[1] - t[1], p0[0] - t[0]) - Math.atan2(p1[1] - t[1], p1[0] - t[0]);
return [Math.cos(angle / 2), 0, 0, Math.sin(angle / 2)];
}
function position(projection, point) {
var spherical = projection.invert(point);
return spherical && isFinite(spherical[0]) && isFinite(spherical[1]) && cartesian(spherical);
}
function quaternionFromEuler(euler) {
var λ = .5 * euler[0] * radians,
φ = .5 * euler[1] * radians,
γ = .5 * euler[2] * radians,
sinλ = Math.sin(λ), cosλ = Math.cos(λ),
sinφ = Math.sin(φ), cosφ = Math.cos(φ),
sinγ = Math.sin(γ), cosγ = Math.cos(γ);
return [
cosλ * cosφ * cosγ + sinλ * sinφ * sinγ,
sinλ * cosφ * cosγ - cosλ * sinφ * sinγ,
cosλ * sinφ * cosγ + sinλ * cosφ * sinγ,
cosλ * cosφ * sinγ - sinλ * sinφ * cosγ
];
}
function multiply(a, b) {
var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3],
b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3];
return [
a0 * b0 - a1 * b1 - a2 * b2 - a3 * b3,
a0 * b1 + a1 * b0 + a2 * b3 - a3 * b2,
a0 * b2 - a1 * b3 + a2 * b0 + a3 * b1,
a0 * b3 + a1 * b2 - a2 * b1 + a3 * b0
];
}
function rotateBetween(a, b) {
if (!a || !b) return;
var axis = cross(a, b),
norm = Math.sqrt(dot(axis, axis)),
halfγ = .5 * Math.acos(Math.max(-1, Math.min(1, dot(a, b)))),
k = Math.sin(halfγ) / norm;
return norm && [Math.cos(halfγ), axis[2] * k, -axis[1] * k, axis[0] * k];
}
// Interpolate between two quaternions (slerp).
function interpolateBetween(a, b) {
var d = Math.max(-1, Math.min(1, dot(a, b))),
s = d < 0 ? -1 : 1,
θ = Math.acos(s * d),
sinθ = Math.sin(θ);
return sinθ ? function(t) {
var A = s * Math.sin((1 - t) * θ) / sinθ,
B = Math.sin(t * θ) / sinθ;
return [
a[0] * A + b[0] * B,
a[1] * A + b[1] * B,
a[2] * A + b[2] * B,
a[3] * A + b[3] * B
];
} : function() { return a; };
}
function eulerFromQuaternion(q) {
return [
Math.atan2(2 * (q[0] * q[1] + q[2] * q[3]), 1 - 2 * (q[1] * q[1] + q[2] * q[2])) * degrees,
Math.asin(Math.max(-1, Math.min(1, 2 * (q[0] * q[2] - q[3] * q[1])))) * degrees,
Math.atan2(2 * (q[0] * q[3] + q[1] * q[2]), 1 - 2 * (q[2] * q[2] + q[3] * q[3])) * degrees
// 0
];
}
function cartesian(spherical) {
var λ = spherical[0] * radians,
φ = spherical[1] * radians,
cosφ = Math.cos(φ);
return [
cosφ * Math.cos(λ),
cosφ * Math.sin(λ),
Math.sin(φ)
];
}
function dot(a, b) {
for (var i = 0, n = a.length, s = 0; i < n; ++i) s += a[i] * b[i];
return s;
}
function cross(a, b) {
return [
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0]
];
}
// Like d3.dispatch, but for custom events abstracting native UI events. These
// events have a target component (such as a brush), a target element (such as
// the svg:g element containing the brush) and the standard arguments `d` (the
// target element's data) and `i` (the selection index of the target element).
function d3_eventDispatch(target) {
var i = 0,
n = arguments.length,
argumentz = [];
while (++i < n) argumentz.push(arguments[i]);
var dispatch = d3.dispatch.apply(null, argumentz);
// Creates a dispatch context for the specified `thiz` (typically, the target
// DOM element that received the source event) and `argumentz` (typically, the
// data `d` and index `i` of the target element). The returned function can be
// used to dispatch an event to any registered listeners; the function takes a
// single argument as input, being the event to dispatch. The event must have
// a "type" attribute which corresponds to a type registered in the
// constructor. This context will automatically populate the "sourceEvent" and
// "target" attributes of the event, as well as setting the `d3.event` global
// for the duration of the notification.
dispatch.of = function(thiz, argumentz) {
return function(e1) {
try {
var e0 =
e1.sourceEvent = d3.event;
e1.target = target;
d3.event = e1;
dispatch[e1.type].apply(thiz, argumentz);
} finally {
d3.event = e0;
}
};
};
return dispatch;
}
})();
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.zoomlayer {
fill: transparent;
}
.land {
fill: rgb(83, 207, 142);
}
.graticule {
fill: none;
stroke: grey;
}
</style>
<body>
<svg class='fig'></svg>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/d3.geo.projection.v0.min.js" charset="utf-8"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script src="d3.geo.zoom.js"></script>
<script>
(function () {
var pathToTopojson = './world110.json' // or world50.json
d3.json(pathToTopojson, main);
function main(err, world) {
var width = 720,
height = 400;
var countries = topojson.feature(world, world.objects.land);
var projection = d3.geo.kavrayskiy7()
.scale(120)
.rotate([0, 0, 0]);
// var projection = d3.geo.orthographic()
// .clipAngle(90)
// .scale(170)
// .rotate([0, 0, 0]);
projection.translate([width / 2, height / 2]);
window.projection = projection;
var path = d3.geo.path()
.projection(projection);
var graticule = d3.geo.graticule();
var svg = d3.select('.fig')
.attr('height', height)
.attr('width', width);
var g = svg.append("g")
.attr("class", "map");
g.append("rect")
.attr("class", "zoomlayer")
.attr("width", width)
.attr("height", height);
g.append("path")
.datum(countries)
.attr("d", path)
.attr("class", "land");
g.append("path")
.datum(graticule)
.attr("d", path)
.attr("class", "graticule");
var zoom = d3.geo.zoom()
.projection(projection)
.on("zoom.redraw", function() {
svg.selectAll("path")
.attr("d", path);
});
g
.call(zoom)
.on("dblclick.zoom", null);
}
})();
</script>
</body>
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment