Made using Jason Davis' zomable/rotatable world globe with orthographic projection. He described the drag behavior here: https://www.jasondavies.com/maps/rotate/
Last active
November 17, 2017 20:35
-
-
Save jamieeeeeeeee/b01fe3ce35cd8f3f0396b54b772cb166 to your computer and use it in GitHub Desktop.
Rotatable globe showing the world's most populous cities, made with D3
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
| // Copyright (c) 2013, Jason Davies, http://www.jasondavies.com | |
| // See LICENSE.txt for details. | |
| (function() { | |
| var radians = Math.PI / 180, | |
| degrees = 180 / Math.PI; | |
| // TODO make incremental rotate optional | |
| d3.geo.zoom = function() { | |
| var projection, | |
| zoomPoint, | |
| event = d3.dispatch("zoomstart", "zoom", "zoomend"), | |
| zoom = d3.behavior.zoom() | |
| .on("zoomstart", function() { | |
| var mouse0 = d3.mouse(this), | |
| rotate = quaternionFromEuler(projection.rotate()), | |
| point = position(projection, mouse0); | |
| if (point) zoomPoint = point; | |
| zoomOn.call(zoom, "zoom", function() { | |
| projection.scale(d3.event.scale); | |
| var mouse1 = d3.mouse(this), | |
| between = rotateBetween(zoomPoint, position(projection, mouse1)); | |
| projection.rotate(eulerFromQuaternion(rotate = between | |
| ? multiply(rotate, between) | |
| : multiply(bank(projection, mouse0, mouse1), rotate))); | |
| mouse0 = mouse1; | |
| event.zoom.apply(this, arguments); | |
| }); | |
| event.zoomstart.apply(this, arguments); | |
| }) | |
| .on("zoomend", function() { | |
| zoomOn.call(zoom, "zoom", null); | |
| event.zoomend.apply(this, arguments); | |
| }), | |
| zoomOn = zoom.on; | |
| zoom.projection = function(_) { | |
| return arguments.length ? zoom.scale((projection = _).scale()) : projection; | |
| }; | |
| return d3.rebind(zoom, event, "on"); | |
| }; | |
| function bank(projection, p0, p1) { | |
| var t = projection.translate(), | |
| angle = Math.atan2(p0[1] - t[1], p0[0] - t[0]) - Math.atan2(p1[1] - t[1], p1[0] - t[0]); | |
| return [Math.cos(angle / 2), 0, 0, Math.sin(angle / 2)]; | |
| } | |
| function position(projection, point) { | |
| var t = projection.translate(), | |
| spherical = projection.invert(point); | |
| return spherical && isFinite(spherical[0]) && isFinite(spherical[1]) && cartesian(spherical); | |
| } | |
| function quaternionFromEuler(euler) { | |
| var λ = .5 * euler[0] * radians, | |
| φ = .5 * euler[1] * radians, | |
| γ = .5 * euler[2] * radians, | |
| sinλ = Math.sin(λ), cosλ = Math.cos(λ), | |
| sinφ = Math.sin(φ), cosφ = Math.cos(φ), | |
| sinγ = Math.sin(γ), cosγ = Math.cos(γ); | |
| return [ | |
| cosλ * cosφ * cosγ + sinλ * sinφ * sinγ, | |
| sinλ * cosφ * cosγ - cosλ * sinφ * sinγ, | |
| cosλ * sinφ * cosγ + sinλ * cosφ * sinγ, | |
| cosλ * cosφ * sinγ - sinλ * sinφ * cosγ | |
| ]; | |
| } | |
| function multiply(a, b) { | |
| var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], | |
| b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3]; | |
| return [ | |
| a0 * b0 - a1 * b1 - a2 * b2 - a3 * b3, | |
| a0 * b1 + a1 * b0 + a2 * b3 - a3 * b2, | |
| a0 * b2 - a1 * b3 + a2 * b0 + a3 * b1, | |
| a0 * b3 + a1 * b2 - a2 * b1 + a3 * b0 | |
| ]; | |
| } | |
| function rotateBetween(a, b) { | |
| if (!a || !b) return; | |
| var axis = cross(a, b), | |
| norm = Math.sqrt(dot(axis, axis)), | |
| halfγ = .5 * Math.acos(Math.max(-1, Math.min(1, dot(a, b)))), | |
| k = Math.sin(halfγ) / norm; | |
| return norm && [Math.cos(halfγ), axis[2] * k, -axis[1] * k, axis[0] * k]; | |
| } | |
| function eulerFromQuaternion(q) { | |
| return [ | |
| Math.atan2(2 * (q[0] * q[1] + q[2] * q[3]), 1 - 2 * (q[1] * q[1] + q[2] * q[2])) * degrees, | |
| Math.asin(Math.max(-1, Math.min(1, 2 * (q[0] * q[2] - q[3] * q[1])))) * degrees, | |
| Math.atan2(2 * (q[0] * q[3] + q[1] * q[2]), 1 - 2 * (q[2] * q[2] + q[3] * q[3])) * degrees | |
| ]; | |
| } | |
| function cartesian(spherical) { | |
| var λ = spherical[0] * radians, | |
| φ = spherical[1] * radians, | |
| cosφ = Math.cos(φ); | |
| return [ | |
| cosφ * Math.cos(λ), | |
| cosφ * Math.sin(λ), | |
| Math.sin(φ) | |
| ]; | |
| } | |
| function dot(a, b) { | |
| for (var i = 0, n = a.length, s = 0; i < n; ++i) s += a[i] * b[i]; | |
| return s; | |
| } | |
| function cross(a, b) { | |
| return [ | |
| a[1] * b[2] - a[2] * b[1], | |
| a[2] * b[0] - a[0] * b[2], | |
| a[0] * b[1] - a[1] * b[0] | |
| ]; | |
| } | |
| })(); |
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> | |
| <meta charset="utf-8"> | |
| <style> | |
| #globe{ | |
| background: #fcfcfa; | |
| width: 900px; | |
| height: 500px; | |
| margin-left: 50px; | |
| } | |
| .stroke { | |
| fill: none; | |
| stroke: #000; | |
| stroke-width: 3px; | |
| } | |
| .fill { | |
| fill: #fff; | |
| } | |
| .graticule { | |
| fill: none; | |
| stroke: #777; | |
| stroke-width: .5px; | |
| stroke-opacity: .5; | |
| } | |
| .land { | |
| fill: #344729; | |
| } | |
| .boundary { | |
| fill: none; | |
| stroke: #fff; | |
| stroke-width: .5px; | |
| } | |
| .overlay { | |
| fill: none; | |
| pointer-events: all; | |
| } | |
| .city{ | |
| fill: #D17846; | |
| } | |
| h1{ | |
| font-size: 3em; | |
| font-family: 'Playfair Display', serif; | |
| color: #252525; | |
| padding-left: 10px; | |
| margin: 5px; | |
| } | |
| h2 { | |
| font-size: 1.5em; | |
| font-family: 'Droid Sans Mono', Arial, serif; | |
| font-weight: 400; | |
| color: #525252; | |
| padding-left: 20px; | |
| } | |
| </style> | |
| <body> | |
| <script src="http://d3js.org/d3.v3.min.js"></script> | |
| <script src="http://d3js.org/topojson.v1.min.js"></script> | |
| <script src="d3.geo.zoom.js"></script> | |
| <script src="//d3js.org/queue.v1.min.js"></script> | |
| <link href='https://fonts.googleapis.com/css?family=Playfair+Display:400,900' rel='stylesheet' type='text/css'> | |
| <link href='https://fonts.googleapis.com/css?family=Droid+Sans+Mono' rel='stylesheet' type='text/css'> | |
| <div id="header"> | |
| <h1>The Most Populous Cities in the World<h1> | |
| <h2> Data from Natural Earth</h2> | |
| </div> | |
| <div id="globe"></div> | |
| <script> | |
| var width = 680, | |
| height = 680; | |
| // Map configuration | |
| var rScale = d3.scale.sqrt(); | |
| var peoplePerPixel = 50000; | |
| var max_population = []; | |
| var projection = d3.geo.orthographic() | |
| .scale(270) | |
| .translate([width / 2, height / 2]) | |
| .clipAngle(90) | |
| .precision(.1); | |
| var zoom = d3.behavior.zoom() | |
| .scaleExtent([1,6]) | |
| .on("zoom",zoomed); | |
| var zoomEnhanced = d3.geo.zoom().projection(projection) | |
| .on("zoom",zoomedEnhanced); | |
| var drag = d3.behavior.drag() | |
| .origin(function() { var r = projection.rotate(); return {x: r[0], y: -r[1]}; }) | |
| .on("drag", dragged) | |
| .on("dragstart", dragstarted) | |
| .on("dragend", dragended); | |
| var path = d3.geo.path() | |
| .projection(projection); | |
| var graticule = d3.geo.graticule(); | |
| var svg = d3.select("#globe").append("svg") | |
| .attr("width", width) | |
| .attr("height", height); | |
| var pathG = svg.append("g"); | |
| svg.append("rect") | |
| .attr("class", "overlay") | |
| .attr("width", width) | |
| .attr("height", height) | |
| .call(zoomEnhanced) | |
| pathG.append("defs").append("path") | |
| .datum({type: "Sphere"}) | |
| .attr("id", "sphere") | |
| .attr("d", path); | |
| pathG.append("use") | |
| .attr("class", "stroke") | |
| .attr("xlink:href", "#sphere"); | |
| pathG.append("use") | |
| .attr("class", "fill") | |
| .attr("xlink:href", "#sphere"); | |
| pathG.append("path") | |
| .datum(graticule) | |
| .attr("class", "graticule") | |
| .attr("d", path); | |
| d3.json("worldTopo-with-places-simplified.json", function(error, world) { | |
| // to render meridians/graticules on top of lands, use insert which adds new path before graticule in the selection | |
| pathG.insert("path", ".graticule") | |
| .datum(topojson.feature(world, world.objects.world_subunits)) | |
| .attr("class", "land") | |
| .attr("d", path) | |
| //this line of code draw countries on to the map | |
| pathG.insert("path", ".graticule") | |
| .datum(topojson.mesh(world, world.objects.world_subunits, function(a, b) { return a !== b; })) | |
| .attr("class", "boundary") | |
| .attr("d", path); | |
| }); | |
| //FUCKING WORKING | |
| d3.json("world-places.json", function(error, world_places) { | |
| if (error) return console.error(error); | |
| data = world_places.features; | |
| // setting the circle size (not radius!) according to the number of inhabitants per city | |
| population_array = []; | |
| for (i = 0; i < world_places.features.length; i++) { | |
| if (world_places.features[i].properties ) { | |
| population_array.push(world_places.features[i].properties.POP_MAX); | |
| } else { | |
| population_array.push(1); | |
| } | |
| } | |
| max_population = population_array.sort(d3.descending)[0] | |
| var rMin = 0; | |
| var rMax = Math.sqrt(max_population / (peoplePerPixel * Math.PI)); | |
| rScale.domain([0, max_population]); | |
| rScale.range([rMin, rMax]); | |
| path.pointRadius(function(d) { | |
| return d.properties ? rScale(d.properties.POP_MAX) : 1; | |
| //console.log(rScale(d.properties.POP_MAX)); | |
| }); | |
| console.log("geo"); | |
| console.log(data[0].properties.POP_MAX); | |
| pathG.selectAll("path.city") | |
| .data(data) | |
| .enter().append("path") | |
| .attr("class", "city") | |
| .attr("d", path) | |
| .attr("fill-opacity", 0.7); | |
| }); | |
| // apply transformations to map and all elements on it | |
| function zoomed() | |
| { | |
| pathG.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")"); | |
| //grids.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")"); | |
| //geofeatures.select("path.graticule").style("stroke-width", 0.5 / d3.event.scale); | |
| pathG.selectAll("path.boundary").style("stroke-width", 0.5 / d3.event.scale); | |
| } | |
| function zoomedEnhanced() | |
| { | |
| pathG.selectAll("path").attr("d",path); | |
| //pathG.selectAll("circle").attr("cx"); | |
| //pathG.selectAll("circle").attr("cy"); | |
| } | |
| function dragstarted(d) | |
| { | |
| //stopPropagation prevents dragging to "bubble up" which triggers same event for all elements below this object | |
| d3.event.sourceEvent.stopPropagation(); | |
| d3.select(this).classed("dragging", true); | |
| } | |
| function dragged() { | |
| projection.rotate([d3.event.x, -d3.event.y]); | |
| pathG.selectAll("path").attr("d", path); | |
| //pathG.selectAll("circle").attr("cx"); | |
| //pathG.selectAll("circle").attr("cy"); | |
| } | |
| function dragended(d) | |
| { | |
| d3.select(this).classed("dragging", false); | |
| } | |
| d3.select(self.frameElement).style("height", height + "px"); | |
| </script> |
View raw
(Sorry about that, but we can’t show files that are this big right now.)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
