Skip to content

Instantly share code, notes, and snippets.

@serdaradali
Last active April 29, 2024 21:22
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save serdaradali/11346541 to your computer and use it in GitHub Desktop.
Save serdaradali/11346541 to your computer and use it in GitHub Desktop.
Interactive world globe

Zoomable/rotatable world globe that uses orthographic projection. Drag behavior is enhanced as described here: https://www.jasondavies.com/maps/rotate/

Performance is not good due to redrawing whole world upon zoom/drag.

// Copyright (c) 2013, 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,
zoomPoint,
event = d3.dispatch("zoomstart", "zoom", "zoomend"),
zoom = d3.behavior.zoom()
.on("zoomstart", function() {
var mouse0 = d3.mouse(this),
rotate = quaternionFromEuler(projection.rotate()),
point = position(projection, mouse0);
if (point) zoomPoint = point;
zoomOn.call(zoom, "zoom", function() {
projection.scale(d3.event.scale);
var mouse1 = d3.mouse(this),
between = rotateBetween(zoomPoint, position(projection, mouse1));
projection.rotate(eulerFromQuaternion(rotate = between
? multiply(rotate, between)
: multiply(bank(projection, mouse0, mouse1), rotate)));
mouse0 = mouse1;
event.zoom.apply(this, arguments);
});
event.zoomstart.apply(this, arguments);
})
.on("zoomend", function() {
zoomOn.call(zoom, "zoom", null);
event.zoomend.apply(this, arguments);
}),
zoomOn = zoom.on;
zoom.projection = function(_) {
return arguments.length ? zoom.scale((projection = _).scale()) : projection;
};
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 t = projection.translate(),
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];
}
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
];
}
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]
];
}
})();
<!DOCTYPE html>
<meta charset="utf-8">
<style>
#globe{
background: #fcfcfa;
width: 900px;
height: 500px;
margin-left: 50px;
}
.stroke {
fill: none;
stroke: #000;
stroke-width: 3px;
}
.fill {
fill: #fff;
}
.graticule {
fill: none;
stroke: #777;
stroke-width: .5px;
stroke-opacity: .5;
}
.land {
fill: #222;
}
.boundary {
fill: none;
stroke: #fff;
stroke-width: .5px;
}
.overlay {
fill: none;
pointer-events: all;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script src="d3.geo.zoom.js"></script>
<div id="globe"></div>
<script>
var width = 680,
height = 680;
var projection = d3.geo.orthographic()
.scale(270)
.translate([width / 2, height / 2])
.clipAngle(90)
.precision(.1);
var zoom = d3.behavior.zoom()
.scaleExtent([1,6])
.on("zoom",zoomed);
var zoomEnhanced = d3.geo.zoom().projection(projection)
.on("zoom",zoomedEnhanced);
var drag = d3.behavior.drag()
.origin(function() { var r = projection.rotate(); return {x: r[0], y: -r[1]}; })
.on("drag", dragged)
.on("dragstart", dragstarted)
.on("dragend", dragended);
var path = d3.geo.path()
.projection(projection);
var graticule = d3.geo.graticule();
var svg = d3.select("#globe").append("svg")
.attr("width", width)
.attr("height", height);
var pathG = svg.append("g");
svg.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height)
.call(zoomEnhanced)
pathG.append("defs").append("path")
.datum({type: "Sphere"})
.attr("id", "sphere")
.attr("d", path);
pathG.append("use")
.attr("class", "stroke")
.attr("xlink:href", "#sphere");
pathG.append("use")
.attr("class", "fill")
.attr("xlink:href", "#sphere");
pathG.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path);
d3.json("worldTopo.json", function(error, world) {
// to render meridians/graticules on top of lands, use insert which adds new path before graticule in the selection
pathG.insert("path", ".graticule")
.datum(topojson.feature(world, world.objects.land))
.attr("class", "land")
.attr("d", path)
pathG.insert("path", ".graticule")
.datum(topojson.mesh(world, world.objects.countries, function(a, b) { return a !== b; }))
.attr("class", "boundary")
.attr("d", path);
});
// apply transformations to map and all elements on it
function zoomed()
{
pathG.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
//grids.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
//geofeatures.select("path.graticule").style("stroke-width", 0.5 / d3.event.scale);
pathG.selectAll("path.boundary").style("stroke-width", 0.5 / d3.event.scale);
}
function zoomedEnhanced()
{
pathG.selectAll("path").attr("d",path);
}
function dragstarted(d)
{
//stopPropagation prevents dragging to "bubble up" which triggers same event for all elements below this object
d3.event.sourceEvent.stopPropagation();
d3.select(this).classed("dragging", true);
}
function dragged() {
projection.rotate([d3.event.x, -d3.event.y]);
pathG.selectAll("path").attr("d", path);
}
function dragended(d)
{
d3.select(this).classed("dragging", false);
}
d3.select(self.frameElement).style("height", height + "px");
</script>
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