Built with blockbuilder.org
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0;background: #000; }
</style>
</head>
<body>
<script>
var width = 1200,
height = 700,
originalScale = 300,
color = d3.scaleLinear()
.range(['#011002', '#010210']),
scale = originalScale,
scaleChange,
rotation;
var sphere = {type: 'Sphere'};
var graticule = d3.geoGraticule();
// set up the main canvas and the projection
var canvas = d3.select('body').append('canvas')
.attr('width', width)
.attr('height', height);
var context = canvas.node().getContext('2d');
context.imageSmoothingEnabled = true;
context.globalCompositeOperation = 'lighter'; // blend overlapping colors towards white (vs default source-over)
context.translate(width / 2, height / 2);
var projection = d3.geoOrthographic()
.scale(scale)
.rotate([-182, -23])
.translate([0, 0])
.clipAngle(180);
var path = d3.geoPath()
.projection(projection)
.context(context);
d3.queue()
.defer(d3.csv, 'routes.csv')
.defer(d3.json, 'world-110m-simple.json')
.await(load);
function load(error, routes, world) {
if (error) { console.log(error); }
var land = topojson.feature(world, world.objects.countries),
grid = graticule();
function drawWorld() {
context.clearRect(-width/2, -height/2, width, height);
// draw grid
context.save();
context.lineWidth = 0.5;
context.strokeStyle = '#202020';
context.beginPath();
path(sphere);
context.stroke();
context.beginPath();
path(grid);
context.stroke();
// draw land
context.beginPath();
path(land);
context.fillStyle = '#202020';
context.fill();
context.beginPath();
path(land);
context.lineWidth = .5;
context.strokeStyle = '#000000';
context.stroke();
const c = projection.invert([0, 0]),
r = projection.scale();
// draw routes
routes.slice(0,1000).forEach(d => {
const origin = [+d.olon, +d.olat],
destination = [+d.dlon, +d.dlat],
route = d3.geoInterpolate(origin, destination),
frac = d3.geoDistance(origin, destination)/Math.PI;
context.beginPath();
context.lineWidth = 0.5;
steps = Math.ceil(20*frac+1);
context.moveTo(...projection(route(0)));
d3.range(0, 1, 1/steps).forEach(t => {
context.strokeStyle = color(t);
context.lineTo(...projection(route(t)))
context.stroke();
});
});
context.restore();
} // drawWorld()
// First draw
requestAnimationFrame(drawWorld);
var zoom = d3.zoom()
.scaleExtent([0.5, 4])
.on("zoom", zoomed)
canvas.call(zoom);
var previousScaleFactor = 1;
function zoomed() {
var dx = d3.event.sourceEvent.movementX;
var dy = d3.event.sourceEvent.movementY;
var event = d3.event.sourceEvent.type;
context.save();
context.clearRect(0, 0, width, height);
if (event === 'wheel') {
scaleFactor = d3.event.transform.k;
scaleChange = scaleFactor - previousScaleFactor;
scale = scale + scaleChange * originalScale;
projection.scale(scale);
previousScaleFactor = scaleFactor;
} else {
var r = projection.rotate();
rotation = [r[0] + dx * 0.4, r[1] - dy * 0.5, r[2]];
projection.rotate(rotation);
}
requestAnimationFrame(drawWorld);
context.restore();
} // zoomed()
} // load()
</script>
</body>