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