Skip to content

Instantly share code, notes, and snippets.

@syntagmatic
Last active August 10, 2022 03:54
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save syntagmatic/ba569633d51ebec6ec6e to your computer and use it in GitHub Desktop.
Comparing Map Projections

A mashup of Map Projection Distortions and transitions using the D3.js extended geographic projections plugin.

A comparison of map projections by four different types of distortion:

  • Acc. 40° 150% – The Acceptance index is a numerical measure that summarizes overall projection distortion, in this case with a maximum angular distortion of 40° and areal distortion of up to 150%.
  • Scale – The weighted mean error for overall scale distortion.
  • Areal – The weighted mean error for areal distortion.
  • Angular – The mean angular deformation index.

Lower is better. Data transcribed from the Natural Earth Projection by @mbostock.

Read more about map projections on Wikipedia.

<!DOCTYPE html>
<meta charset="utf-8">
<title>Map Projections</title>
<style>
svg {
font: 11px sans-serif;
}
.background path {
fill: none;
stroke: none;
stroke-width: 30px;
pointer-events: stroke;
}
.foreground path {
fill: none;
stroke: steelblue;
stroke-width: 2.5px;
}
.axis .title {
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
}
.axis line,
.axis path {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.label {
-webkit-transition: fill 125ms linear;
}
.active .label:not(.inactive) {
font-weight: bold;
}
.label.inactive {
fill: #888;
}
.foreground path.inactive {
stroke: #888;
stroke-opacity: .3;
stroke-width: 1.5px;
}
.stroke {
fill: none;
stroke: #000;
stroke-width: 3px;
}
.fill {
fill: #a4bac7;
}
path.foreground {
fill: none;
stroke: #333;
stroke-width: 1.5px;
}
path.graticule {
fill: none;
stroke: #aaa;
stroke-width: .5px;
}
.line:nth-child(2n) {
stroke-dasharray: 2,2;
}
.land {
fill: #d7c7ad;
stroke: #a5967e;
}
</style>
<body>
<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>
<script>
/* Map */
var map_width = 960,
map_height = 290;
var projection = d3.geo.aitoff()
.translate([map_width / 2 - .5, map_height / 2 - .5]).scale(90);
var path = d3.geo.path()
.projection(projection);
var graticule = d3.geo.graticule();
var map_svg = d3.select("body").append("svg")
.attr("width", map_width)
.attr("height", map_height);
map_svg.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path);
d3.json("world-110m.json", function(error,world) {
if (error) throw error;
map_svg.insert("path", ".graticule")
.datum(topojson.feature(world, world.objects.land))
.attr("class", "land")
.attr("d", path);
});
/* Parallel Coordinates */
var margin = {top: 30, right: 160, bottom: 20, left: 250},
width = 960 - margin.left - margin.right,
height = 350 - margin.top - margin.bottom;
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)
};
var dimensions = [
{
name: "name",
scale: d3.scale.ordinal().rangePoints([0, height]),
type: String
},
{
name: "Acc. 40º 150%",
scale: d3.scale.linear().range([0, height]),
type: Number
},
{
name: "Scale",
scale: d3.scale.linear().range([height, 0]),
type: Number
},
{
name: "Areal",
scale: d3.scale.sqrt().range([height, 0]),
type: Number
},
{
name: "Angular",
scale: d3.scale.linear().range([height, 0]),
type: Number
}
];
var x = d3.scale.ordinal()
.domain(dimensions.map(function(d) { return d.name; }))
.rangePoints([0, width]);
var line = d3.svg.line()
.defined(function(d) { return !isNaN(d[1]); });
var yAxis = d3.svg.axis()
.orient("left");
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var dimension = svg.selectAll(".dimension")
.data(dimensions)
.enter().append("g")
.attr("class", "dimension")
.attr("transform", function(d) { return "translate(" + x(d.name) + ")"; });
d3.tsv("projections.tsv", function(data) {
data = data.filter(function(d) { return d.name in projections });
dimensions.forEach(function(dimension) {
dimension.scale.domain(dimension.type === Number
? d3.extent(data, function(d) { return +d[dimension.name]; })
: data.map(function(d) { return d[dimension.name]; }).sort());
});
svg.append("g")
.attr("class", "background")
.selectAll("path")
.data(data)
.enter().append("path")
.attr("d", draw);
svg.append("g")
.attr("class", "foreground")
.selectAll("path")
.data(data)
.enter().append("path")
.attr("d", draw);
dimension.append("g")
.attr("class", "axis")
.each(function(d) { d3.select(this).call(yAxis.scale(d.scale)); })
.append("text")
.attr("class", "title")
.attr("text-anchor", "middle")
.attr("y", -9)
.text(function(d) { return d.name; });
var lazyMouseover = debounce(mouseover, 100);
// Rebind the axis data to simplify mouseover.
svg.select(".axis").selectAll("text:not(.title)")
.attr("class", "label")
.data(data, function(d) { return d.name || d; });
var projection_line = svg.selectAll(".axis text.label,.background path,.foreground path")
.on("mouseover", lazyMouseover)
.on("mouseout", mouseout);
mouseover(data.filter(function(d) { return d.name == "Aitoff" })[0]);
function mouseover(d) {
if (!(d.name in projections)) return;
if (d.name == projection.name) return;
svg.classed("active", true);
projection_line.classed("inactive", function(p) { return p !== d; });
projection_line.filter(function(p) { return p === d; }).each(moveToFront);
// update map
var last_projection = projection;
projection = projections[d.name]
.translate([map_width / 2 - .5, map_height / 2 - .5]);
path = d3.geo.path()
.projection(projection);
map_svg.selectAll("path")
.transition()
.duration(450)
.attr("d", path)
.attrTween("d", projectionTween(last_projection, projection));
}
function mouseout(d) {
return;
}
function moveToFront() {
this.parentNode.appendChild(this);
}
});
function draw(d) {
return line(dimensions.map(function(dimension) {
return [x(dimension.name), dimension.scale(d[dimension.name])];
}));
}
d3.select(self.frameElement).style("height", (map_height+height+margin.top+margin.bottom) + "px");
function projectionTween(projection0, projection1) {
return function(d) {
var t = 0;
var projection = d3.geo.projection(project)
.scale(1)
.translate([map_width / 2, map_height / 2]);
var path = d3.geo.path()
.projection(projection);
function project(λ, φ) {
λ *= 180 / Math.PI, φ *= 180 / Math.PI;
var p0 = projection0([λ, φ]), p1 = projection1([λ, φ]);
return [(1 - t) * p0[0] + t * p1[0], (1 - t) * -p0[1] + t * -p1[1]];
}
return function(_) {
t = _;
return path(d);
};
};
}
// From underscore.js
function debounce(func, wait, immediate) {
var timeout, args, context, timestamp, result;
var later = function() {
var last = new Date().getTime() - timestamp;
if (last < wait && last >= 0) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
if (!immediate) {
result = func.apply(context, args);
if (!timeout) context = args = null;
}
}
};
return function() {
context = this;
args = arguments;
timestamp = new Date().getTime();
var callNow = immediate && !timeout;
if (!timeout) timeout = setTimeout(later, wait);
if (callNow) {
result = func.apply(context, args);
context = args = null;
}
return result;
};
};
</script>
Acc. 40º 150% Scale Areal Angular name
87.7 0.29 0.37 18.25 Eckert III
87.5 0.25 0.19 20.54 Natural Earth
87.4 0.27 0.17 24.2 Winkel II
86.5 0.23 0.28 19.15 Kavraisky VII
85 0.26 0.18 23.28 Winkel Tripel
84.3 0.27 0.19 21.27 Robinson
83.2 0.25 0.43 16.14 Fahey
81.9 0.36 0 28.73 Eckert IV
81.8 0.26 0.24 22.31 Hölzel
80.4 0.26 0.34 20.41 Wagner VI
80 0.3 0.29 23.47 Eckert V
78.4 0.32 0.07 26.38 McBryde-Thomas Flat-Pole Sine (No. 2)
78.1 0.29 0.23 25.8 Winkel I
77.9 0.28 0.3 22.68 Wagner III
76.5 0.32 0.12 26.88 Wagner II
76.2 0.31 0.25 NaN Denoyer Semi-elliptical
75.2 0.3 0.31 24.31 Putnins P5'
74.3 0.29 0.57 16.84 Equidistant Cylindrical (Plate Carrée)
74.1 0.38 0 30.56 Kavraisky V
74 0.37 0 30.71 Wagner VII
72.4 0.39 0 31.54 Putnins P4'
71.9 0.57 0 30.9 Cylindrical Equal-Area
70.6 0.39 0 32.28 Mollweide
70.5 0.29 0.51 20.36 Ginzburg VIII (TsNIIGAiK 1944)
70.2 0.43 0 30.9 Nell-Hammer
69.1 0.39 0.1 30.79 Putnins P1
68.1 0.36 0.23 30.17 Aitoff
68.1 0.36 0.29 30.3 Eckert I
67 0.44 0 33.33 Foucaut Sinusoidal
63.7 0.44 0 35.2 Boggs Eumorphic
63 0.45 0 35.5 Eckert-Greifendorff
62.9 0.39 1.3 7.63 Miller Cylindrical I
62.4 0.47 0 36 Quartic Authalic
61.7 0.35 1.05 10.06 Larrivée
60.3 0.48 0 36.99 Craster Parabolic (Putnins P4)
58.9 0.46 0 36.39 Goode Homolosine
57.4 0.41 1.46 7.75 van der Grinten (I)
57.4 0.57 4.79 0 Mercator
57.1 0.51 0 39.01 Sinusoidal
49 0.3 0.73 10.62 Gall (Gall Stereographic)
42.2 0.74 0 52.39 Foucaut
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@timelyportfolio
Copy link

wow! this is really well done. thanks so much for sharing.

@wyckster
Copy link

Why does Australia disappear when I switch to Mercator?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment