Whenever we try to represent our 3D earth on a 2D map we necessarily introduce distortion. This tool attempts to visualize the phenomenon.
Original prompt by @curran
Bounding box solution by @tyrasd
Whenever we try to represent our 3D earth on a 2D map we necessarily introduce distortion. This tool attempts to visualize the phenomenon.
Original prompt by @curran
Bounding box solution by @tyrasd
| <!DOCTYPE html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/d3-geo-projection/0.2.9/d3.geo.projection.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.19/topojson.min.js"></script> | |
| <style> | |
| svg { | |
| margin: 22px; | |
| } | |
| select { | |
| margin-left: 20px; | |
| } | |
| path.foreground { | |
| fill: none; | |
| stroke: #333; | |
| stroke-width: 1.5px; | |
| } | |
| path.graticule { | |
| fill: none; | |
| stroke: #aaa; | |
| stroke-width: .5px; | |
| } | |
| #left { | |
| cursor: move; | |
| } | |
| #left .land { | |
| fill: #d7c7ad; | |
| stroke: #a5967e; | |
| } | |
| #right .land { | |
| fill: #cfcece; | |
| stroke: #a5967e; | |
| } | |
| #left circle { | |
| fill: #d8355e; | |
| } | |
| #right circle { | |
| stroke: #d8355e; | |
| fill: none; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <svg id="left"></svg> | |
| <svg id="right"></svg> | |
| <select></select> | |
| <script> | |
| var map_width = 400; | |
| var map_height = 400; | |
| var center = [-90, 37]; | |
| var scale0 = (map_width - 1) / 2 / Math.PI * 6 | |
| var scale1 = (map_width - 1) / 2 / Math.PI * 3 | |
| var zoom = d3.behavior.zoom() | |
| .translate([map_width / 2, map_height / 2]) | |
| .scale(scale0) | |
| .scaleExtent([scale0, 8 * scale0]) | |
| .on("zoom", zoomed) | |
| var projectionLeft = d3.geo.aitoff() | |
| .center(center) | |
| var projectionRight = d3.geo.orthographic() | |
| .center(center) | |
| .translate([map_width/5, map_height / 5]) | |
| .scale(scale1) | |
| .clipAngle(90) | |
| var pathLeft = d3.geo.path() | |
| .projection(projectionLeft); | |
| var pathRight = d3.geo.path() | |
| .projection(projectionRight); | |
| function zoomed() { | |
| projectionLeft | |
| .translate(zoom.translate()) | |
| .scale(zoom.scale()) | |
| var newCenter = projectionLeft.invert([map_width/2,map_height/2]); | |
| projectionRight | |
| .rotate([-newCenter[0], -newCenter[1]]) | |
| update(); | |
| } | |
| function update() { | |
| d3.selectAll("#left path") | |
| .attr("d", pathLeft); | |
| d3.selectAll("#right path") | |
| .attr("d", pathRight); | |
| d3.selectAll("#left circle") | |
| .attr({ | |
| cx: function(d,i) { return d.x }, | |
| cy: function(d,i) { return d.y } | |
| }) | |
| d3.selectAll("#right circle") | |
| .attr({ | |
| cx: function(d,i) { | |
| var latlon = projectionLeft.invert([d.x, d.y]) | |
| return projectionRight(latlon)[0] | |
| }, | |
| cy: function(d,i) { | |
| var latlon = projectionLeft.invert([d.x, d.y]) | |
| return projectionRight(latlon)[1] | |
| } | |
| }) | |
| } | |
| var graticule = d3.geo.graticule(); | |
| var svgLeft = d3.select("#left") | |
| .attr("width", map_width) | |
| .attr("height", map_height); | |
| var svgRight = d3.select("#right") | |
| .attr("width", map_width + 40) | |
| .attr("height", map_height); | |
| svgLeft | |
| .call(zoom) | |
| .call(zoom.event); | |
| svgLeft.append("path") | |
| .datum(graticule) | |
| .attr("class", "graticule") | |
| .attr("d", pathLeft); | |
| svgRight.append("path") | |
| .datum(graticule) | |
| .attr("class", "graticule") | |
| .attr("d", pathRight); | |
| d3.json("world-110m.json", function(error,world) { | |
| if (error) throw error; | |
| svgLeft.insert("path", ".graticule") | |
| .datum(topojson.feature(world, world.objects.land)) | |
| .attr("class", "land") | |
| .attr("d", pathLeft); | |
| svgRight.insert("path", ".graticule") | |
| .datum(topojson.feature(world, world.objects.land)) | |
| .attr("class", "land") | |
| .attr("d", pathRight); | |
| var points = generateRect(100, 25, 25, map_width - 50, map_height - 50); | |
| svgLeft.selectAll("circle") | |
| .data(points) | |
| .enter().append("circle") | |
| .attr({ | |
| r: 3 | |
| }) | |
| svgRight.selectAll("circle") | |
| .data(points) | |
| .enter().append("circle") | |
| .attr({ | |
| r: 2 | |
| }) | |
| zoomed(); | |
| }); | |
| var projections = { | |
| "Aitoff": d3.geo.aitoff().scale(90), | |
| "Boggs Eumorphic": d3.geo.boggs().scale(90), | |
| "Craster Parabolic (Putnins P4)": d3.geo.craster().scale(90), | |
| "Cylindrical Equal-Area": d3.geo.cylindricalEqualArea().scale(120), | |
| "Eckert I": d3.geo.eckert1().scale(95), | |
| "Eckert III": d3.geo.eckert3().scale(105), | |
| "Eckert IV": d3.geo.eckert4().scale(105), | |
| "Eckert V": d3.geo.eckert5().scale(100), | |
| "Equidistant Cylindrical (Plate Carrée)": d3.geo.equirectangular().scale(90), | |
| "Fahey": d3.geo.fahey().scale(75), | |
| "Foucaut Sinusoidal": d3.geo.foucaut().scale(80), | |
| "Gall (Gall Stereographic)": d3.geo.cylindricalStereographic().scale(70), | |
| "Ginzburg VIII (TsNIIGAiK 1944)": d3.geo.ginzburg8().scale(75), | |
| "Kavraisky VII": d3.geo.kavrayskiy7().scale(90), | |
| "Larrivée": d3.geo.larrivee().scale(55), | |
| "McBryde-Thomas Flat-Pole Sine (No. 2)": d3.geo.mtFlatPolarSinusoidal().scale(95), | |
| "Mercator": d3.geo.mercator().scale(50), | |
| "Miller Cylindrical I": d3.geo.miller().scale(60), | |
| "Mollweide": d3.geo.mollweide().scale(100), | |
| "Natural Earth": d3.geo.naturalEarth().scale(100), | |
| "Nell-Hammer": d3.geo.nellHammer().scale(120), | |
| "Quartic Authalic": d3.geo.hammer().coefficient(Infinity).scale(95), | |
| "Robinson": d3.geo.robinson().scale(90), | |
| "Sinusoidal": d3.geo.sinusoidal().scale(90), | |
| "van der Grinten (I)": d3.geo.vanDerGrinten().scale(50), | |
| "Wagner VI": d3.geo.wagner6().scale(90), | |
| "Wagner VII": d3.geo.wagner7().scale(90), | |
| "Winkel Tripel": d3.geo.winkel3().scale(90), | |
| "Wiechel": d3.geo.wiechel().scale(90) | |
| }; | |
| var selector = d3.select("select") | |
| selector.selectAll("option") | |
| .data(Object.keys(projections)) | |
| .enter().append("option") | |
| .attr({ | |
| value: function(d) { return d } | |
| }).text(function(d) { return d }) | |
| selector.on("change", function(d) { | |
| console.log("sup", d3.event) | |
| var proj = d3.event.target.selectedOptions[0].value; | |
| projectionLeft = projections[proj].center(center); | |
| pathLeft = d3.geo.path() | |
| .projection(projectionLeft); | |
| zoomed(); | |
| }) | |
| function generateRect(num, x, y, width, height) { | |
| var points = [] | |
| var sideNum = Math.floor(num/4) + 1; | |
| // top | |
| d3.range(sideNum).forEach(function(i) { | |
| points.push({ x: x + i * width/sideNum, y: y }) | |
| }) | |
| // right | |
| d3.range(sideNum).forEach(function(i) { | |
| points.push({ x: x + width, y: y + i * height/sideNum }) | |
| }) | |
| // bottom | |
| d3.range(sideNum).forEach(function(i) { | |
| points.push({ x: x + width - i * width/sideNum, y: y + height }) | |
| }) | |
| // left | |
| d3.range(sideNum).forEach(function(i) { | |
| points.push({ x: x, y: y + height - i * height/sideNum }) | |
| }) | |
| return points; | |
| } | |
| </script> | |
| </body> | |
| <script> | |
| (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ | |
| (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), | |
| m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) | |
| })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); | |
| ga('create', 'UA-67666917-1', 'auto'); | |
| ga('send', 'pageview'); | |
| </script> |