|
<!DOCTYPE html> |
|
<svg width='960' height='600'></svg> |
|
|
|
<script src="https://unpkg.com/d3-array@1"></script> |
|
<script src="https://unpkg.com/d3-contour@1"></script> |
|
<script src="https://unpkg.com/d3-collection@1"></script> |
|
<script src="https://unpkg.com/d3-color@1"></script> |
|
<script src="https://unpkg.com/d3-dispatch@1"></script> |
|
<script src="https://unpkg.com/d3-geo@1"></script> |
|
<script src="https://unpkg.com/d3-geo-projection@2"></script> |
|
<script src="https://unpkg.com/d3-interpolate@1"></script> |
|
<script src="https://unpkg.com/d3-request@1"></script> |
|
<script src="https://unpkg.com/d3-selection@1"></script> |
|
<script src="https://unpkg.com/d3-scale@1"></script> |
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script> |
|
<script src="https://unpkg.com/geotiff@0.4/dist/geotiff.browserify.min.js"></script> |
|
<script src="http://d3js.org/queue.v1.min.js"></script> |
|
<script src="http://d3js.org/topojson.v1.min.js"></script> |
|
|
|
<script> |
|
|
|
queue() |
|
.defer(d3.json, "world-50m.json") |
|
.defer(d3.json, "lakes-50m.json") |
|
.defer(d3.request("gpwv4-unpd.coarse.tiff").responseType("arraybuffer").get) |
|
.await(ready); |
|
|
|
function ready (error, world, lakes, unpd) { |
|
if (error) throw error; |
|
|
|
var tiff = GeoTIFF.parse(unpd.response), |
|
image = tiff.getImage(), |
|
values = image.readRasters()[0], |
|
m = image.getHeight(), |
|
n = image.getWidth(); |
|
|
|
values = values.map((v) => v < 0 ? 0 : v); |
|
|
|
var numThresholds = 12; |
|
var thresholds = d3.range(0, numThresholds).map((v) => Math.pow(2, v)); |
|
|
|
var color = d3.scaleThreshold() |
|
.domain(thresholds) |
|
.range(d3.range(0, numThresholds).map((v) => d3.interpolateMagma(v / numThresholds))); |
|
|
|
var contours = d3.contours() |
|
.size([n, m]) |
|
.smooth(false) |
|
.thresholds([0, ...thresholds]); |
|
|
|
var projection = d3.geoInterruptedMollweide() |
|
.scale(165) |
|
.precision(0.1); |
|
|
|
var path = d3.geoPath(projection); |
|
|
|
var svg = d3.select('svg'), |
|
width = +svg.attr("width"), |
|
height = +svg.attr("height"); |
|
|
|
svg.append('path').attr('class', 'background') |
|
.datum({type: 'Sphere'}) |
|
.attr('fill', color(0)) |
|
.attr('d', path); |
|
|
|
var defs = svg.append("defs"); |
|
|
|
defs.append("path") |
|
.datum({type: "Sphere"}) |
|
.attr("id", "sphere") |
|
.attr("d", path); |
|
|
|
defs.append("path") |
|
.datum(topojson.feature(world, world.objects.land)) |
|
.attr("id", "land") |
|
.attr("d", path); |
|
|
|
defs.append("clipPath") |
|
.attr("id", "clipSphere") |
|
.append("use").attr("xlink:href", "#sphere"); |
|
|
|
defs.append("clipPath") |
|
.attr("id", "clipLand") |
|
.attr("clip-path", "url(#clipSphere)") |
|
.append("use").attr("xlink:href", "#land"); |
|
|
|
svg.append('g').attr('class', 'contours') |
|
.selectAll("path") |
|
.data(contours(values).map(invert)) |
|
.enter().append("path") |
|
.attr("fill", function(d) { return color(d.value); }) |
|
.attr('clip-path', 'url(#clipLand)') |
|
.attr("d", path); |
|
|
|
svg.insert("path") |
|
.datum(topojson.feature(world, world.objects.land)) |
|
.attr('fill', 'none') |
|
.attr('stroke', 'white') |
|
.attr('stroke-width', 0.2) |
|
.attr("d", path); |
|
|
|
svg.insert("path") |
|
.datum(lakes) |
|
.attr('fill', color(0)) |
|
.attr('stroke', 'white') |
|
.attr('stroke-width', 0.1) |
|
.attr("d", path); |
|
|
|
var x = d3.scaleLog() |
|
.base(2) |
|
.domain([0.5, thresholds.slice(-1)])//.slice(0, -1)) |
|
.rangeRound([0, 600]); |
|
|
|
// legend code based on Mike Bostock's https://bl.ocks.org/mbostock/39b34968ad5eab65de1d7da81f78bb27 |
|
// used here under terms of GPL v3 |
|
|
|
var keyGroup = svg.append("g") |
|
.attr("class", "key") |
|
.attr("transform", "translate(180,560)"); |
|
|
|
keyGroup.selectAll("rect") |
|
.data(color.range().map(function(d) { |
|
d = color.invertExtent(d); |
|
if (d[0] == null) d[0] = x.domain()[0]; |
|
if (d[1] == null) d[1] = x.domain()[1]; |
|
return d; |
|
})) |
|
.enter().append("rect") |
|
.attr("height", 10) |
|
.attr("x", (d) => x(d[0])) |
|
.attr("width", (d) => x(d[1]) - x(d[0])) |
|
.attr("fill", (d) => color(d[0])); |
|
|
|
keyGroup.append("text") |
|
.attr("class", "caption") |
|
.attr("x", x.range()[0]) |
|
.attr("y", -8) |
|
.attr("fill", "#000") |
|
.attr("text-anchor", "start") |
|
.attr("font-weight", "bold") |
|
.attr("font-size", "130%") |
|
.text("Estimated population density in 2015 (persons per square kilometer)"); |
|
|
|
keyGroup.call(d3.axisBottom(x) |
|
.tickSize(16) |
|
.tickValues(color.domain().slice(0, -1))) |
|
.select(".domain") |
|
.remove(); |
|
|
|
// 'invert' function copyright Mike Bostock, used here under terms of GPL v3 |
|
// more info: https://bl.ocks.org/mbostock/83c0be21dba7602ee14982b020b12f51 |
|
function invert(d) { |
|
var shared = {}; |
|
|
|
var p = { |
|
type: "Polygon", |
|
coordinates: d3.merge(d.coordinates.map(function(polygon) { |
|
return polygon.map(function(ring) { |
|
return ring.map(function(point) { |
|
return [ |
|
point[0] / n * 360 - 180, |
|
85 - point[1] / m * 145 |
|
]; |
|
}).reverse(); |
|
}); |
|
})) |
|
}; |
|
|
|
// Record the y-intersections with the antimeridian. |
|
p.coordinates.forEach(function(ring) { |
|
ring.forEach(function(p) { |
|
if (p[0] === -180) shared[p[1]] |= 1; |
|
else if (p[0] === 180) shared[p[1]] |= 2; |
|
}); |
|
}); |
|
|
|
// Offset any unshared antimeridian points to prevent their stitching. |
|
p.coordinates.forEach(function(ring) { |
|
ring.forEach(function(p) { |
|
if ((p[0] === -180 || p[0] === 180) && shared[p[1]] !== 3) { |
|
p[0] = p[0] === -180 ? -179.9995 : 179.9995; |
|
} |
|
}); |
|
}); |
|
|
|
p = d3.geoStitch(p); |
|
if (!p.coordinates.length) p = {type: "Sphere"}; // TODO fix d3.geoStitch |
|
p.value = d.value; |
|
return p; |
|
} |
|
} |
|
|
|
</script> |