Whenever we try to represent our 3D earth on a 2D map we necessarily introduce distortion. This tool attempts to visualize the phenomenon. It's a fun riff on my earlier attempt.
The commonly used Mercator projection is shown here by default. Try zooming in and seeing how much less distortion happens as you pan around. This is because the projection is designed to be used in navigating within countries, not showing the whole globe.
For some real fun, switch to the Sinusoidal projection and pan around!
You can fork this and add your own image with this short node script: (remember to npm install get-pixels
)
getPixels = require("get-pixels")
fs = require 'fs'
output = []
getPixels "mbostock.jpg", (err, data) ->
for y in [0...data.shape[1]]
for x in [0...data.shape[0]]
p = [data.get(x,y,0), data.get(x,y, 1), data.get(x,y, 2)]
output.push(p)
fs.writeFileSync 'mbostock.json', JSON.stringify(output)
You can then upload the resulting json file (feel free to rename it) using blockbuilder.org.
For your convenience, this block can be forked quickly here: http://blockbuilder.org/enjalot/5233898432653069ea8e
Original prompt by @curran
Bounding box solution by @tyrasd
projection comparison
map zoom
forked from enjalot's block: visualizing map distortion
xxxxxxxxxx
<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;
}
canvas {
pointer-events: none;
position: absolute;
top: 28px;
}
#left-canvas {
left: 28px;
}
#right-canvas {
right: 40px;
}
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;
}
</style>
</head>
<body>
<svg id="left"></svg>
<svg id="right"></svg>
<canvas id="left-canvas"></canvas>
<canvas id="right-canvas"></canvas>
<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 facePoints = [];
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.mercator()
.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);
ctxRight.clearRect(0, 0, map_width + 40, map_height);
facePoints.forEach(function(d) {
var latlon = projectionLeft.invert([d.x, d.y])
var xy = projectionRight(latlon)
if(!xy) return;
ctxRight.fillStyle = "rgba(" + d.color + ",0.5)"
ctxRight.beginPath();
ctxRight.arc(xy[0], xy[1], 2.3, 0, Math.PI*2, true);
ctxRight.closePath();
ctxRight.fill();
})
}
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);
var canvasLeft = d3.select("#left-canvas")
.attr("width", map_width)
.attr("height", map_height);
var canvasRight = d3.select("#right-canvas")
.attr("width", map_width + 40)
.attr("height", map_height);
var ctxLeft = canvasLeft.node().getContext("2d")
var ctxRight = canvasRight.node().getContext("2d")
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("mbostock.json", function(error,bostock) {
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 xpixels = 73;
var ypixels = 73;
facePoints = bostock.map(function(d,i) {
var x = i % xpixels * map_width/xpixels;
var y = Math.floor(i / ypixels) * map_height/ypixels;
return {
x: x,
y: y,
color: d
}
})
facePoints.forEach(function(d) {
ctxLeft.fillStyle = "rgba(" + d.color + ",1)"
ctxLeft.beginPath();
ctxLeft.arc(d.x, d.y, 2.3, 0, Math.PI*2, true);
ctxLeft.closePath();
ctxLeft.fill();
})
zoomed();
});
});
var projections = {
"Mercator": d3.geo.mercator().scale(50),
"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),
"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)
};
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();
})
</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>
https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js
https://cdnjs.cloudflare.com/ajax/libs/d3-geo-projection/0.2.9/d3.geo.projection.min.js
https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.19/topojson.min.js