Extent indicator globe using d3.geo.orthographic and radial gradients.
Slippy map code from:
http://bl.ocks.org/3943330 by tmcw
http://bl.ocks.org/4132797 by mbostock
Map tiles from Stamen
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
margin: 0;
}
/* misc */
.info {
position: absolute;
bottom: 10px;
left: 10px;
font: 14px sans-serif;
}
.attrib {
position: absolute;
bottom: 10px;
right: 10px;
font: 10px sans-serif;
padding: 5px;
background-color:white;
opacity:.8;
}
.attrib a {
color: black;
font-weight:800;
}
/* tiles */
.map {
position: relative;
overflow: hidden;
}
.layer {
position: absolute;
}
.tile {
position: absolute;
width: 256px;
height: 256px;
opacity:.8;
}
/* globe */
svg {
position:absolute;
bottom:10px;
}
.land {
fill: rgb(84, 77, 69);
stroke-opacity: 1;
}
.countries path {
stroke: rgb(80, 64, 39);
stroke-linejoin: round;
stroke-width:.5;
fill: rgb(117, 87, 57);
opacity: .1;
}
.countries path:hover {
fill-opacity:.1;
stroke-width:1;
opacity: 1;
}
.graticule {
fill: none;
stroke: black;
stroke-width:.5;
opacity:.3;
}
.extent {
fill: #933;
opacity: .6;
}
.noclicks {
pointer-events:none;
}
.point { fill:rgb(57, 38, 19); }
/* point classes */
.point.r1 { opacity: .8; }
.point.r2 { opacity: .8; }
.point.r3,
.point.r4,
.point.r5 { opacity: .3; }
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="//d3js.org/d3.geo.tile.v0.min.js"></script>
<script src="//d3js.org/queue.v1.min.js"></script>
<script src="//d3js.org/topojson.v0.min.js"></script>
<script>
// slippy map code from
// //bl.ocks.org/3943330 by tmcw
// //bl.ocks.org/4132797 by mbostock
var width = window.innerWidth,
height = window.innerHeight,
prefix = prefixMatch(["webkit", "ms", "Moz", "O"]);
var inset = {
w: 320,
h: 320,
projection: null, extentRect: null, svg: null, path: null, graticule: null,
init: function() {
inset.projection = d3.geo.orthographic()
.scale(140)
.translate([inset.w / 2, inset.h / 2])
.clipAngle(90)
inset.path = d3.geo.path()
.projection(inset.projection)
.pointRadius(1.5);
inset.graticule = d3.geo.graticule();
inset.extentRect = [{
"type": "Feature",
"geometry": { "type": "Polygon", "coordinates": [[]]}
}]
inset.svg = d3.select("body").append("svg")
.attr("width", inset.w)
.attr("height", inset.h)
.attr("class","noclicks")
queue()
.defer(d3.json, "world-110m.json")
.defer(d3.json, "places.json")
.await(inset.ready);
},
ready: function(error,world,places) {
var scale = inset.projection.scale();
var defs = inset.svg.append("defs")
var ocean = defs.append("radialGradient")
.attr("id", "ocean")
.attr("cx", "75%")
.attr("cy", "25%");
ocean.append("stop").attr("offset", "5%").attr("stop-color", "#e6e6f4");
ocean.append("stop").attr("offset", "100%").attr("stop-color", "#a2abb3");
var highlight = defs.append("radialGradient")
.attr("id", "highlight")
.attr("cx", "75%")
.attr("cy", "25%");
highlight.append("stop")
.attr("offset", "5%").attr("stop-color", "#ffd")
.attr("stop-opacity","0.6");
highlight.append("stop")
.attr("offset", "100%").attr("stop-color", "#ba9")
.attr("stop-opacity","0.2");
var shade = defs.append("radialGradient")
.attr("id", "shade")
.attr("cx", "50%")
.attr("cy", "40%");
shade.append("stop")
.attr("offset","50%").attr("stop-color", "#a2abb3")
.attr("stop-opacity","0")
shade.append("stop")
.attr("offset","100%").attr("stop-color", "#57616b")
.attr("stop-opacity","0.3")
var halo = defs.append("radialGradient")
.attr("id", "halo")
.attr("cx", "50%")
.attr("cy", "50%");
halo.append("stop")
.attr("offset","85%").attr("stop-color", "#FFF")
.attr("stop-opacity","1")
halo.append("stop")
.attr("offset","100%").attr("stop-color", "#FFF")
.attr("stop-opacity","0")
inset.svg.append("ellipse")
.attr("cx", inset.w/2).attr("cy", inset.h/2)
.attr("rx", scale+20)
.attr("ry", scale+20)
.attr("class", "noclicks")
.style("fill", "url(#halo)");
inset.svg.append("circle")
.attr("cx", inset.w / 2).attr("cy", inset.h / 2)
.attr("r", scale)
.attr("class", "noclicks")
.style("fill", "url(#ocean)");
inset.svg.append("path")
.datum(topojson.object(world, world.objects.land))
.attr("class", "land")
.attr("d", inset.path);
inset.svg.append("path")
.datum(inset.graticule)
.attr("class", "graticule noclicks")
.attr("d", inset.path);
inset.svg.append("circle")
.attr("cx", inset.w / 2).attr("cy", inset.h / 2)
.attr("r", scale)
.attr("class","noclicks")
.style("fill", "url(#highlight)");
inset.svg.append("circle")
.attr("cx", inset.w / 2).attr("cy", inset.h/ 2)
.attr("r", scale)
.attr("class","noclicks")
.style("fill", "url(#shade)");
inset.svg.append("g").attr("class","points")
.selectAll("text").data(places.features)
.enter().append("path")
.attr("class", function(d){
return "point r" + (5-d.properties.scalerank)
})
.attr("d", inset.path);
inset.svg.append("g").attr("class","extents")
.selectAll("path").data(inset.extentRect)
.enter().append("path")
.attr("class", "extent")
.attr("d", inset.path);
},
refresh: function(dims) {
inset.projection.rotate([-dims.center[0],-dims.center[1]])
var e = dims.topline.concat(dims.bottomline);
e.push([dims.topline[0]])
inset.extentRect[0].geometry.coordinates[0] = e;
inset.svg.select(".extent").attr("d", inset.path);
inset.svg.select(".land").attr("d", inset.path);
inset.svg.select(".graticule").attr("d", inset.path);
inset.svg.select(".extent").attr("d", inset.path);
inset.svg.selectAll(".point").attr("d", inset.path);
}
}
var bg = {
tile: null,
init: function() {},
refresh: function() {},
}
var tile = d3.geo.tile()
.size([width, height]);
var projection = d3.geo.mercator();
var zoom = d3.behavior.zoom()
.scale(1 << 13)
.scaleExtent([1 << 12, 1 << 23])
.translate([width / 2, height / 2])
.on("zoom", refresh);
var map = d3.select("body").append("div")
.attr("class", "map")
.style("width", width + "px")
.style("height", height + "px")
.call(zoom)
.on("mousemove", mousemoved);
var layer = map.append("div").attr("class", "layer");
var info = map.append("div").attr("class", "info");
var attrib = map.append("div").attr("class", "attrib").html('Map tiles by <a href="//stamen.com">Stamen Design</a>, under <a href="//creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="//openstreetmap.org">OpenStreetMap</a>, under <a href="//creativecommons.org/licenses/by-sa/3.0">CC BY SA</a>.')
inset.init();
refresh();
function refresh() {
var tiles = tile
.scale(zoom.scale())
.translate(zoom.translate())
();
projection
.scale(zoom.scale())
.translate(zoom.translate());
var map_dims = {
topline: [],
bottomline: [],
center: projection.invert([width/2,height/2])
// for static globe / moving extent
// center: [-70,45]
}
var samples = 8,
step = width/samples;
for (var i = 0; i < samples; i++) {
map_dims.topline
.push(projection.invert( [step*i,0] ))
map_dims.bottomline
.push(projection.invert( [step*(samples-i-1),height] ))
}
inset.refresh(map_dims)
var image = layer
.style(prefix + "transform", matrix3d(tiles.scale, tiles.translate))
.selectAll(".tile")
.data(tiles, function(d) { return d; });
image.exit().remove();
image.enter().append("img")
.attr("class", "tile")
.attr("src", function(d) { return "//tile.stamen.com/toner-lite/" + d[2] + "/" + d[0] + "/" + d[1] + ".png"; })
.style("left", function(d) { return (d[0] << 8) + "px"; })
.style("top", function(d) { return (d[1] << 8) + "px"; });
}
function mousemoved() {
info.text(formatLocation(projection.invert(d3.mouse(this)), zoom.scale()));
}
function matrix3d(scale, translate) {
var k = scale / 256, r = scale % 1 ? Number : Math.round;
return "matrix3d(" + [k, 0, 0, 0, 0, k, 0, 0, 0, 0, k, 0, r(translate[0] * scale), r(translate[1] * scale), 0, 1 ] + ")";
}
function prefixMatch(p) {
var i = -1, n = p.length, s = document.body.style;
while (++i < n) if (p[i] + "Transform" in s) return "-" + p[i].toLowerCase() + "-";
return "";
}
function formatLocation(p, k) {
var format = d3.format("." + Math.floor(Math.log(k) / 2 - 2) + "f");
return (p[1] < 0 ? format(-p[1]) + "°S" : format(p[1]) + "°N") + " "
+ (p[0] < 0 ? format(-p[0]) + "°W" : format(p[0]) + "°E");
}
</script>