block by nitaku 49a6bde57d8d8555b6823c8c6d05c5a8

World population - Dorling diagram

Full Screen

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.

index.js

// 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);

index.html

<!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>

index.coffee

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

index.css

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;
}