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> |