Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active March 25, 2022 13:31
Show Gist options
  • Save nitaku/49a6bde57d8d8555b6823c8c6d05c5a8 to your computer and use it in GitHub Desktop.
Save nitaku/49a6bde57d8d8555b6823c8c6d05c5a8 to your computer and use it in GitHub Desktop.
World population - Dorling diagram

Not a real Dorling diagram, I know, but similar. Each bubble tries to stay centered in its country's centroid, like in this example, but also avoid collisions with other bubbles. Similar to the previous example, but providing more geographical information.

svg = d3.select 'body'
.append 'svg'
width = d3.select('svg').node().getBoundingClientRect().width
height = d3.select('svg').node().getBoundingClientRect().height
CONTINENTS = ['North America', 'Africa', 'South America', 'Asia', 'Europe', 'Oceania', 'Seven seas (open ocean)']
# ZOOM
zoomable_layer = svg.append 'g'
zoom = d3.zoom()
.scaleExtent([-Infinity,Infinity])
.on 'zoom', () ->
zoomable_layer
.attrs
transform: d3.event.transform
# SEMANTIC ZOOM
# scale back all objects that have to be semantically zoomed
zoomable_layer.selectAll '.label > text'
.attrs
transform: "scale(#{1/d3.event.transform.k})"
# LOD & OVERLAPPING
lod(d3.event.transform.k)
svg.call zoom
# PROJECTION
projection = d3.geoWinkel3()
.rotate [0, 0]
.center [16, 13.8]
.scale 1.3*width / (2 * Math.PI)
.translate [width/2, height/2]
path = d3.geoPath projection
# GRATICULE and OUTLINE
graticule = d3.geoGraticule()
# POPULATION BUBBLES SCALE
radius = d3.scaleSqrt()
.range [0, 65]
# FORCE
simulation = d3.forceSimulation()
.force 'collision', d3.forceCollide((d) -> d.r + 0.35)
.force 'attract', d3.forceAttract().target((d) -> [d.foc_x, d.foc_y])
# COLORS
color = d3.scaleOrdinal(d3.schemeCategory10)
.domain CONTINENTS
contents = zoomable_layer.append 'g'
d3.json 'ne_50m_admin_0_countries.topo.json', (geo_data) ->
countries_data = topojson.feature(geo_data, geo_data.objects.countries).features
land = topojson.merge(geo_data, geo_data.objects.countries.geometries.filter (d) -> d.properties.continent isnt 'Antarctica')
# land
contents.append 'path'
.attrs
class: 'land'
d: path land
# find the biggest polygon in multipolygons
countries_data.forEach (d) ->
if d.geometry.type is 'Polygon'
d.main = d
else if d.geometry.type is 'MultiPolygon'
subpolys = []
d.geometry.coordinates.forEach (p) ->
sp = {
coordinates: p
properties: d.properties
type: 'Polygon'
}
sp.area = d3.geoArea(sp)
subpolys.push sp
# store the biggest polygon as main
d.main = subpolys.reduce ((a, b) -> if a.area > b.area then a else b), subpolys[0]
d3.csv 'population.csv', (data) ->
# use ISO a3 code as ID
# WARNING some records do not match
index = {}
data.forEach (d) ->
index[d['Country Code']] = d
population_data = []
countries_data.forEach (d) ->
if d.properties.iso_a3 of index
population_data.push {
id: d.properties.iso_a3
parent: d.properties.continent
country: d
value: +index[d.properties.iso_a3]['2016']
}
# precompute radii
radius
.domain [0, d3.max population_data, (d) -> d.value]
population_data.forEach (d) -> d.r = radius(d.value)
# force simulation starts with each country bubble placed in its centroid
# the centroid is also the attraction focus
population_data.forEach (d) ->
d.centroid = projection d3.geoCentroid(d.country.main)
d.x = d.centroid[0]
d.y = d.centroid[1]
d.foc_x = d.centroid[0]
d.foc_y = d.centroid[1]
# bubbles
bubbles = zoomable_layer.selectAll '.bubble'
.data population_data
en_bubbles = bubbles.enter().append 'circle'
.attrs
class: 'bubble'
r: (d) -> d.r
fill: (d) -> color d.parent
en_bubbles.append 'title'
.text (d) -> "#{d.country.properties.name_long}\nPopulation: #{d3.format(',')(d.value)}"
# labels
labels = zoomable_layer.selectAll '.label'
.data population_data
en_labels = labels.enter().append 'g'
.attrs
class: 'label'
en_labels.append 'text'
.text (d) -> d.country.properties.name_long
.attrs
dy: '0.35em'
# lod
lod(1)
# Simulation
simulation
.nodes population_data
.stop()
for i in [0...Math.ceil(Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay()))]
simulation.tick()
en_bubbles.attrs
transform: (d) -> "translate(#{d.x},#{d.y})"
en_labels.attrs
transform: (d) -> "translate(#{d.x},#{d.y})"
lod = (z) ->
zoomable_layer.selectAll '.label'
.classed 'hidden', (d) -> d.r < 23/z
body, html {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
body {
display: flex;
justify-content: center;
align-items: center;
}
svg {
width: 100%;
height: 100%;
}
.land {
fill: #E8E8E8;
}
.bubble {
fill-opacity: 0.3;
stroke: black;
stroke-width: 0.5;
vector-effect: non-scaling-stroke;
}
.bubble:hover {
fill-opacity: 0.5;
}
.label {
font-family: sans-serif;
font-size: 10px;
pointer-events: none;
text-anchor: middle;
}
.label.hidden {
display: none;
}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>World population - Dorling diagram</title>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-selection-multi.v0.4.min.js"></script>
<script src="https://d3js.org/d3-geo-projection.v2.min.js"></script>
<script src="//d3js.org/topojson.v2.min.js"></script>
<script src="https://unpkg.com/d3-force-attract@latest"></script>
<link rel="stylesheet" href="index.css">
</head>
<body>
<script src="index.js"></script>
</body>
</html>
// Generated by CoffeeScript 1.10.0
(function() {
var CONTINENTS, color, contents, graticule, height, lod, path, projection, radius, simulation, svg, width, zoom, zoomable_layer;
svg = d3.select('body').append('svg');
width = d3.select('svg').node().getBoundingClientRect().width;
height = d3.select('svg').node().getBoundingClientRect().height;
CONTINENTS = ['North America', 'Africa', 'South America', 'Asia', 'Europe', 'Oceania', 'Seven seas (open ocean)'];
zoomable_layer = svg.append('g');
zoom = d3.zoom().scaleExtent([-Infinity, Infinity]).on('zoom', function() {
zoomable_layer.attrs({
transform: d3.event.transform
});
zoomable_layer.selectAll('.label > text').attrs({
transform: "scale(" + (1 / d3.event.transform.k) + ")"
});
return lod(d3.event.transform.k);
});
svg.call(zoom);
projection = d3.geoWinkel3().rotate([0, 0]).center([16, 13.8]).scale(1.3 * width / (2 * Math.PI)).translate([width / 2, height / 2]);
path = d3.geoPath(projection);
graticule = d3.geoGraticule();
radius = d3.scaleSqrt().range([0, 65]);
simulation = d3.forceSimulation().force('collision', d3.forceCollide(function(d) {
return d.r + 0.35;
})).force('attract', d3.forceAttract().target(function(d) {
return [d.foc_x, d.foc_y];
}));
color = d3.scaleOrdinal(d3.schemeCategory10).domain(CONTINENTS);
contents = zoomable_layer.append('g');
d3.json('ne_50m_admin_0_countries.topo.json', function(geo_data) {
var countries_data, land;
countries_data = topojson.feature(geo_data, geo_data.objects.countries).features;
land = topojson.merge(geo_data, geo_data.objects.countries.geometries.filter(function(d) {
return d.properties.continent !== 'Antarctica';
}));
contents.append('path').attrs({
"class": 'land',
d: path(land)
});
countries_data.forEach(function(d) {
var subpolys;
if (d.geometry.type === 'Polygon') {
return d.main = d;
} else if (d.geometry.type === 'MultiPolygon') {
subpolys = [];
d.geometry.coordinates.forEach(function(p) {
var sp;
sp = {
coordinates: p,
properties: d.properties,
type: 'Polygon'
};
sp.area = d3.geoArea(sp);
return subpolys.push(sp);
});
return d.main = subpolys.reduce((function(a, b) {
if (a.area > b.area) {
return a;
} else {
return b;
}
}), subpolys[0]);
}
});
return d3.csv('population.csv', function(data) {
var bubbles, en_bubbles, en_labels, i, index, j, labels, population_data, ref;
index = {};
data.forEach(function(d) {
return index[d['Country Code']] = d;
});
population_data = [];
countries_data.forEach(function(d) {
if (d.properties.iso_a3 in index) {
return population_data.push({
id: d.properties.iso_a3,
parent: d.properties.continent,
country: d,
value: +index[d.properties.iso_a3]['2016']
});
}
});
radius.domain([
0, d3.max(population_data, function(d) {
return d.value;
})
]);
population_data.forEach(function(d) {
return d.r = radius(d.value);
});
population_data.forEach(function(d) {
d.centroid = projection(d3.geoCentroid(d.country.main));
d.x = d.centroid[0];
d.y = d.centroid[1];
d.foc_x = d.centroid[0];
return d.foc_y = d.centroid[1];
});
bubbles = zoomable_layer.selectAll('.bubble').data(population_data);
en_bubbles = bubbles.enter().append('circle').attrs({
"class": 'bubble',
r: function(d) {
return d.r;
},
fill: function(d) {
return color(d.parent);
}
});
en_bubbles.append('title').text(function(d) {
return d.country.properties.name_long + "\nPopulation: " + (d3.format(',')(d.value));
});
labels = zoomable_layer.selectAll('.label').data(population_data);
en_labels = labels.enter().append('g').attrs({
"class": 'label'
});
en_labels.append('text').text(function(d) {
return d.country.properties.name_long;
}).attrs({
dy: '0.35em'
});
lod(1);
simulation.nodes(population_data).stop();
for (i = j = 0, ref = Math.ceil(Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay())); 0 <= ref ? j < ref : j > ref; i = 0 <= ref ? ++j : --j) {
simulation.tick();
}
en_bubbles.attrs({
transform: function(d) {
return "translate(" + d.x + "," + d.y + ")";
}
});
return en_labels.attrs({
transform: function(d) {
return "translate(" + d.x + "," + d.y + ")";
}
});
});
});
lod = function(z) {
return zoomable_layer.selectAll('.label').classed('hidden', function(d) {
return d.r < 23 / z;
});
};
}).call(this);
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.
View raw

(Sorry about that, but we can’t show files that are this big right now.)

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