|
<html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> |
|
<title>Global Population By Region Until 2100</title> |
|
<meta name="description" content="d3-voronoi-map plugin to remake 'Global Population by Region from 1950 to 2100'"> |
|
<script src="https://d3js.org/d3.v6.min.js" charset="utf-8"></script> |
|
<script src="https://rawcdn.githack.com/Kcnarf/d3-weighted-voronoi/v1.0.0/build/d3-weighted-voronoi.js"></script> |
|
<script src="https://rawcdn.githack.com/Kcnarf/d3-voronoi-map/v2.0.0/build/d3-voronoi-map.js"></script> |
|
<style> |
|
|
|
#wip { |
|
display: none; |
|
position: absolute; |
|
top: 200px; |
|
left: 330px; |
|
font-size: 40px; |
|
text-align: center; |
|
} |
|
|
|
svg { |
|
background-color: rgb(250,250,250); |
|
} |
|
|
|
#title { |
|
letter-spacing: 4px; |
|
font-weight: 700; |
|
font-size: x-large; |
|
} |
|
|
|
text.tiny { |
|
font-size: 10pt; |
|
} |
|
text.light { |
|
fill: lightgrey |
|
} |
|
|
|
.map-container { |
|
transition: transform 0.2s ease-in-out; |
|
} |
|
|
|
.symbol { |
|
fill: none; |
|
stroke: lightgrey; |
|
stroke-width: 14px; |
|
} |
|
|
|
.cell { |
|
stroke: darkgrey; |
|
stroke-width: 1px; |
|
} |
|
|
|
.population { |
|
text-anchor: middle; |
|
dominant-baseline: central; |
|
} |
|
|
|
.population-total, .year { |
|
fill: lightgrey; |
|
text-anchor: middle; |
|
font-size: 20px; |
|
font-weight: 700; |
|
} |
|
|
|
.dashed { |
|
stroke-dasharray: 2, 4; |
|
} |
|
|
|
.remarquable-circles { |
|
fill: none; |
|
stroke: lightgrey; |
|
stroke-width: 1px; |
|
} |
|
|
|
.remarquable-notes path { |
|
fill: none; |
|
stroke: grey; |
|
} |
|
|
|
.remarquable-notes circle { |
|
fill: grey; |
|
} |
|
|
|
.remarquable-note.year2100>path { |
|
stroke-dasharray: none; |
|
} |
|
|
|
.legend-color { |
|
stroke-width: 1px; |
|
stroke:darkgrey; |
|
} |
|
|
|
.highlighter { |
|
fill: transparent; |
|
stroke: none; |
|
} |
|
.highlight { |
|
stroke: black; |
|
stroke-width: 1px; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<svg> |
|
<defs> |
|
<filter id="inset-shadow"> |
|
<feGaussianBlur stdDeviation="5" result="offset-blur"></feGaussianBlur> |
|
<!-- Shadow Blur --> |
|
<feComposite operator="out" in="SourceGraphic" in2="offset-blur" result="inverse"></feComposite> |
|
<!-- Invert the drop shadow to create an inner shadow --> |
|
<feFlood flood-color="white" flood-opacity="1" result="color"></feFlood> <!-- Color & Opacity --> |
|
<feComposite operator="in" in="color" in2="inverse" result="shadow"></feComposite> |
|
<!-- Clip color inside shadow --> |
|
<feComponentTransfer in="shadow" result="shadow"> |
|
<!-- Shadow Opacity --> |
|
<feFuncA type="linear" slope=".75"></feFuncA> |
|
</feComponentTransfer> |
|
<feComposite operator="over" in="shadow" in2="SourceGraphic"></feComposite> |
|
<!-- Put shadow over original object --> |
|
</filter> |
|
</defs> |
|
</svg> |
|
|
|
<div id="wip"> |
|
Work in progress ... |
|
</div> |
|
|
|
<script> |
|
//begin: constants |
|
var _PI = Math.PI, |
|
_2PI = 2*Math.PI, |
|
cos = Math.cos, |
|
sin = Math.sin, |
|
sqrt = Math.sqrt; |
|
//end: constants |
|
|
|
//begin: raw data global def |
|
var overallData; |
|
var totalPopulationByYear = {}; |
|
var remarquableData; |
|
//end: raw data global def |
|
|
|
//begin: data-related utils |
|
function populationAccessor(d){ return d["populationOfYear"]; }; |
|
function highlighterGroupId(d){ return "group-"+d.id; }; |
|
function sortById(d0,d1) {return d1.id-d0.id; }; |
|
//end: data-related utils |
|
|
|
//begin: drawing utils |
|
function liner(poly){ return "M"+poly.join(",")+"z"; }; |
|
//end: drawing utils |
|
|
|
//begin: layout conf. |
|
var svgWidth = 960, |
|
svgHeight = 500, |
|
margin = {top: 10, right: 10, bottom: 10, left: 10}, |
|
height = svgHeight - margin.top - margin.bottom, |
|
width = svgWidth - margin.left - margin.right, |
|
halfWidth = width/2, |
|
halfHeight = height/2, |
|
quarterWidth = width/4, |
|
quarterHeight = height/4, |
|
titleY = 20, |
|
legendsTopY = 70, |
|
mapCenter = [quarterWidth-20, halfHeight+20]; |
|
//end: layout conf. |
|
|
|
//begin: map conf. |
|
var baseRadius = 85; |
|
var baseTotalPopulation, |
|
year, |
|
dataOfYear, |
|
totalPopulationOfYear, |
|
circlingPolygon, |
|
firstFrame, |
|
simulation, |
|
polygons; |
|
//end: map conf. |
|
|
|
//begin: reusable d3Selection |
|
var svg, drawingArea, mapContainer; |
|
//end: reusable d3Selection |
|
|
|
d3.csv("globalPopulationByRegionUntil2100.csv").then(function(data) { |
|
data.forEach(function(d) {csvParser(d)}); |
|
overallData = data; |
|
|
|
initData(); |
|
initLayout(); |
|
|
|
loopThroughYears(); |
|
}); |
|
|
|
////////////////////////////////////// |
|
// Mechanics involved for animation // |
|
////////////////////////////////////// |
|
|
|
function loopThroughYears() { |
|
totalPopulationOfYear = totalPopulationByYear[year]; |
|
makeDataForYear(); |
|
simulate(); |
|
if (year<2100) { |
|
year += 5; |
|
setTimeout(loopThroughYears, 750); |
|
} |
|
} |
|
|
|
function simulate(){ |
|
simulation = d3.voronoiMapSimulation(dataOfYear) |
|
.clip(circlingPolygon) |
|
.weight(populationAccessor) |
|
.convergenceRatio(0.02) |
|
.on("tick", function() { |
|
// function called after each iteration of computation |
|
// called only in simulation mode, not in static mode (see below) |
|
polygons = simulation.state().polygons; |
|
redrawMap(); |
|
}) |
|
.on("end", function() { |
|
attachMouseListener(dataOfYear); |
|
firstFrame = false; |
|
}); |
|
|
|
|
|
if (firstFrame) { |
|
// an adequate positioning policy is used (in a circular manner, ~ at their real life position, tweaked by rows' positions in .csv file) |
|
simulation.initialPosition(d3.voronoiMapInitialPositionPie().startAngle(-Math.PI/10)); |
|
} else { |
|
// if a simulation has already be computed, |
|
// previously computed coordinates and weights are reused |
|
// in order to maintaint position of each cell, which is more user friendly |
|
simulation.initialPosition((d)=>[d.previousX, d.previousY]) |
|
.initialWeight((d)=>d.previousWeight); |
|
} |
|
} |
|
|
|
////////////////////////////////////// |
|
// Data management // |
|
////////////////////////////////////// |
|
|
|
function initData() { |
|
baseTotalPopulation = totalPopulationByYear[1950]; |
|
year = 1950; |
|
circlingPolygon = computeCirclingPolygon(baseRadius); |
|
firstFrame = true; |
|
makeRemarquableData(); |
|
} |
|
|
|
function makeDataForYear() { |
|
totalPopulationOfYear = totalPopulationByYear[year]; |
|
dataOfYear = overallData.map((d)=>{ |
|
return { |
|
id: d.id, |
|
continent: d.continent, |
|
populationOfYear: d[year], |
|
color: d.color, |
|
previousX: NaN, |
|
previousY: NaN, |
|
previousWeight: NaN |
|
} |
|
}).sort(sortById); |
|
|
|
if (!firstFrame) { |
|
var previousPolygonById = {}; |
|
simulation.state().polygons.forEach((p)=>{ |
|
previousPolygonById[p.site.originalObject.data.originalData.id]=p |
|
}) |
|
|
|
dataOfYear.forEach((d) => { |
|
previousPolygon = previousPolygonById[d.id]; |
|
d.previousX = previousPolygon.site.x, //pick previously computed X coord |
|
d.previousY = previousPolygon.site.y, //pick previously computed Y coord |
|
d.previousWeight = previousPolygon.site.weight //pick previously computed weight |
|
}) |
|
} |
|
} |
|
|
|
function csvParser(d) { |
|
d3.range(1950, 2101, 5).map(function(year){ |
|
d[year] = +d[year]; |
|
if (totalPopulationByYear[year]) { |
|
totalPopulationByYear[year] += d[year]; |
|
} else { |
|
totalPopulationByYear[year] = d[year]; |
|
} |
|
}); |
|
d.id = +d.id; |
|
d.color = d.color; |
|
return d; |
|
} |
|
|
|
////////////////////////////////////// |
|
// Drawing // |
|
////////////////////////////////////// |
|
|
|
function computeCirclingPolygon(radius) { |
|
var points = 60, |
|
increment = _2PI/points, |
|
circlingPolygon = []; |
|
|
|
for (var a=0, i=0; i<points; i++, a+=increment) { |
|
circlingPolygon.push( |
|
[radius*Math.cos(a), radius*Math.sin(a)] |
|
) |
|
} |
|
|
|
return circlingPolygon; |
|
}; |
|
|
|
function initLayout() { |
|
svg = d3.select("svg") |
|
.attr("width", svgWidth) |
|
.attr("height", svgHeight); |
|
|
|
drawingArea = svg.append("g") |
|
.classed("drawingArea", true) |
|
.attr("transform", "translate("+[margin.left,margin.top]+")"); |
|
|
|
mapContainer = drawingArea.append("g") |
|
.classed("map-container", true) |
|
.attr("transform", "translate("+mapCenter+")"); |
|
drawSymbol(); |
|
|
|
var yearRadius = baseRadius+5, |
|
halfYearRadius = yearRadius/2; |
|
|
|
svg.select("defs") |
|
.append("path") |
|
.attr("d", "M "+[-yearRadius,0]+" A "+yearRadius+" "+yearRadius+" 0 1 1 "+[0, yearRadius]) |
|
.attr("id", "year_path"); |
|
|
|
mapContainer.append("text") |
|
.append("textPath") |
|
.attr("xlink:href", "#year_path") |
|
.attr("startOffset", "5%") |
|
.append("tspan") |
|
.classed("year", true) |
|
.attr("transform", "rotate(-45)translate(0,"+(-baseRadius-6)+")"); |
|
|
|
mapContainer.append("text") |
|
.append("textPath") |
|
.attr("xlink:href", "#year_path") |
|
.attr("startOffset", "40%") |
|
.append("tspan") |
|
.classed("population-total", true) |
|
.attr("transform", "rotate(45)translate(0,"+(-baseRadius-6)+")"); |
|
|
|
mapContainer.append("g") |
|
.classed('cells', true); |
|
mapContainer.append("g") |
|
.classed('populations', true); |
|
mapContainer.append("g") |
|
.classed('highlighters', true); |
|
|
|
drawRemarquables(); |
|
drawLegends(); |
|
drawTitle(); |
|
drawFooter(); |
|
} |
|
|
|
function makeRemarquableData() { |
|
var radiusStrokeCompensation = 3; |
|
remarquableData = []; |
|
|
|
remarquableData.push({ |
|
radius: baseRadius+radiusStrokeCompensation, |
|
desc: "1950's global population", |
|
class: "year1950" |
|
}); |
|
remarquableData.push({ |
|
radius: baseRadius*sqrt(2)+radiusStrokeCompensation, |
|
desc: "Two times 1950's global population, reached by year ~1995", |
|
class: "2-times" |
|
}); |
|
remarquableData.push({ |
|
radius: baseRadius*sqrt(3)+radiusStrokeCompensation, |
|
desc: "Three times 1950's global population, projected to be reached by year ~2025", |
|
class: "3-times" |
|
}); |
|
remarquableData.push({ |
|
radius: baseRadius*2+radiusStrokeCompensation, |
|
desc: "Four times 1950's global population, projected to be reached by year ~2060", |
|
class: "4-times" |
|
}); |
|
remarquableData.push({ |
|
radius: baseRadius * sqrt(totalPopulationByYear[2100]/baseTotalPopulation)+radiusStrokeCompensation, |
|
desc: "2100, projected to be more than four times 1950's global population", |
|
class: "year2100" |
|
}); |
|
} |
|
|
|
function drawRemarquables() { |
|
drawRemarquableCircles(); |
|
drawRemarquableNotes(); |
|
} |
|
|
|
function drawRemarquableCircles() { |
|
var remarquableCircles = drawingArea.insert("g", ".map-container") |
|
.classed("remarquable-circles", true) |
|
.attr("transform", "translate("+mapCenter+")"); |
|
|
|
|
|
remarquableCircles.selectAll('.remarquable') |
|
.data(remarquableData) |
|
.enter() |
|
.append("circle") |
|
.attr("class", (d) => d.class) |
|
.classed("remarquable-circle dashed", true) |
|
.attr("r", (d)=>d.radius); |
|
} |
|
|
|
function drawRemarquableNotes() { |
|
var radius2100 = remarquableData[4].radius; |
|
var remarquableNoteRadius = radius2100 + 20, |
|
textInitialAngle = -1*_PI/20, |
|
textInBetweenAngle = _PI/20, |
|
arcInitialAngle = textInitialAngle + 4*textInBetweenAngle, |
|
arcDeltaAngle = _PI/40, |
|
arcStartAngle = arcInitialAngle + arcDeltaAngle, |
|
arcEndAngle = arcInitialAngle - arcDeltaAngle; |
|
|
|
var remarquableNotes = drawingArea.append("g") |
|
.classed("remarquable-notes", true) |
|
.attr("transform", "translate("+mapCenter+")"); |
|
|
|
var enteringNotes = remarquableNotes.selectAll('.remarquable-note') |
|
.data(remarquableData) |
|
.enter(); |
|
|
|
var noteGroups = enteringNotes |
|
.append("g") |
|
.attr("class", (d) => d.class) |
|
.classed("remarquable-note", true); |
|
|
|
//begin: draw text note |
|
noteGroups.append("text") |
|
.attr("transform", function (d,i){ |
|
var angle = textInitialAngle+i*textInBetweenAngle, |
|
x = remarquableNoteRadius*cos(angle), |
|
y = remarquableNoteRadius*sin(angle); |
|
return "translate("+[x+5,y+5]+")" |
|
}) |
|
.text((d)=>d.desc); |
|
//end: draw text note |
|
|
|
//begin: draw arc |
|
noteGroups.append("path") |
|
.attr("d", function(d,i) { |
|
arcStartX = d.radius*cos(arcStartAngle); |
|
arcStartY = d.radius*sin(arcStartAngle); |
|
arcEndX = d.radius*cos(arcEndAngle); |
|
arcEndY = d.radius*sin(arcEndAngle); |
|
if (i!==4) { |
|
var path = "M "+[arcStartX, arcStartY]; |
|
path += " A "+[d.radius, d.radius]+" 0 0,0 "+[arcEndX, arcEndY]; |
|
return path; |
|
} else { |
|
return ""; |
|
} |
|
}); |
|
//end: draw arc |
|
|
|
//begin: draw line |
|
noteGroups.append("path") |
|
.classed("dashed", true) |
|
.attr("d", function(d,i) { |
|
var textX = remarquableNoteRadius*cos(textInitialAngle+i*textInBetweenAngle), |
|
textY = remarquableNoteRadius*sin(textInitialAngle+i*textInBetweenAngle), |
|
midArcX = d.radius*cos(arcInitialAngle), |
|
midArcY = d.radius*sin(arcInitialAngle); |
|
|
|
var path = "M "+[midArcX, midArcY]; |
|
path += " L "+[textX, textY]; |
|
return path; |
|
}); |
|
//end: draw line |
|
} |
|
|
|
function drawTitle() { |
|
drawingArea.append("text") |
|
.attr("id", "title") |
|
.attr("transform", "translate("+[halfWidth, titleY]+")") |
|
.attr("text-anchor", "middle") |
|
.text("Global Population by Region from 1950 to 2100") |
|
} |
|
|
|
function drawFooter() { |
|
drawingArea.append("text") |
|
.classed("tiny light", true) |
|
.attr("transform", "translate("+[0, height]+")") |
|
.attr("text-anchor", "start") |
|
.text("Remake of 'Global Population by Region from 1950 to 2100'") |
|
drawingArea.append("text") |
|
.classed("tiny light", true) |
|
.attr("transform", "translate("+[halfWidth+45, height]+")") |
|
.attr("text-anchor", "middle") |
|
.text("by @_Kcnarf") |
|
drawingArea.append("text") |
|
.classed("tiny light", true) |
|
.attr("transform", "translate("+[width, height]+")") |
|
.attr("text-anchor", "end") |
|
.text("bl.ocks.org/Kcnarf/6195b6ec020c180ad50a14b739510ddc") |
|
} |
|
|
|
function drawLegends() { |
|
var legendHeight = 13, |
|
interLegend = 4, |
|
colorWidth = legendHeight*4; |
|
|
|
var legendContainer = drawingArea.append("g") |
|
.classed("legend", true) |
|
.attr("transform", "translate("+[width, legendsTopY]+")"); |
|
|
|
var legends = legendContainer.selectAll(".legend") |
|
.data(overallData.reverse()) |
|
.enter(); |
|
|
|
var legend = legends.append("g") |
|
.classed("legend", true) |
|
.attr("transform", function(d,i){ |
|
return "translate("+[0, i*(legendHeight+interLegend)]+")"; |
|
}) |
|
|
|
legend.append("rect") |
|
.classed("legend-color", true) |
|
.attr("filter", "url(#inset-shadow)") |
|
.attr("x", -colorWidth) |
|
.attr("width", colorWidth) |
|
.attr("height", legendHeight) |
|
.style("fill", function(d){ return d.color; }); |
|
legend.append("text") |
|
.classed("tiny", true) |
|
.attr("transform", "translate("+[-(colorWidth+5), legendHeight-2]+")") |
|
.style("text-anchor", "end") |
|
.text(function(d){ return d.continent; }); |
|
legend.append("rect") |
|
.attr("class", highlighterGroupId) |
|
.classed("highlighter", true) |
|
.attr("x", -colorWidth) |
|
.attr("width", colorWidth) |
|
.attr("height", legendHeight); |
|
|
|
legendContainer.append("text") |
|
.attr("transform", "translate("+[0, -interLegend]+")") |
|
.style("text-anchor", "end") |
|
.text("Regions"); |
|
} |
|
|
|
function drawSymbol() { |
|
var symbol = mapContainer.append("g").classed("symbol", true); |
|
|
|
symbol.append("circle") |
|
.attr("r", baseRadius-5); |
|
} |
|
|
|
function redrawMap() { |
|
var radiusRatio = sqrt(totalPopulationOfYear/baseTotalPopulation); |
|
|
|
// here we apply a scale to the entire viz in order to encode the growth of the entire population over years |
|
mapContainer.attr("transform", "translate("+mapCenter+")scale("+radiusRatio+")"); |
|
|
|
mapContainer.select(".year") |
|
.style("font-size", 1/radiusRatio*20) //daownscale to preserve fontsize |
|
.text("year "+year); |
|
|
|
var globalPopulationText = ""; |
|
if (year > 2019) { |
|
globalPopulationText += "~ " |
|
} |
|
globalPopulationText += (totalPopulationOfYear/1000).toFixed(1)+" B people" |
|
mapContainer.select(".population-total") |
|
.text(globalPopulationText); |
|
|
|
var cells = mapContainer.select(".cells") |
|
.selectAll(".cell") |
|
.data(polygons); |
|
cells.enter() |
|
.append("path") |
|
.classed("cell", true) |
|
.merge(cells) |
|
.attr("filter", "url(#inset-shadow)") |
|
.attr("d", liner) |
|
.style("fill", function(d){ |
|
return d.site.originalObject.data.originalData.color; |
|
}); |
|
|
|
var populations = mapContainer.select(".populations") |
|
.selectAll(".population") |
|
.data(polygons); |
|
populations.enter() |
|
.append("text") |
|
.classed("population", true) |
|
.merge(populations) |
|
.attr("transform", function(d){ |
|
return "translate("+[d.site.x, d.site.y]+")scale("+1/radiusRatio+")"; // +6 for vertical centering |
|
}) |
|
.text(function(d){ |
|
return populationAccessor(d.site.originalObject.data.originalData); |
|
}) |
|
|
|
var highlighters = mapContainer.select(".highlighters") |
|
.selectAll(".highlighter") |
|
.data(polygons); |
|
highlighters.enter() |
|
.append("path") |
|
.merge(highlighters) |
|
.attr("class", function(d) { |
|
return highlighterGroupId(d.site.originalObject.data.originalData); |
|
}) |
|
.classed("highlighter", true) |
|
.attr("d", liner); |
|
} |
|
|
|
function attachMouseListener(dataOfYear){ |
|
var regionId; |
|
|
|
dataOfYear.forEach(function(d){ |
|
regionId = d.id |
|
|
|
d3.selectAll(".group-"+regionId) |
|
.on("mouseenter", highlight(regionId, true)) |
|
.on("mouseleave", highlight(regionId, false)); |
|
}) |
|
} |
|
|
|
function highlight(regionId, highlight){ |
|
return function() { |
|
d3.selectAll(".group-"+regionId) |
|
.classed("highlight", highlight); |
|
} |
|
} |
|
</script> |
|
</body> |
|
</html> |