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.
Last active
March 25, 2022 13:31
-
-
Save nitaku/49a6bde57d8d8555b6823c8c6d05c5a8 to your computer and use it in GitHub Desktop.
World population - Dorling diagram
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); |
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