Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active November 8, 2019 08:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save nitaku/b66b6e352187c836c1f53b091f7a1b05 to your computer and use it in GitHub Desktop.
Save nitaku/b66b6e352187c836c1f53b091f7a1b05 to your computer and use it in GitHub Desktop.
World population - bubbles on map

This example shows bubbles with area proportional to each country's population (2016) superimposed on a world map. Bubbles are also colored according to the continent.

Showing population and geography together, this map could probably be better redesigned as a choropleth of population density. This example is an exercise to prepare for different kinds of data, for which a geographical density would not make sense.

Population data is obtained from data.worldbank.org. The name of the source is "World Development Indicators" and was last updated on 2017, August 2nd.

Based on this example about implementing a map of the world and this one about population bubbles.

svg = d3.select 'svg'
width = svg.node().getBoundingClientRect().width
height = svg.node().getBoundingClientRect().height
# 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 [0, 0]
.scale (width - 3) / (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, 50]
# COLORS
color = d3.scaleOrdinal(d3.schemeCategory10)
.domain ['North America', 'Africa', 'South America', 'Asia', 'Europe', 'Oceania']
zoomable_layer.append 'path'
.datum graticule.outline()
.attrs
class: 'sphere_fill'
d: path
contents = zoomable_layer.append 'g'
zoomable_layer.append 'path'
.datum graticule
.attrs
class: 'graticule'
d: path
zoomable_layer.append 'path'
.datum graticule.outline()
.attrs
class: 'sphere_stroke'
d: path
d3.json 'ne_50m_admin_0_countries.topo.json', (geo_data) ->
countries_data = topojson.feature(geo_data, geo_data.objects.countries).features
# label each polygon instead of each multipolygon (to help with islands etc.)
labels_data = []
countries_data.forEach (d) ->
if d.geometry.type is 'Polygon'
# compute area to aid label hiding
d.area = d3.geoArea(d)
d.main = d
labels_data.push d
else if d.geometry.type is 'MultiPolygon'
subpolys = []
d.geometry.coordinates.forEach (p) ->
sp = {
coordinates: p
properties: d.properties
type: 'Polygon'
}
# compute area to aid label hiding
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]
labels_data = labels_data.concat subpolys
# countries
countries = contents.selectAll '.country'
.data countries_data
en_countries = countries.enter().append 'path'
.attrs
class: 'country'
d: path
# labels
labels = contents.selectAll '.label'
.data labels_data
en_labels = labels.enter().append 'g'
.attrs
class: 'label'
transform: (d) ->
[x,y] = projection d3.geoCentroid(d)
return "translate(#{x},#{y})"
en_labels
.classed 'no_iso_code', (d) -> d.properties.iso_a2 is '-99'
en_labels.append 'text'
.text (d) -> d.properties.name_long
.attrs
dy: '0.35em'
# lod
lod(1)
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 {
country: d
value: +index[d.properties.iso_a3]['2016']
}
radius
.domain [0, d3.max population_data, (d) -> d.value]
# sort by descending population to avoid covering a small bubble with a big one
population_data.sort (a,b) -> d3.descending(a.value, b.value)
# bubbles
bubbles = contents.selectAll '.bubble'
.data population_data
en_bubbles = bubbles.enter().append 'circle'
.attrs
class: 'bubble'
fill: (d) -> color d.country.properties.continent
r: (d) -> radius d.value
transform: (d) ->
[x,y] = projection d3.geoCentroid(d.country.main)
return "translate(#{x},#{y})"
en_bubbles.append 'title'
.text (d) -> "#{d.country.properties.name_long}\nPopulation: #{d3.format(',')(d.value)}"
lod = (z) ->
zoomable_layer.selectAll '.label'
.classed 'hidden', (d) -> d.area < Math.pow(0.2/z,2)
body, html {
padding: 0;
margin: 0;
height: 100%;
}
svg {
width: 100%;
height: 100%;
background: white;
}
.sphere_stroke {
fill: none;
stroke: black;
stroke-width: 2px;
vector-effect: non-scaling-stroke;
}
.sphere_fill {
fill: white;
}
.graticule {
fill: none;
stroke: #777;
stroke-width: 0.5px;
stroke-opacity: 0.5;
vector-effect: non-scaling-stroke;
pointer-events: none;
}
.country {
fill: #999;
fill-opacity: 0.3;
stroke: white;
stroke-width: 0.5;
vector-effect: non-scaling-stroke;
}
.label {
font-family: sans-serif;
font-size: 10px;
pointer-events: none;
text-anchor: middle;
}
.label.no_iso_code {
font-style: italic;
}
.label.hidden {
display: none;
}
.bubble {
fill-opacity: 0.2;
stroke: black;
stroke-width: 0.5;
vector-effect: non-scaling-stroke;
}
.bubble:hover {
fill-opacity: 0.4;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>World population - bubbles on map</title>
<link type="text/css" href="index.css" rel="stylesheet"/>
<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>
</head>
<body>
<svg></svg>
<script src="index.js"></script>
</body>
</html>
// Generated by CoffeeScript 1.10.0
(function() {
var color, contents, graticule, height, lod, path, projection, radius, svg, width, zoom, zoomable_layer;
svg = d3.select('svg');
width = svg.node().getBoundingClientRect().width;
height = svg.node().getBoundingClientRect().height;
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([0, 0]).scale((width - 3) / (2 * Math.PI)).translate([width / 2, height / 2]);
path = d3.geoPath(projection);
graticule = d3.geoGraticule();
radius = d3.scaleSqrt().range([0, 50]);
color = d3.scaleOrdinal(d3.schemeCategory10).domain(['North America', 'Africa', 'South America', 'Asia', 'Europe', 'Oceania']);
zoomable_layer.append('path').datum(graticule.outline()).attrs({
"class": 'sphere_fill',
d: path
});
contents = zoomable_layer.append('g');
zoomable_layer.append('path').datum(graticule).attrs({
"class": 'graticule',
d: path
});
zoomable_layer.append('path').datum(graticule.outline()).attrs({
"class": 'sphere_stroke',
d: path
});
d3.json('ne_50m_admin_0_countries.topo.json', function(geo_data) {
var countries, countries_data, en_countries, en_labels, labels, labels_data;
countries_data = topojson.feature(geo_data, geo_data.objects.countries).features;
labels_data = [];
countries_data.forEach(function(d) {
var subpolys;
if (d.geometry.type === 'Polygon') {
d.area = d3.geoArea(d);
d.main = d;
return labels_data.push(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);
});
d.main = subpolys.reduce((function(a, b) {
if (a.area > b.area) {
return a;
} else {
return b;
}
}), subpolys[0]);
return labels_data = labels_data.concat(subpolys);
}
});
countries = contents.selectAll('.country').data(countries_data);
en_countries = countries.enter().append('path').attrs({
"class": 'country',
d: path
});
labels = contents.selectAll('.label').data(labels_data);
en_labels = labels.enter().append('g').attrs({
"class": 'label',
transform: function(d) {
var ref, x, y;
ref = projection(d3.geoCentroid(d)), x = ref[0], y = ref[1];
return "translate(" + x + "," + y + ")";
}
});
en_labels.classed('no_iso_code', function(d) {
return d.properties.iso_a2 === '-99';
});
en_labels.append('text').text(function(d) {
return d.properties.name_long;
}).attrs({
dy: '0.35em'
});
lod(1);
return d3.csv('population.csv', function(data) {
var bubbles, en_bubbles, index, population_data;
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({
country: d,
value: +index[d.properties.iso_a3]['2016']
});
}
});
radius.domain([
0, d3.max(population_data, function(d) {
return d.value;
})
]);
population_data.sort(function(a, b) {
return d3.descending(a.value, b.value);
});
bubbles = contents.selectAll('.bubble').data(population_data);
en_bubbles = bubbles.enter().append('circle').attrs({
"class": 'bubble',
fill: function(d) {
return color(d.country.properties.continent);
},
r: function(d) {
return radius(d.value);
},
transform: function(d) {
var ref, x, y;
ref = projection(d3.geoCentroid(d.country.main)), x = ref[0], y = ref[1];
return "translate(" + x + "," + y + ")";
}
});
return en_bubbles.append('title').text(function(d) {
return d.country.properties.name_long + "\nPopulation: " + (d3.format(',')(d.value));
});
});
});
lod = function(z) {
return zoomable_layer.selectAll('.label').classed('hidden', function(d) {
return d.area < Math.pow(0.2 / z, 2);
});
};
}).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