block by curran 115407b42ef85b0758595d05c825b346

Cities on the Globe

Full Screen

A globe with pan & zoom showing circles for each city of over 100,000 inhabitants. Data from Geonames, preprocessed. The same data is used in World City Explorer.

Uses world-110m and world-50m geographic shapes from TOPOJSON World Atlas.

Inspired by

forked from curran‘s block: Orthographic Zoom III

index.html

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <script src="https://d3js.org/d3.v4.min.js"></script>
  <script src="https://d3js.org/topojson.v1.min.js"></script>
  <script src="https://unpkg.com/d3-tip@0.7.1/index.js"></script>
  <style>
    .d3-tip {
      font-family: sans-serif;
      font-size: 1.5em;
      line-height: 1;
      padding: 7px;
      background: black;
      color: lightgray;
      border-radius: 20px;
    }
    circle:hover { 
      stroke: white;
      stroke-width: 0.5px;
      fill-opacity: 1;
    }
  </style>
</head>
<body>
  <svg width="960" height="500"></svg>
  <script>
    const svg = d3.select('svg').style('background-color', '#222');
    const path = svg.append('path').attr('stroke', 'gray');
    const citiesG = svg.append('g');
    const projection = d3.geoOrthographic();
    const initialScale = projection.scale();
    const geoPath = d3.geoPath().projection(projection);
    let moving = false;
    const rValue = d => d.population;
    const rScale = d3.scaleSqrt().range([0, 20]);
    
    var commaFormat = d3.format(',');
    var tip = d3.tip()
      .attr('class', 'd3-tip')
      .offset([-10, 0])
      .html(d => `${d.name}: ${commaFormat(d.population)}`);
    svg.call(tip);

    d3.queue()
      .defer(d3.json, 'https://unpkg.com/world-atlas@1/world/110m.json')
      .defer(d3.json, 'https://unpkg.com/world-atlas@1/world/50m.json')
      .defer(d3.csv, 'geonames_cities100000.csv')
      .await((error, world110m, world50m, cities) => {
        const countries110m = topojson
          .feature(world110m, world110m.objects.countries);
        const countries50m = topojson
          .feature(world50m, world50m.objects.countries);
      
      
        cities.forEach(d => {
          d.latitude = +d.latitude;
          d.longitude = +d.longitude;
          d.population = +d.population;
        });
      
        rScale.domain([0, d3.max(cities, rValue)]);
      
        cities.forEach(d => {
          d.radius = rScale(rValue(d));
        });
      
        const render = () => {
          
          // Render low resolution boundaries when moving,
          // render high resolution boundaries when stopped.
          path.attr('d', geoPath(moving ? countries110m : countries50m));
          
          const point = {
            type: 'Point',
            coordinates: [0, 0]
          };
          cities.forEach(d => {
            point.coordinates[0] = d.longitude;
            point.coordinates[1] = d.latitude;
            d.projected = geoPath(point) ? projection(point.coordinates) : null;
          });
          
          const k = Math.sqrt(projection.scale() / 200);
          const circles = citiesG.selectAll('circle')
            .data(cities.filter(d => d.projected));
          circles.enter().append('circle')
            .merge(circles)
              .attr('cx', d => d.projected[0])
              .attr('cy', d => d.projected[1])
              .attr('fill', 'steelblue')
              .attr('fill-opacity', 0.4)
              .attr('r', d => d.radius * k)
              .on('mouseover', tip.show)
              .on('mouseout', tip.hide);
          circles.exit().remove();
        };
        render();

        let rotate0, coords0;
        const coords = () => projection.rotate(rotate0)
          .invert([d3.event.x, d3.event.y]);

        svg
          .call(d3.drag()
            .on('start', () => {
              rotate0 = projection.rotate();
              coords0 = coords();
              moving = true;
            })
            .on('drag', () => {
              const coords1 = coords();
              projection.rotate([
                rotate0[0] + coords1[0] - coords0[0],
                rotate0[1] + coords1[1] - coords0[1],
              ])
              render();
            })
            .on('end', () => {
              moving = false;
              render();
            })
            // Goal: let zoom handle pinch gestures (not working correctly).
            .filter(() => !(d3.event.touches && d3.event.touches.length === 2))
          )
          .call(d3.zoom()
            .on('zoom', () => {
              projection.scale(initialScale * d3.event.transform.k);
              render();
            })
            .on('start', () => {
              moving = true;
            })
            .on('end', () => {
              moving = false;
              render();
            })
          )
      });
  </script>
</body>