Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
D3 V5 - Faux-3d Shaded Globe With Zoom, Places and Arcs

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>
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment