Comparing Map Projections

5 min read Original article ↗
<!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>