Skip to content

Instantly share code, notes, and snippets.

@rajgoel
Last active April 28, 2023 11:07
Show Gist options
  • Select an option

  • Save rajgoel/000f227d1f9c79b6f8a4225e8c9a49ba to your computer and use it in GitHub Desktop.

Select an option

Save rajgoel/000f227d1f9c79b6f8a4225e8c9a49ba to your computer and use it in GitHub Desktop.
d3js - rotating responsive globe with day/night
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Rotating Globe</title>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://d3js.org/d3-request.v1.min.js"></script>
<script src="https://d3js.org/d3-queue.v3.min.js"></script>
<script src="https://d3js.org/topojson.v3.min.js"></script>
<script src="globe.js"></script>
</head>
<body>
<div id="globe"></div>
<script>
globe(document.getElementById("globe"));
</script>
</body>
</html>
// 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
// https://gist.github.com/lunarmoon26/09d4d0ef25fd32ed663db969f5bc79fe
// https://gist.github.com/mbostock/4597134
let globe = function(container) {
container.setAttribute('data-prevent-swipe', 'true');
container.innerHTML +=
`<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>
<filter id="twilight" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="12" />
</filter>
</defs>
</svg>`;
var node = container.querySelector("svg");
var style = document.createElement('style');
var css =
`.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, .3);
fill-opacity: .15;
fill: white;
}
.countries .focused {
stroke: rgba(0, 0, 0, .3);
fill-opacity: .3;
fill: white;
}
.graticule {
fill: none;
stroke: black;
stroke-width: .5;
opacity: .2;
}
.night {
stroke: none;
fill: black;
fill-opacity: .5;
}`;
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
node.appendChild(style);
var timer = undefined,
time = Date.now(),
rotation,
rotate = [10, -1],
velocity = [0.01, 0];
var π = Math.PI,
radians = π / 180,
degrees = 180 / π;
var nightCircle = d3.geoCircle(),
solarTime = utc(new Date),
solarAntipode = antipode(solarPosition(solarTime));
setInterval(function() {
solarTime = utc(new Date);
solarAntipode = antipode(solarPosition(solarTime));
svg.selectAll(".night")
.datum(nightCircle.center(solarAntipode)).attr("d", path);
}, 1000);
var width = 800,
height = 800,
radius = 400,
offsetX = width / 2,
offsetY = height / 2,
initRotation = [0, -30],
flyerAltitude = 80;
var projection = d3
.geoOrthographic()
.scale(radius)
.rotate(initRotation)
.translate([offsetX, offsetY])
.clipAngle(90);
var path = d3
.geoPath()
.projection(projection)
.pointRadius(1.5);
var graticule = d3.geoGraticule();
//Drag
var lambda = d3
.scaleLinear()
.domain([0, width])
.range([-180, 180]);
var phi = d3
.scaleLinear()
.domain([0, height])
.range([90, -90]);
var svg = d3
.select(node)
.attr("width", width)
.attr("height", height)
.attr("transform-origin", offsetX + "px " + offsetY + "px")
.call(
d3
.drag()
.subject(function() {
var r = projection.rotate();
return {
x: lambda.invert(r[0]),
y: phi.invert(r[1])
};
})
.on("drag", function() {
stopRotation();
projection.rotate([lambda(d3.event.x), phi(d3.event.y)]);
refresh();
})
.on("end", function() {
startRotation();
})
)
d3.queue()
.defer(d3.json, "https://raw.githubusercontent.com/d3/d3.github.com/master/world-110m.v1.json")
.await(ready);
function ready(error, world) {
svg
.append("circle")
.attr("cx", offsetX)
.attr("cy", offsetY)
.attr("r", projection.scale())
.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")
.attr("d", path);
svg
.append("circle")
.attr("cx", offsetX)
.attr("cy", offsetY)
.attr("r", projection.scale())
.style("fill", "url(#globe_highlight)");
svg
.append("circle")
.attr("cx", offsetX)
.attr("cy", offsetY)
.attr("r", projection.scale())
.style("fill", "url(#globe_shading)");
svg
.append("g")
.attr("class", "countries")
.selectAll("path")
.data(topojson.feature(world, world.objects.countries).features)
.enter()
.append("path")
.attr("d", path)
.on("click", function(feature) {
focus(this, feature);
});
var center = nightCircle.center(solarAntipode);
svg.append("path")
.datum(center)
.attr("pointer-events", "none")
.attr("filter", "url(#twilight)")
.attr("class", "night");
timer = d3.timer(function() {
var dt = Date.now() - time;
projection.rotate([rotate[0] + velocity[0] * dt, 0]);
refresh();
});
}
function getCentroid(geometry) {
if (geometry.type == "Polygon") {
return d3.geoPath().centroid(geometry);
}
// fix for strange calcluation of MultiPolygon (Russia)
var coordinates = [0, 0],
n = 0;
for (var j = 0; j < geometry.coordinates.length; j++) {
for (var i = 0; i < geometry.coordinates[j][0].length; i++) {
coordinates[0] += geometry.coordinates[j][0][i][0];
coordinates[1] += geometry.coordinates[j][0][i][1];
}
n += geometry.coordinates[j][0].length;
}
coordinates[0] /= n;
coordinates[1] /= n;
return coordinates;
}
function focus(element, feature) {
if (element.classList.contains("focused")) {
element.classList.remove("focused")
startRotation();
} else {
stopRotation();
var centroid = getCentroid(feature.geometry);
(function transition() {
d3.transition()
.duration(1500)
.tween("rotate", function() {
var r = d3.interpolate(projection.rotate(), [-centroid[0], -centroid[1]]);
return function(t) {
projection.rotate(r(t));
svg.selectAll("path").attr("d", path)
.classed("focused", function(d, i) {
return d && d.id == feature.id ? focused = d : false;
});
};
})
})();
}
}
function stopRotation() {
timer.stop();
}
function startRotation() {
var r = projection.rotate()[0];
var k = projection.rotate()[1];
time = Date.now();
timer.restart(function() {
var dt = Date.now() - time;
projection.rotate([r + velocity[0] * dt, k]);
refresh();
});
}
function refresh() {
svg.selectAll(".land").attr("d", path);
svg.selectAll(".countries path").attr("d", path);
svg.selectAll(".graticule").attr("d", path);
svg.selectAll(".night").attr("d", path);
}
function utc(time) {
var utcTime = Date.UTC(
time.getUTCFullYear(),
time.getUTCMonth(),
time.getUTCDate(),
time.getUTCHours(),
time.getUTCMinutes(),
time.getUTCSeconds()
);
return new Date(utcTime);
}
function antipode(position) {
return [position[0] + 180, -position[1]];
}
function solarPosition(time) {
var timeOfDay = time.getUTCHours() * 3600 + time.getUTCMinutes() * 60 + time.getUTCSeconds(),
longitude = 180 - timeOfDay / 86400 * 360,
centuries = (time - Date.UTC(2000, 0, 1, 12)) / 864e5 / 36525; // since J2000
return [
longitude - equationOfTime(centuries) * degrees,
solarDeclination(centuries) * degrees
];
}
// Equations based on NOAA’s Solar Calculator; all angles in radians.
// http://www.esrl.noaa.gov/gmd/grad/solcalc/
function equationOfTime(centuries) {
var e = eccentricityEarthOrbit(centuries),
m = solarGeometricMeanAnomaly(centuries),
l = solarGeometricMeanLongitude(centuries),
y = Math.tan(obliquityCorrection(centuries) / 2);
y *= y;
return y * Math.sin(2 * l) -
2 * e * Math.sin(m) +
4 * e * y * Math.sin(m) * Math.cos(2 * l) -
0.5 * y * y * Math.sin(4 * l) -
1.25 * e * e * Math.sin(2 * m);
}
function solarDeclination(centuries) {
return Math.asin(Math.sin(obliquityCorrection(centuries)) * Math.sin(solarApparentLongitude(centuries)));
}
function solarApparentLongitude(centuries) {
return solarTrueLongitude(centuries) - (0.00569 + 0.00478 * Math.sin((125.04 - 1934.136 * centuries) * radians)) * radians;
}
function solarTrueLongitude(centuries) {
return solarGeometricMeanLongitude(centuries) + solarEquationOfCenter(centuries);
}
function solarGeometricMeanAnomaly(centuries) {
return (357.52911 + centuries * (35999.05029 - 0.0001537 * centuries)) * radians;
}
function solarGeometricMeanLongitude(centuries) {
var l = (280.46646 + centuries * (36000.76983 + centuries * 0.0003032)) % 360;
return (l < 0 ? l + 360 : l) / 180 * π;
}
function solarEquationOfCenter(centuries) {
var m = solarGeometricMeanAnomaly(centuries);
return (Math.sin(m) * (1.914602 - centuries * (0.004817 + 0.000014 * centuries)) +
Math.sin(m + m) * (0.019993 - 0.000101 * centuries) +
Math.sin(m + m + m) * 0.000289) * radians;
}
function obliquityCorrection(centuries) {
return meanObliquityOfEcliptic(centuries) + 0.00256 * Math.cos((125.04 - 1934.136 * centuries) * radians) * radians;
}
function meanObliquityOfEcliptic(centuries) {
return (23 + (26 + (21.448 - centuries * (46.8150 + centuries * (0.00059 - centuries * 0.001813))) / 60) / 60) * radians;
}
function eccentricityEarthOrbit(centuries) {
return 0.016708634 - centuries * (0.000042037 + 0.0000001267 * centuries);
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment