block by nitaku 95a31e8beb6fc937b42f43d6c6434614

World population - bubbles without map

Full Screen

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.

index.js

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

index.html

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

index.coffee

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

index.css

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