block by nitaku 8de69327a7587770f7efd70f6054d5f6

World population - circle packing

Full Screen

Another take on world population as bubble areas, this time with almost no geography at all. Countries are grouped and colored according to their continent.

Data from Natural Earth & data.worldbank.org.

index.js

// Generated by CoffeeScript 1.10.0
(function() {
  var color, height, lod, pack, 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;

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

  pack = d3.pack().size([width - 2, height - 2]).padding(3);

  color = d3.scaleOrdinal(d3.schemeCategory10).domain(['North America', 'Africa', 'South America', 'Asia', 'Europe', 'Oceania', 'Seven seas (open ocean)']);

  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;
    return d3.csv('population.csv', function(data) {
      var bubbles, en, en_labels, index, labels, population_data, root;
      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']
          });
        }
      });
      population_data.push({
        id: "root",
        parent: ""
      });
      population_data.push({
        id: "North America",
        parent: "root"
      });
      population_data.push({
        id: "Africa",
        parent: "root"
      });
      population_data.push({
        id: "South America",
        parent: "root"
      });
      population_data.push({
        id: "Asia",
        parent: "root"
      });
      population_data.push({
        id: "Europe",
        parent: "root"
      });
      population_data.push({
        id: "Oceania",
        parent: "root"
      });
      population_data.push({
        id: "Seven seas (open ocean)",
        parent: "root"
      });
      root = (d3.stratify().id(function(d) {
        return d.id;
      }).parentId(function(d) {
        return d.parent;
      }))(population_data);
      root.sum(function(d) {
        return d.value;
      }).sort(function(a, b) {
        return b.value - a.value;
      });
      pack(root);
      bubbles = zoomable_layer.selectAll('.bubble').data(root.leaves());
      en = bubbles.enter().append('circle').attrs({
        "class": 'bubble',
        cx: function(d) {
          return d.x;
        },
        cy: function(d) {
          return d.y;
        },
        r: function(d) {
          return d.r;
        },
        fill: function(d) {
          return color(d.parent.id);
        }
      });
      en.append('title').text(function(d) {
        return d.data.country.properties.name_long + "\nPopulation: " + (d3.format(',')(d.value));
      });
      labels = zoomable_layer.selectAll('.label').data(root.leaves());
      en_labels = labels.enter().append('g').attrs({
        "class": 'label',
        transform: function(d) {
          return "translate(" + d.x + "," + d.y + ")";
        }
      });
      en_labels.append('text').text(function(d) {
        return d.data.country.properties.name_long;
      }).attrs({
        dy: '0.35em'
      });
      return lod(1);
    });
  });

  lod = function(z) {
    return zoomable_layer.selectAll('.label').classed('hidden', function(d) {
      return d.r < 18 / z;
    });
  };

}).call(this);

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>World population - circle Packing</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="//d3js.org/topojson.v2.min.js"></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

# 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

# PACK
pack = d3.pack()
  .size([width - 2, height - 2])
  .padding(3)
  
# COLORS
color = d3.scaleOrdinal(d3.schemeCategory10)
  .domain ['North America', 'Africa', 'South America', 'Asia', 'Europe', 'Oceania', 'Seven seas (open ocean)']

d3.json 'ne_50m_admin_0_countries.topo.json', (geo_data) ->
    countries_data = topojson.feature(geo_data, geo_data.objects.countries).features
    
    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']
          }

      # adding dummy root since d3 stratify does not handle multiple roots
      population_data.push {id: "root", parent: ""}
      # also add continents
      population_data.push {id: "North America", parent: "root"}
      population_data.push {id: "Africa", parent: "root"}
      population_data.push {id: "South America", parent: "root"}
      population_data.push {id: "Asia", parent: "root"}
      population_data.push {id: "Europe", parent: "root"}
      population_data.push {id: "Oceania", parent: "root"}
      population_data.push {id: "Seven seas (open ocean)", parent: "root"}

      # tree construction
      root = (d3.stratify()
        .id((d) -> d.id)
        .parentId((d) -> d.parent)
        )(population_data)
      root
        .sum (d) -> d.value
        .sort (a, b) -> b.value - a.value
  
      pack(root)
  
      # bubbles
      bubbles = zoomable_layer.selectAll '.bubble'
        .data root.leaves()

      en = bubbles.enter().append 'circle'
        .attrs
          class: 'bubble'
          cx: (d) -> d.x
          cy: (d) -> d.y
          r: (d) -> d.r
          fill: (d) -> color d.parent.id

        en.append 'title'
          .text (d) -> "#{d.data.country.properties.name_long}\nPopulation: #{d3.format(',')(d.value)}"
          
      # labels
      labels = zoomable_layer.selectAll '.label'
        .data root.leaves()

      en_labels = labels.enter().append 'g'
        .attrs
          class: 'label'
          transform: (d) -> "translate(#{d.x},#{d.y})"
          
      en_labels.append 'text'
        .text (d) -> d.data.country.properties.name_long
        .attrs
          dy: '0.35em'
          
      # lod
      lod(1)
          
lod = (z) ->
  zoomable_layer.selectAll '.label'
    .classed 'hidden', (d) -> d.r < 18/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%;
}

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