Population of the countries of the world, encoded as circular areas as in the previous example. This time, countries are not depicted. Bubbles are placed onto a country’s centroid, and the graticule is left to help locating faraway bubbles (e.g., the pacific islands) and give a sense of geographical displacement.
// 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_data;
countries_data = topojson.feature(geo_data, geo_data.objects.countries).features;
countries_data.forEach(function(d) {
var subpolys;
if (d.geometry.type === 'Polygon') {
d.area = d3.geoArea(d);
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, index, labels, 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 + ")";
}
});
en_bubbles.append('title').text(function(d) {
return d.country.properties.name_long + "\nPopulation: " + (d3.format(',')(d.value));
});
labels = contents.selectAll('.label').data(population_data);
en_labels = labels.enter().append('g').attrs({
"class": 'label',
transform: function(d) {
var ref, x, y;
ref = projection(d3.geoCentroid(d.country.main)), x = ref[0], y = ref[1];
return "translate(" + x + "," + y + ")";
}
});
en_labels.append('text').text(function(d) {
return d.country.properties.name_long;
}).attrs({
dy: '0.35em'
});
return lod(1);
});
});
lod = function(z) {
return zoomable_layer.selectAll('.label').classed('hidden', function(d) {
return radius(d.value) < 18 / z;
});
};
}).call(this);
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>World population - bubbles without 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>
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
# subdivide multipolygons
countries_data.forEach (d) ->
if d.geometry.type is 'Polygon'
# compute area to aid label hiding
d.area = d3.geoArea(d)
d.main = 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]
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)}"
# labels
labels = contents.selectAll '.label'
.data population_data
en_labels = labels.enter().append 'g'
.attrs
class: 'label'
transform: (d) ->
[x,y] = projection d3.geoCentroid(d.country.main)
return "translate(#{x},#{y})"
en_labels.append 'text'
.text (d) -> d.country.properties.name_long
.attrs
dy: '0.35em'
# lod
lod(1)
lod = (z) ->
zoomable_layer.selectAll '.label'
.classed 'hidden', (d) -> radius(d.value) < 18/z
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;
}