This block shows how population by region is expected to change over the coming years.
It is a remake of a dataviz featured in Visual Capitalist's article Animation: Global Population by Region From 1950 to 2100, which was inspired by geographer Simon Kuestenmacher :
For this remake, the main idea was to make it more connected to our Earth (as I found the original pie chart and bar chart lacking this feeling). So I use a disk. The increase of this disk encodes the increase of the global population over years. In this disk, the area of each cell encodes the population of its corresponding region.
Technically speaking:
forked from Kcnarf's block: Global Population by Region from 1950 to 2100 - a remake
xxxxxxxxxx
<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.v5.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 "+halfYearRadius+" "+halfYearRadius+" 0 0 1 "+[yearRadius,0])
.attr("id", "year_path");
​
mapContainer.append("text")
.append("textPath")
.attr("xlink:href", "#year_path")
.attr("startOffset", "30%")
.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", "70%")
.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>
Updated missing url https://rawcdn.githack.com/Kcnarf/d3-weighted-voronoi/v1.0.0/build/d3-weighted-voronoi.js to https://rawcdn.githack.com/kcnarf/d3-weighted-voronoi/v1.0.0/build/d3-weighted-voronoi.js
Updated missing url https://rawcdn.githack.com/Kcnarf/d3-voronoi-map/v2.0.0/build/d3-voronoi-map.js to https://rawcdn.githack.com/kcnarf/d3-voronoi-map/v2.0.0/build/d3-voronoi-map.js
https://d3js.org/d3.v5.min.js
https://rawcdn.githack.com/Kcnarf/d3-weighted-voronoi/v1.0.0/build/d3-weighted-voronoi.js
https://rawcdn.githack.com/Kcnarf/d3-voronoi-map/v2.0.0/build/d3-voronoi-map.js