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
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="http://d3js.org/d3.v3.min.js"></script> | |
<script src="http://d3js.org/d3.geo.tile.v0.min.js"></script> | |
<script src="http://d3js.org/queue.v1.min.js"></script> | |
<script src="http://d3js.org/topojson.v0.min.js"></script> | |
<script> | |
// slippy map code from | |
// http://bl.ocks.org/3943330 by tmcw | |
// http://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="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://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 "http://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> |