Ported to d3v4 by Philippe Rivière from Ian Johnson's block: visualizing map distortion
I'm using a fork of d3-geo that allows inverse projections on all projections (see d3-geo-projection
issue #85).
@enjalot: 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
projection comparison
map zoom
forked from Fil's block: visualizing map distortion d3v4
forked from Fil's block: visualizing map distortion d3v4 (with Bertin and a forked d3-geo for inverse) [UNLISTED]
xxxxxxxxxx
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-geo-projection.v2.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<script src="https://d3js.org/d3-hsv.v0.1.min.js"></script>
<script src="https://d3js.org/d3-contour.v1.min.js"></script>
<style>
select {
position: fixed;
top: 20px;
left: 20px;
}
.foreground {
fill: none;
stroke: #333;
stroke-width: 1.5px;
}
.graticule {
fill: none;
stroke: #aaa;
stroke-width: .5px;
}
.land {
fill: #cfcece;
stroke: #a5967e;
}
</style>
</head>
<body>
<script>
// DISTORTION
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]
];
}
function norm(a){
return Math.sqrt(dot(a,a));
}
var epsilon = 1e-4;
function distortion(projection, point) {
var point1 = [point[0] + epsilon, point[1]],
point2 = [point[0], point[1] + epsilon],
p0 = projection(point),
p1 = projection(point1),
p2 = projection(point2),
d1 = [ p1[0] - p0[0], p1[1] - p0[1], 0 ],
d2 = [ p2[0] - p0[0], p2[1] - p0[1], 0 ];
// areal distorsion = norm(d1 x d2) / cos(lat)
var areal = norm(cross(d1, d2)) / Math.abs(Math.cos(point[1] * Math.PI/180)) / epsilon / epsilon;
// angular distorsion = (d1 . d2) / ( norm(d1) * norm(d2) )
var angular = Math.abs(dot(d1, d2) / norm(d1) / norm(d2));
//console.log(areal, angular)
return {
areal: areal,
angular: angular
};
}
// /DISTORTION
// import from math
var sin = Math.sin, cos = Math.cos, pi = Math.PI;
// import from d3-geo-projection
var hammerRaw = d3.geoHammerRaw;
var visionscarto_bertin_1953_alpha3 = function (λ, φ) {
var fu = 1.4;
if (λ + φ < -fu) {
var u = (λ - φ + 1.6) * (λ + φ + fu) / 8;
λ += u;
φ -= 0.8 * u * sin(φ + pi/2);
}
var r = hammerRaw(1.68, 2)(λ, φ);
var k = 12,
d = (1 - cos(λ * φ)) / k;
if (r[1] < 0) {
r[0] *= 1 + d;
}
if (r[1] > 0) {
r[1] *= 1 + d / 1.5 * r[0] * r[0];
}
return r;
};
(d3.geoVisionscarto = (function () {
return d3.geoProjection(visionscarto_bertin_1953_alpha3)
.rotate([-16.5, -42]);
}));
var projections = {
"Bertin1953": d3.geoVisionscarto(),
"Aitoff": d3.geoAitoff().scale(90),
"Boggs Eumorphic": d3.geoBoggs().scale(90),
"Craster Parabolic (Putnins P4)": d3.geoCraster().scale(90),
"Cylindrical Equal-Area": d3.geoCylindricalEqualArea().scale(120),
"Eckert I": d3.geoEckert1().scale(95),
"Eckert III": d3.geoEckert3().scale(105),
"Eckert IV": d3.geoEckert4().scale(105),
"Eckert V": d3.geoEckert5().scale(100),
"Equidistant Cylindrical (Plate Carrée)": d3.geoEquirectangular().scale(90),
"Fahey": d3.geoFahey().scale(75),
"Foucaut Sinusoidal": d3.geoFoucaut().scale(80),
"Gall (Gall Stereographic)": d3.geoCylindricalStereographic().scale(70),
"Ginzburg VIII (TsNIIGAiK 1944)": d3.geoGinzburg8().scale(75),
"Kavraisky VII": d3.geoKavrayskiy7().scale(90),
"Larrivée": d3.geoLarrivee().scale(55),
"McBryde-Thomas Flat-Pole Sine (No. 2)": d3.geoMtFlatPolarSinusoidal().scale(95),
"Mercator": d3.geoMercator().scale(50),
"Miller Cylindrical I": d3.geoMiller().scale(60),
"Mollweide": d3.geoMollweide().scale(100),
"Natural Earth": d3.geoNaturalEarth().scale(100),
"Nell-Hammer": d3.geoNellHammer().scale(120),
"Quartic Authalic": d3.geoHammer().coefficient(Infinity).scale(95),
"Robinson": d3.geoRobinson().scale(90),
"Sinusoidal": d3.geoSinusoidal().scale(90),
"van der Grinten (I)": d3.geoVanDerGrinten().scale(50),
"Wagner VI": d3.geoWagner6().scale(90),
"Wagner VII": d3.geoWagner7().scale(90),
"Winkel Tripel": d3.geoWinkel3().scale(90),
"Wiechel": d3.geoWiechel().scale(90) };
var width = 960;
var height = 500;
var color = d3.scaleLinear()
.domain([0,1])
.range(['white', 'orange']);
var projection = projections['Bertin1953']
.fitExtent([[0,0],[width, height]], {type:"Sphere"});
var path = d3.geoPath()
.projection(projection);
var path3 = function(d) {
var d1 = {
type:"MultiPolygon",
coordinates: d.coordinates.map(d => d.map(
k1 => k1.map(k => projection([lonScale(k[0]), latScale(k[1])]))
))
};
//console.log(d, d1)
return path(d1);
}
var n = 120, m = 60, scale = width / n;
var path2 = d3.geoPath(d3.geoIdentity().scale(scale));
var lonScale = d3.scaleLinear()
.domain([0,n])
.range([-179,179]),
latScale = d3.scaleLinear()
.domain([0,m])
.range([-89,89]);
var graticule = d3.geoGraticule();
function update() {
svg.selectAll("path")
.attr("d", path);
var contours = d3.contours().size([n, m])
.thresholds(d3.range(-1,2,0.04));
values = d3.merge(
d3.range(m)
.map(lat =>
d3.range(n)
.map(lon => {
if (point = projection.invert([lon * scale,lat * scale]))
{
return distortion(projection, point).angular;
}
else
return -1;
})));
svg.selectAll("path.contour")
.remove();
svg.selectAll("path.contour")
.data(contours(values))
.enter().append("path")
.attr('class', 'contour')
.attr("stroke", function(d) { return color(d.value); })
.attr("fill", function(d) { return color(d.value); })
.attr("fill-opacity", 0.5)
.attr("d", path2);
}
svg = d3.select('body')
.append('svg')
.attr("width", width)
.attr("height", height);
svg.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path);
d3.json("world-110m.json", function(error,world) {
if (error) throw error;
svg.insert("path", ".graticule")
.datum(topojson.feature(world, world.objects.land))
.attr("class", "land")
.attr("d", path);
});
var selector = d3.select('body').append("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;
projection = projections[proj]
.fitExtent([[0,0],[width, height]], {type:"Sphere"});
path = d3.geoPath()
.projection(projection);
update();
})
</script>
</body>
https://d3js.org/d3.v4.min.js
https://d3js.org/d3-geo-projection.v2.min.js
https://d3js.org/topojson.v1.min.js
https://d3js.org/d3-hsv.v0.1.min.js
https://d3js.org/d3-contour.v1.min.js