Last active
April 28, 2023 11:07
-
-
Save rajgoel/000f227d1f9c79b6f8a4225e8c9a49ba to your computer and use it in GitHub Desktop.
d3js - rotating responsive globe with day/night
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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