Skip to content

Instantly share code, notes, and snippets.

@KoGor
Last active September 28, 2016 20:41
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save KoGor/7024936 to your computer and use it in GitHub Desktop.
Save KoGor/7024936 to your computer and use it in GitHub Desktop.
Globe to Map III

Globe rotating manually via drag event, when mouse click event fires - transition begins. For transition backwards use double click on any country. In this example transition is based on interpolation between two projections and map always cut along antimeridian, it creates effect of unreelling/reelling.

For other variations of transition from Orthographic to Equirectangular and back check these examples:

This projected is licensed under the terms of the MIT license.

<!DOCTYPE html>
<html lang="en">
<meta charset="utf-8">
<head>
<title>Globe to Map transition</title>
</head>
<body>
<!-- start -->
<div id="map">
<link href="mapStyle.css" rel="stylesheet">
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script src="http://d3js.org/queue.v1.min.js"></script>
<script src="unreelGlobe2Map.js"></script>
</div>
<!-- end -->
</body>
</html>
#map {
position: relative;
}
svg {
background: #FFF;
pointer-events: all;
}
.zone {
stroke-width: .5px;
stroke-linejoin: round;
cursor: pointer;
}
.zoneTooltip {
position: absolute;
display: none;
pointer-events: none;
background: #fff;
padding: 3px;
text-align: left;
border: solid #ccc 1px;
color: #666;
font-size: 12px;
font-family: sans-serif;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
}
.infoLabel {
position: absolute;
bottom: 15px;
left: 10px;
display: none;
border: solid #ccc 1px;
color: #666;
background: #FFFFFF;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
-webkit-box-shadow: 0px 0px 3px #66C2FF;
-moz-box-shadow: 0px 0px 3px #66C2FF;
box-shadow: 0px 0px 3px #66C2FF;
text-align: left;
padding: 2px;
font-size: 12px;
font-family: sans-serif;
pointer-events: none;
}
.infoLabel span {
margin: 0px 5px;
}
.mapData {
fill: #E6E6E6;
stroke: #ffffff;
stroke-width: 1px;
}
.ortho {
fill: #E6E6E6;
stroke: #E6E6E6;
stroke-width: 1px;
}
.mapData:hover {
fill: #66C2FF;
}
.focused {
fill: #FF7519;
}
var mapWidth = 960,
mapHeight = 500,
focused = false,
ortho = true,
sens = 0.25;
var projectionGlobe = d3.geo.orthographic()
.scale(240)
.center([0, 0])
.translate([mapWidth / 2, mapHeight / 2])
.clipAngle(90);
var projectionMap = d3.geo.equirectangular()
.scale(145)
.center([0, 0])
.translate([mapWidth / 2, mapHeight / 2])
var projection = projectionGlobe;
var path = d3.geo.path()
.projection(projection);
var globe2map = interpolatedProjection(projectionGlobe, projectionMap),
map2globe = interpolatedProjection(projectionMap, projectionGlobe);
var svgMap = d3.select("div#map").append("svg")
.attr("overflow", "hidden")
.attr("width", mapWidth)
.attr("height", mapHeight);
var zoneTooltip = d3.select("div#map").append("div").attr("class", "zoneTooltip"),
pointLlabel = d3.select("div#map").append("div").attr("class", "pointLabel"),
infoLabel = d3.select("div#map").append("div").attr("class", "infoLabel");
var g = svgMap.append("g");
//Unreelling transformation
function animation(interProj) {
defaultRotate();
g.transition()
.duration(7500)
.tween("projection", function() {
return function(_) {
interProj.alpha(_);
g.selectAll("path").attr("d", path);
};
})
}
function interpolatedProjection(a, b) {
var projection = d3.geo.projection(raw).scale(1),
center = projection.center,
translate = projection.translate,
clip = projection.clipAngle,
α;
function raw(λ, φ) {
var pa = a([λ *= 180 / Math.PI, φ *= 180 / Math.PI]), pb = b([λ, φ]);
return [(1 - α) * pa[0] + α * pb[0], (α - 1) * pa[1] - α * pb[1]];
}
projection.alpha = function(_) {
if (!arguments.length) return α;
α = +_;
var ca = a.center(), cb = b.center(),
ta = a.translate(), tb = b.translate();
center([(1 - α) * ca[0] + α * cb[0], (1 - α) * ca[1] + α * cb[1]]);
translate([(1 - α) * ta[0] + α * tb[0], (1 - α) * ta[1] + α * tb[1]]);
if (ortho === true) {clip(180 - α * 90);}
return projection;
};
delete projection.scale;
delete projection.translate;
delete projection.center;
return projection.alpha(0);
}
//Rotate to default before animation
function defaultRotate() {
d3.transition()
.duration(1500)
.tween("rotate", function() {
var r = d3.interpolate(projection.rotate(), [0, 0]);
return function(t) {
projection.rotate(r(t));
g.selectAll("path").attr("d", path);
};
})
};
//Starter for function AFTER All transitions
function endall(transition, callback) {
var n = 0;
transition
.each(function() { ++n; })
.each("end", function() { if (!--n) callback.apply(this, arguments); });
}
//Loading data
queue()
.defer(d3.json, "/d/5685937/world-110m.json")
.defer(d3.tsv, "/d/5685937/world-110m-country-names.tsv")
.await(ready);
function ready(error, world, countryData) {
var countryById = {},
countries = topojson.feature(world, world.objects.countries).features;
//Adding countries by name
countryData.forEach(function(d) {
countryById[d.id] = d.name;
});
//Drawing countries on the globe
var world = g.selectAll("path").data(countries);
world.enter().append("path")
.attr("class", "mapData")
.attr("d", path)
.classed("ortho", ortho = true);
//Drag event
world.call(d3.behavior.drag()
.origin(function() { var r = projection.rotate(); return {x: r[0] / sens, y: -r[1] / sens}; })
.on("drag", function() {
var λ = d3.event.x * sens,
φ = -d3.event.y * sens,
rotate = projection.rotate();
//Restriction for rotating upside-down
φ = φ > 30 ? 30 :
φ < -30 ? -30 :
φ;
projection.rotate([λ, φ]);
g.selectAll("path.ortho").attr("d", path);
g.selectAll(".focused").classed("focused", focused = false);
}))
//Events processing
world.on("mouseover", function(d) {
if (ortho === true) {
infoLabel.text(countryById[d.id])
.style("display", "inline");
} else {
zoneTooltip.text(countryById[d.id])
.style("left", (d3.event.pageX + 7) + "px")
.style("top", (d3.event.pageY - 15) + "px")
.style("display", "block");
}
})
.on("mouseout", function(d) {
if (ortho === true) {
infoLabel.style("display", "none");
} else {
zoneTooltip.style("display", "none");
}
})
.on("mousemove", function() {
if (ortho === false) {
zoneTooltip.style("left", (d3.event.pageX + 7) + "px")
.style("top", (d3.event.pageY - 15) + "px");
}
})
.on("click", function(d) {
if (focused === d) return reset();
g.selectAll(".focused").classed("focused", false);
d3.select(this).classed("focused", focused = d);
infoLabel.text(countryById[d.id])
.style("display", "inline");
//Transforming Globe to Map
if (ortho === true) {
defaultRotate();
setTimeout(function() {
projection = globe2map;
path.projection(projection);
animation(projection);
g.selectAll(".ortho").classed("ortho", ortho = false);
}
, 1600);
}
});
//Adding extra data when focused
function focus(d) {
if (focused === d) return reset();
g.selectAll(".focused").classed("focused", false);
d3.select(this).classed("focused", focused = d);
}
//Reset projection
function reset() {
g.selectAll(".focused").classed("focused", focused = false);
infoLabel.style("display", "none");
zoneTooltip.style("display", "none");
//Transforming Map to Globe
projection = map2globe;
path.projection(projection);
animation(projection);
g.selectAll("path").classed("ortho", ortho = true);
}
};
@tshachar
Copy link

tshachar commented Nov 5, 2014

have you manged to solve the country spreading issue?
as far as i can see the country's that get spread are the ones that are connected at the edges of the flat map to the left side.

wish i could help with code fix but i'm new to D3 and still learning to understand this amazing thing.

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