A D3 V5 implementation of a shaded globe mimic the 3d effect. Drag to rotate and middle wheel to zoom. The flyer arcs are interpolated from 2 control points.
You can modify the dataset (Topojson format) to change landmarks.
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<title>D3 V5 - Faux-3d Shaded Globe With Zoom, Places and Arcs</title> | |
<style> | |
.land { | |
fill: rgb(117, 87, 57); | |
stroke-opacity: 1; | |
} | |
.countries path { | |
stroke: rgba(0, 0, 0, .1); | |
stroke-linejoin: round; | |
stroke-width: .5; | |
fill: transparent; | |
} | |
.countries path:hover { | |
stroke: rgba(0, 0, 0, .6); | |
fill-opacity: .3; | |
fill: white; | |
} | |
.graticule { | |
fill: none; | |
stroke: black; | |
stroke-width: .5; | |
opacity: .2; | |
} | |
.labels { | |
font: 8px sans-serif; | |
fill: black; | |
opacity: .5; | |
} | |
.noclicks { | |
pointer-events: none; | |
} | |
.point { | |
opacity: .6; | |
} | |
.arcs { | |
opacity: .1; | |
stroke: gray; | |
stroke-width: 3; | |
} | |
.flyers { | |
stroke-width: 1; | |
opacity: .6; | |
stroke: darkred; | |
} | |
.arc, | |
.flyer { | |
stroke-linejoin: round; | |
fill: none; | |
} | |
.arc {} | |
.flyer {} | |
.flyer:hover {} | |
</style> | |
</head> | |
<body> | |
<svg> | |
<defs> | |
<radialGradient cx="75%" cy="25%" id="ocean_fill"> | |
<stop offset="5%" stop-color="#ddf" /> | |
<stop offset="100%" stop-color="#9ab" /> | |
</radialGradient> | |
<radialGradient cx="75%" cy="25%" id="globe_highlight"> | |
<stop offset="5%" stop-color="#ffd" stop-opacity="0.6" /> | |
<stop offset="100%" stop-color="#ba9" stop-opacity="0.2" /> | |
</radialGradient> | |
<radialGradient cx="50%" cy="40%" id="globe_shading"> | |
<stop offset="50%" stop-color="#9ab" stop-opacity="0" /> | |
<stop offset="100%" stop-color="#3e6184" stop-opacity="0.3" /> | |
</radialGradient> | |
<radialGradient cx="50%" cy="50%" id="drop_shadow"> | |
<stop offset="20%" stop-color="#000" stop-opacity="0.5" /> | |
<stop offset="100%" stop-color="#000" stop-opacity="0" /> | |
</radialGradient> | |
</defs> | |
</svg> | |
<script src="http://d3js.org/d3.v5.min.js"></script> | |
<!-- Use d3-fetch instead of d3-request in ES6 --> | |
<script src="https://d3js.org/d3-request.v1.min.js"></script> | |
<script src="https://d3js.org/d3-queue.v3.min.js"></script> | |
<script src="http://d3js.org/topojson.v3.min.js"></script> | |
<script> | |
// References: | |
// http://bl.ocks.org/dwtkns/4686432 | |
// http://bl.ocks.org/dwtkns/4973620 | |
// http://bl.ocks.org/KoGor/5994804 | |
// https://medium.com/@xiaoyangzhao/drawing-curves-on-webgl-globe-using-three-js-and-d3-draft-7e782ffd7ab | |
// https://raw.githubusercontent.com/d3/d3.github.com/master/world-110m.v1.json | |
var width = 960, | |
height = 500, | |
radius = 220, | |
sensitivity = 0.25, | |
offsetX = width / 2, | |
offsetY = height / 2, | |
maxElevation = 45, | |
initRotation = [0, -30], | |
scaleExtent = [1, 8], | |
flyerAltitude = 80; | |
var projection = d3 | |
.geoOrthographic() | |
.scale(radius) | |
.rotate(initRotation) | |
.translate([offsetX, offsetY]) | |
.clipAngle(90); | |
var skyProjection = d3 | |
.geoOrthographic() | |
.scale(radius + flyerAltitude) | |
.rotate(initRotation) | |
.translate([offsetX, offsetY]) | |
.clipAngle(90); | |
var path = d3 | |
.geoPath() | |
.projection(projection) | |
.pointRadius(1.5); | |
var swoosh = d3.line() | |
.x(function (d) { return d[0] }) | |
.y(function (d) { return d[1] }) | |
.curve(d3.curveBasis); | |
var graticule = d3.geoGraticule(); | |
var svg = d3 | |
.select("svg") | |
.attr("width", width) | |
.attr("height", height) | |
.attr("transform-origin", offsetX + "px " + offsetY + "px") | |
.call( | |
d3 | |
.drag() | |
.subject(function () { | |
var r = projection.rotate(); | |
return { x: r[0] / sensitivity, y: -r[1] / sensitivity }; | |
}) | |
.on("drag", dragged) | |
) | |
.call( | |
d3 | |
.zoom() | |
.scaleExtent(scaleExtent) | |
.on("zoom", zoomed) | |
) | |
.on("dblclick.zoom", null); | |
d3.queue() | |
.defer(d3.json, "https://raw.githubusercontent.com/d3/d3.github.com/master/world-110m.v1.json") | |
.defer(d3.json, "places.json") | |
.defer(d3.json, "links.json") | |
.await(ready); | |
function ready(error, world, places, links) { | |
svg | |
.append("ellipse") | |
.attr("cx", offsetX - 40) | |
.attr("cy", offsetY + radius - 20) | |
.attr("rx", projection.scale() * 0.9) | |
.attr("ry", projection.scale() * 0.25) | |
.attr("class", "noclicks") | |
.style("fill", "url(#drop_shadow)"); | |
svg | |
.append("circle") | |
.attr("cx", offsetX) | |
.attr("cy", offsetY) | |
.attr("r", projection.scale()) | |
.attr("class", "noclicks") | |
.style("fill", "url(#ocean_fill)"); | |
svg | |
.append("path") | |
.datum(topojson.feature(world, world.objects.land)) | |
.attr("class", "land") | |
.attr("d", path); | |
svg | |
.append("path") | |
.datum(graticule) | |
.attr("class", "graticule noclicks") | |
.attr("d", path); | |
svg | |
.append("circle") | |
.attr("cx", offsetX) | |
.attr("cy", offsetY) | |
.attr("r", projection.scale()) | |
.attr("class", "noclicks") | |
.style("fill", "url(#globe_highlight)"); | |
svg | |
.append("circle") | |
.attr("cx", offsetX) | |
.attr("cy", offsetY) | |
.attr("r", projection.scale()) | |
.attr("class", "noclicks") | |
.style("fill", "url(#globe_shading)"); | |
svg | |
.append("g") | |
.attr("class", "points") | |
.selectAll(".point") | |
.data(places.features) | |
.enter() | |
.append("path") | |
.attr("class", "point") | |
.attr("d", path); | |
svg | |
.append("g") | |
.attr("class", "labels") | |
.selectAll(".label") | |
.data(places.features) | |
.enter() | |
.append("text") | |
.attr("class", "label") | |
.text(function (d) { | |
return d.properties.name; | |
}); | |
svg | |
.append("g") | |
.attr("class", "countries") | |
.selectAll("path") | |
.data(topojson.feature(world, world.objects.countries).features) | |
.enter() | |
.append("path") | |
.attr("d", path); | |
position_labels(); | |
svg.append("g").attr("class", "arcs") | |
.selectAll("path").data(links.features) | |
.enter().append("path") | |
.attr("class", "arc") | |
.attr("d", path) | |
.attr("opacity", function (d) { | |
return fade_at_edge(d) | |
}); | |
svg.append("g").attr("class", "flyers") | |
.selectAll("path").data(links.features) | |
.enter().append("path") | |
.attr("class", "flyer") | |
.attr("d", function (d) { return swoosh(flying_arc(d)) }) | |
.attr("opacity", function (d) { | |
return fade_at_edge(d) | |
}); | |
} | |
function position_labels() { | |
var centerPos = projection.invert([offsetX, offsetY]); | |
svg | |
.selectAll(".label") | |
.attr("text-anchor", function (d) { | |
var x = projection(d.geometry.coordinates)[0]; | |
return x < offsetX - 20 ? "end" : x < offsetX + 20 ? "middle" : "start"; | |
}) | |
.attr("transform", function (d) { | |
var loc = projection(d.geometry.coordinates), | |
x = loc[0], | |
y = loc[1]; | |
var offset = x < offsetX ? -5 : 5; | |
return "translate(" + (x + offset) + "," + (y - 2) + ")"; | |
}) | |
.style("display", function (d) { | |
var d = d3.geoDistance(d.geometry.coordinates, centerPos); | |
return d > 1.57 ? "none" : "inline"; | |
}); | |
} | |
function flying_arc(pts) { | |
var source = pts.geometry.coordinates[0], | |
target = pts.geometry.coordinates[1]; | |
var mid1 = location_along_arc(source, target, .333); | |
var mid2 = location_along_arc(source, target, .667); | |
var result = [projection(source), | |
skyProjection(mid1), | |
skyProjection(mid2), | |
projection(target)] | |
// console.log(result); | |
return result; | |
} | |
function fade_at_edge(d) { | |
var centerPos = projection.invert([offsetX, offsetY]); | |
start = d.geometry.coordinates[0]; | |
end = d.geometry.coordinates[1]; | |
var start_dist = 1.57 - d3.geoDistance(start, centerPos), | |
end_dist = 1.57 - d3.geoDistance(end, centerPos); | |
var fade = d3.scaleLinear().domain([-.1, 0]).range([0, .1]) | |
var dist = start_dist < end_dist ? start_dist : end_dist; | |
return fade(dist) | |
} | |
function location_along_arc(start, end, loc) { | |
var interpolator = d3.geoInterpolate(start, end); | |
return interpolator(loc) | |
} | |
function dragged() { | |
var o1 = [d3.event.x * sensitivity, -d3.event.y * sensitivity]; | |
o1[1] = | |
o1[1] > maxElevation | |
? maxElevation | |
: o1[1] < -maxElevation | |
? -maxElevation | |
: o1[1]; | |
projection.rotate(o1); | |
skyProjection.rotate(o1); | |
refresh(); | |
} | |
function zoomed() { | |
if (d3.event) { | |
svg.attr("transform", "scale(" + d3.event.transform.k + ")"); | |
} | |
} | |
function refresh() { | |
svg.selectAll(".land").attr("d", path); | |
svg.selectAll(".countries path").attr("d", path); | |
svg.selectAll(".graticule").attr("d", path); | |
refreshLandmarks(); | |
refreshFlyers(); | |
} | |
function refreshLandmarks() { | |
svg.selectAll(".point").attr("d", path); | |
position_labels(); | |
} | |
function refreshFlyers() { | |
svg.selectAll(".arc").attr("d", path) | |
.attr("opacity", function (d) { | |
return fade_at_edge(d) | |
}); | |
svg.selectAll(".flyer") | |
.attr("d", function (d) { return swoosh(flying_arc(d)) }) | |
.attr("opacity", function (d) { | |
return fade_at_edge(d) | |
}); | |
} | |
</script> | |
</body> | |
</html> |
{"type": "FeatureCollection","features": [ | |
{ "type": "Feature", "properties": { "scalerank": 0, "labelrank": 1, "featurecla": "Populated place", "name": "Los Angeles", "nameascii": "Los Angeles", "adm0name": "United States of America", "adm0_a3": "USA", "adm1name": "California", "iso_a2": "US", "note": null, "latitude": 33.989978250199997, "longitude": -118.179980511, "changed": 0.0, "namediff": 0, "diffnote": null, "pop_max": 12500000, "pop_min": 3694820, "pop_other": 142265, "rank_max": 14, "rank_min": 12, "geonameid": 5368361.0, "meganame": "Los Angeles-Long Beach-Santa Ana", "ls_name": "Los Angeles1", "ls_match": 1, "checkme": 0 }, "geometry": { "type": "Point", "coordinates": [ -118.181926369940413, 33.991924108765431 ] } }, | |
{ "type": "Feature", "properties": { "scalerank": 0, "labelrank": 1, "featurecla": "Admin-0 capital", "name": "Washington, D.C.", "nameascii": "Washington, D.C.", "adm0name": "United States of America", "adm0_a3": "USA", "adm1name": "District of Columbia", "iso_a2": "US", "note": null, "latitude": 38.899549376499998, "longitude": -77.009418580800002, "changed": 0.0, "namediff": 0, "diffnote": null, "pop_max": 4338000, "pop_min": 552433, "pop_other": 2175991, "rank_max": 12, "rank_min": 11, "geonameid": 4140963.0, "meganame": "Washington, D.C.", "ls_name": "Washington, D.C.", "ls_match": 1, "checkme": 5 }, "geometry": { "type": "Point", "coordinates": [ -77.011364439437159, 38.901495235087054 ] } }, | |
{ "type": "Feature", "properties": { "scalerank": 0, "labelrank": 1, "featurecla": "Populated place", "name": "New York", "nameascii": "New York", "adm0name": "United States of America", "adm0_a3": "USA", "adm1name": "New York", "iso_a2": "US", "note": null, "latitude": 40.749979064, "longitude": -73.980016928799998, "changed": 0.0, "namediff": 0, "diffnote": null, "pop_max": 19040000, "pop_min": 8008278, "pop_other": 9292603, "rank_max": 14, "rank_min": 13, "geonameid": 5128581.0, "meganame": "New York-Newark", "ls_name": "New York", "ls_match": 1, "checkme": 0 }, "geometry": { "type": "Point", "coordinates": [ -73.981962787406815, 40.75192492259464 ] } }, | |
{ "type": "Feature", "properties": { "scalerank": 0, "labelrank": 2, "featurecla": "Admin-0 capital", "name": "Moscow", "nameascii": "Moscow", "adm0name": "Russia", "adm0_a3": "RUS", "adm1name": "Moskva", "iso_a2": "RU", "note": null, "latitude": 55.7521641226, "longitude": 37.615522825900001, "changed": 0.0, "namediff": 0, "diffnote": null, "pop_max": 10452000, "pop_min": 10452000, "pop_other": 10585385, "rank_max": 14, "rank_min": 14, "geonameid": 524901.0, "meganame": "Moskva", "ls_name": "Moscow", "ls_match": 1, "checkme": 0 }, "geometry": { "type": "Point", "coordinates": [ 37.613576967271399, 55.754109981248178 ] } }, | |
{ "type": "Feature", "properties": { "scalerank": 0, "labelrank": 2, "featurecla": "Admin-0 capital", "name": "Mexico City", "nameascii": "Mexico City", "adm0name": "Mexico", "adm0_a3": "MEX", "adm1name": "Distrito Federal", "iso_a2": "MX", "note": null, "latitude": 19.442442442800001, "longitude": -99.130988201700006, "changed": 0.0, "namediff": 0, "diffnote": null, "pop_max": 19028000, "pop_min": 10811002, "pop_other": 10018444, "rank_max": 14, "rank_min": 14, "geonameid": 3530597.0, "meganame": "Ciudad de México", "ls_name": "Mexico City", "ls_match": 1, "checkme": 0 }, "geometry": { "type": "Point", "coordinates": [ -99.132934060293906, 19.444388301415472 ] } }, | |
{ "type": "Feature", "properties": { "scalerank": 0, "labelrank": 2, "featurecla": "Admin-0 capital alt", "name": "Lagos", "nameascii": "Lagos", "adm0name": "Nigeria", "adm0_a3": "NGA", "adm1name": "Lagos", "iso_a2": "NG", "note": null, "latitude": 6.44326165348, "longitude": 3.39153107121, "changed": 4.0, "namediff": 0, "diffnote": "Location adjusted. Changed scale rank.", "pop_max": 9466000, "pop_min": 1536, "pop_other": 6567892, "rank_max": 13, "rank_min": 3, "geonameid": 735497.0, "meganame": "Lagos", "ls_name": "Lagos", "ls_match": 1, "checkme": 0 }, "geometry": { "type": "Point", "coordinates": [ 3.389585212598433, 6.445207512093191 ] } }, | |
{ "type": "Feature", "properties": { "scalerank": 0, "labelrank": 1, "featurecla": "Admin-0 capital", "name": "Beijing", "nameascii": "Beijing", "adm0name": "China", "adm0_a3": "CHN", "adm1name": "Beijing", "iso_a2": "CN", "note": null, "latitude": 39.928892231299997, "longitude": 116.388285684, "changed": 0.0, "namediff": 0, "diffnote": null, "pop_max": 11106000, "pop_min": 7480601, "pop_other": 9033231, "rank_max": 14, "rank_min": 13, "geonameid": 1816670.0, "meganame": "Beijing", "ls_name": "Beijing", "ls_match": 1, "checkme": 0 }, "geometry": { "type": "Point", "coordinates": [ 116.386339825659434, 39.930838089909059 ] } }, | |
{ "type": "Feature", "properties": { "scalerank": 0, "labelrank": 1, "featurecla": "Admin-0 capital", "name": "Jakarta", "nameascii": "Jakarta", "adm0name": "Indonesia", "adm0_a3": "IDN", "adm1name": "Jakarta Raya", "iso_a2": "ID", "note": null, "latitude": -6.17441770541, "longitude": 106.829437621, "changed": 0.0, "namediff": 0, "diffnote": null, "pop_max": 9125000, "pop_min": 8540121, "pop_other": 9129613, "rank_max": 13, "rank_min": 13, "geonameid": 1642911.0, "meganame": "Jakarta", "ls_name": "Jakarta", "ls_match": 1, "checkme": 0 }, "geometry": { "type": "Point", "coordinates": [ 106.827491762470117, -6.172471846798885 ] } }, | |
{ "type": "Feature", "properties": { "scalerank": 0, "labelrank": 1, "featurecla": "Admin-1 capital", "name": "Shanghai", "nameascii": "Shanghai", "adm0name": "China", "adm0_a3": "CHN", "adm1name": "Shanghai", "iso_a2": "CN", "note": null, "latitude": 31.216452452599999, "longitude": 121.436504678, "changed": 0.0, "namediff": 0, "diffnote": null, "pop_max": 14987000, "pop_min": 14608512, "pop_other": 16803572, "rank_max": 14, "rank_min": 14, "geonameid": 1796236.0, "meganame": "Shanghai", "ls_name": "Shanghai", "ls_match": 1, "checkme": 0 }, "geometry": { "type": "Point", "coordinates": [ 121.434558819820154, 31.218398311228327 ] } }, | |
{ "type": "Feature", "properties": { "scalerank": 0, "labelrank": 2, "featurecla": "Admin-0 capital", "name": "Tokyo", "nameascii": "Tokyo", "adm0name": "Japan", "adm0_a3": "JPN", "adm1name": "Tokyo", "iso_a2": "JP", "note": null, "latitude": 35.685016905799998, "longitude": 139.751407429000011, "changed": 0.0, "namediff": 0, "diffnote": null, "pop_max": 35676000, "pop_min": 8336599, "pop_other": 12945252, "rank_max": 14, "rank_min": 13, "geonameid": 1850147.0, "meganame": "Tokyo", "ls_name": "Tokyo", "ls_match": 1, "checkme": 0 }, "geometry": { "type": "Point", "coordinates": [ 139.749461570544668, 35.686962764371174 ] } }, | |
{ "type": "Feature", "properties": { "scalerank": 0, "labelrank": 1, "featurecla": "Admin-1 capital", "name": "Mumbai", "nameascii": "Mumbai", "adm0name": "India", "adm0_a3": "IND", "adm1name": "Maharashtra", "iso_a2": "IN", "note": null, "latitude": 19.016990375700001, "longitude": 72.856989297400006, "changed": 0.0, "namediff": 0, "diffnote": null, "pop_max": 18978000, "pop_min": 12691836, "pop_other": 12426085, "rank_max": 14, "rank_min": 14, "geonameid": 1275339.0, "meganame": "Mumbai", "ls_name": "Mumbai", "ls_match": 1, "checkme": 0 }, "geometry": { "type": "Point", "coordinates": [ 72.855043438766472, 19.018936234356602 ] } }, | |
{ "type": "Feature", "properties": { "scalerank": 0, "labelrank": 1, "featurecla": "Admin-1 capital", "name": "Kolkata", "nameascii": "Kolkata", "adm0name": "India", "adm0_a3": "IND", "adm1name": "West Bengal", "iso_a2": "IN", "note": null, "latitude": 22.494969298299999, "longitude": 88.324675658100006, "changed": 4.0, "namediff": 1, "diffnote": "Name changed. Changed scale rank.", "pop_max": 14787000, "pop_min": 4631392, "pop_other": 7783716, "rank_max": 14, "rank_min": 12, "geonameid": 1275004.0, "meganame": "Kolkata", "ls_name": "Calcutta", "ls_match": 1, "checkme": 0 }, "geometry": { "type": "Point", "coordinates": [ 88.32272979950551, 22.496915156896421 ] } }, | |
{ "type": "Feature", "properties": { "scalerank": 0, "labelrank": 1, "featurecla": "Populated place", "name": "Rio de Janeiro", "nameascii": "Rio de Janeiro", "adm0name": "Brazil", "adm0_a3": "BRA", "adm1name": "Rio de Janeiro", "iso_a2": "BR", "note": null, "latitude": -22.9250231742, "longitude": -43.225020794199999, "changed": 0.0, "namediff": 0, "diffnote": null, "pop_max": 11748000, "pop_min": 2010175, "pop_other": 1821489, "rank_max": 14, "rank_min": 12, "geonameid": 3451190.0, "meganame": "Rio de Janeiro", "ls_name": "Rio de Janeiro", "ls_match": 1, "checkme": 0 }, "geometry": { "type": "Point", "coordinates": [ -43.226966652843657, -22.923077315615956 ] } }, | |
{ "type": "Feature", "properties": { "scalerank": 0, "labelrank": 1, "featurecla": "Admin-1 capital", "name": "Sao Paulo", "nameascii": "Sao Paulo", "adm0name": "Brazil", "adm0_a3": "BRA", "adm1name": "São Paulo", "iso_a2": "BR", "note": null, "latitude": -23.558679587, "longitude": -46.625019980399998, "changed": 0.0, "namediff": 0, "diffnote": null, "pop_max": 18845000, "pop_min": 10021295, "pop_other": 11522944, "rank_max": 14, "rank_min": 14, "geonameid": 3448439.0, "meganame": "São Paulo", "ls_name": "Sao Paolo", "ls_match": 1, "checkme": 0 }, "geometry": { "type": "Point", "coordinates": [ -46.626965839055231, -23.556733728378958 ] } }, | |
{ "type": "Feature", "properties": { "scalerank": 0, "labelrank": 0, "featurecla": "Admin-0 capital", "name": "Singapore", "nameascii": "Singapore", "adm0name": "Singapore", "adm0_a3": "SGP", "adm1name": null, "iso_a2": "SG", "note": null, "latitude": 1.29303346649, "longitude": 103.855820678, "changed": 0.0, "namediff": 0, "diffnote": null, "pop_max": 5183700, "pop_min": 3289529, "pop_other": 3314179, "rank_max": 13, "rank_min": 12, "geonameid": 1880252.0, "meganame": "Singapore", "ls_name": "Singapore", "ls_match": 1, "checkme": 5 }, "geometry": { "type": "Point", "coordinates": [ 103.853874819099019, 1.294979325105942 ] } }, | |
{ "type": "Feature", "properties": { "scalerank": 0, "labelrank": 0, "featurecla": "Admin-0 region capital", "name": "Hong Kong", "nameascii": "Hong Kong", "adm0name": "Hong Kong S.A.R.", "adm0_a3": "HKG", "adm1name": null, "iso_a2": "HK", "note": null, "latitude": 22.304980895, "longitude": 114.185009317, "changed": 0.0, "namediff": 0, "diffnote": null, "pop_max": 7206000, "pop_min": 4551579, "pop_other": 4549026, "rank_max": 13, "rank_min": 12, "geonameid": 1819729.0, "meganame": "Hong Kong", "ls_name": "Hong Kong", "ls_match": 1, "checkme": 0 }, "geometry": { "type": "Point", "coordinates": [ 114.183063458463039, 22.30692675357551 ] } } | |
]} |