block by fil 17fc857c3ce36bf8e21ddefab8bc9af4

Weighted Centroid of the world largest cities

Full Screen

1692 largest cities in the world, weighted by their population sizes. Where does the centroid lie on the surface of the Earth?

d3.geoCentroid() doesn’t account for different weights on Points. Here the spherical centroid of the largest cities in the world, both not-weighted (in blue), and weighted by their populations (in salmon).

Original work by Philippe Rivière for Visionscarto.net. Comments and variants very welcome!

Using d3-annotation() and d3-voronoi find(). Double-click to enter edit mode.

For performance, the map itself is drawn on a <canvas> element; the annotations are displayed on a SVG overlay.

index.html

<!DOCTYPE html>

<head>
    <meta charset="utf-8">
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <script src="https://raw.githack.com/susielu/d3-annotation/3f126d6b/d3-annotation.min.js"></script>

    <style>
        body {
            margin: 0;
            position: fixed;
            top: 0;
            right: 0;
            bottom: 0;
            left: 0;
          font-family: "Lucida Grande", "Arial", "Helvetica", sans-serif;
          font-size: 12px;
        }

     .panel {
        position: absolute;
        top: 0;
        left: 0;
     }

      .annotation path {
        fill: none;
        stroke: #ff8684;
        stroke-width: 2
      }

      .annotation text {
        fill: #ff8684;
      }
      .annotation tspan {
      }

      .annotation-note-title {
        font-weight: bold;
      }
      .annotation-note-bg {
        fill: #111144;
        fill-opacity: 0.9;
        filter: url(#blur-effect);
      }


       circle.handle {
        stroke-dasharray: 5;
        stroke: #e91e56;
        fill: rgba(255, 255, 255, .5);
        cursor: move;

        stroke-opacity: .4;
      }

      circle.handle.highlight {
        stroke-opacity: 1;
      }

      .annotation-tip .annotation path {
        stroke: white;
      }
      .annotation-tip .annotation text {
        fill: white;
      }

    </style></head>

<body>
<script>

  function weightedCentroid(data){
    let X0 = Y0 = Z0 = W0 = 0;
    const radians = Math.PI / 180;

    function centroidPoint(lambda, phi, w) {
      lambda *= radians, phi *= radians;
      var cosPhi = Math.cos(phi);
      centroidPointCartesian(cosPhi * Math.cos(lambda), cosPhi * Math.sin(lambda), Math.sin(phi), w);
    }
    
    function centroidPointCartesian(x, y, z, w) {
      W0 += +w;
      if (!w || !W0) return;
      w /= W0;
      X0 += (x - X0) * w;
      Y0 += (y - Y0) * w;
      Z0 += (z - Z0) * w;
    }

    data.map(d => centroidPoint(...d));
    
    var x = X0,
      y = Y0,
      z = Z0,
      m = x * x + y * y + z * z;
    return [Math.atan2(y, x) / radians, Math.asin(z / Math.sqrt(m)) / radians];
    
  }
  
  
    const width = 960,
        height = 500,
        margin = 40,
        scalepop = d3.scaleSqrt().domain([0, 100000]).range([0.2, 24]),
        scalecountry = d3.scaleOrdinal(d3.schemeCategory20b),
        projection = d3.geoEquirectangular().rotate([-10.5,0]);

    d3.csv('cities.csv', function (cities) {

        const data = cities
            .sort((a, b) => d3.descending(+a[2015], +b[2015]))
            .map((d, i) => [+d.Longitude, +d.Latitude, +d[2015], +d['Country Code'], d['Urban Agglomeration']]);


        const canvas = d3.select("body").append("canvas")
            .attr("width", width)
            .attr("height", height)
            .attr("class", "panel"),
              context = canvas.node().getContext("2d");

  // retina display
  var devicePixelRatio = window.devicePixelRatio || 1;
  canvas.style('width', canvas.attr('width')+'px');
  canvas.style('height', canvas.attr('height')+'px');
  canvas.attr('width', canvas.attr('width') * devicePixelRatio);
  canvas.attr('height', canvas.attr('height') * devicePixelRatio);
  context.scale(devicePixelRatio,devicePixelRatio);

        const svg = d3.select("body").append("svg")
            .attr("width", width)
            .attr("height", height)
            .attr("class", "panel");

        // this almost invisible rect allows our svg to receive mousemove events
        svg.append('rect')
            .attr("width", width)
            .attr("height", height)
            .attr("fill", 'rgba(0,0,0,0.01)');

      svg.append('defs')
      .append('filter')
      .attr('id', 'blur-effect')
      .append('feGaussianBlur')
      .attr('stdDeviation', 4);


      const nodes = data.map(d => {
          let p = projection(d);
          d.x = p[0];
          d.y = p[1];
          d.r = scalepop(d[2]);
          d.color = 'rgba(240,255,240,0.4)';
          d.name = d[4];
          return d;
        });

      var gcities = context; //svg.append('g');
      
      drawcanvas(gcities, nodes);

      // draw the unweighted geoCentroid
      const centroid = d3.geoCentroid({
          type: "MultiPoint",
          coordinates: data.map(d => [d[0], d[1]])
        });
        let p1 = projection(centroid);
        centroid.r = 6;
        drawsvg(svg.append('g'), [{
          x: p1[0],
          y: p1[1],
          r: centroid.r,
          color: '#8e84ff',
          stroke: 'black',
        }]);

      // draw the *weighted* geoCentroid
      const wcentroid = weightedCentroid(data.map(d => [d[0], d[1], d[2]]));
        let p2 = projection(wcentroid);
        wcentroid.r = 12;
        let wcentroids = [{
          x: p2[0],
          y: p2[1],
          r: wcentroid.r,
          data: wcentroid,
          color: '#ff8684',
          stroke: 'black',
        }];
        drawsvg(svg.append('g'), wcentroids);
        
        centroid.name = "Centroid";
        centroid.dx = -65;
        centroid.dy = -40;
        wcentroid.name = "Weighted Centroid";
        wcentroid.dx = 90;
        wcentroid.dy = -30;


        function drawsvg (g, nodes) {
          g
          .selectAll('circle')
          .data(nodes)
          .enter()
          .append('circle')
          .attr('r', d => d.r)
          .attr('cx', d => d.x)
          .attr('cy', d => d.y)
          .attr('fill', d => d.color)
          .attr('stroke', d => d.stroke || 'none');
        }


      
        function drawcanvas(context, nodes) {
          context.fillStyle = "#130c30";
          context.fillRect(0,0,width,height)
    
            for (var i = 0, n = nodes.length; i < n; ++i) {
                var node = nodes[i];
                context.beginPath();
                context.moveTo(node.x, node.y);
                context.arc(node.x, node.y, node.r, 0, 2 * Math.PI);
                context.lineWidth = 8;
                if (node.stroke){
                  context.color = node.stroke;
                  context.stroke();
                }
                context.fillStyle = node.color;
                context.fill();
            }
        }

      annotation = d3.annotation()
        .type(d3.annotationCalloutCircle)
        .annotations([centroid, wcentroid]
        .map(d => {
          return {
            data: d,
            dx: d.dx || 0,
            dy: d.dy || 0,
            note: {
               title: d.name || "??",
               label: d.map(d3.format('0.2f')).join(', '),
            },
            subject: {
              radius: d.r,
              radiusPadding: 2,
            },
          }
        }))
        .accessors({ x: d => projection(d)[0], y: d => projection(d)[1] })

      svg.append("g")
        .attr("class", "annotation-centroids")
        .call(annotation)
        .on('dblclick', function() {
          annotation.editMode(!annotation.editMode()).update();
        });

      
      // create a container for tooltips
      tipg = svg.append("g")
        .attr("class", "annotation-tip");

      // this function will call d3.annotation when a tooltip has to be drawn
      function tip (d) {
        annotationtip = d3.annotation()
        .type(d3.annotationCalloutCircle)
        .annotations([d].map(d => {
          return {
            data: d,
            dx: d.dx || (d.x > 450) ? -50 : 50,
            dy: d.dy || (d.y > 240) ? -10 : 10,
            note: {
               label: d.name || "??",
            },
            subject: {
              radius: d.r,
              radiusPadding: 2,
            },
          };
        }))
        .accessors({ x: d => projection(d)[0], y: d => projection(d)[1] })
        tipg.call(annotationtip);
      }

      // use voronoi.find() on mousemove to decide what tooltip to display
      let voronoi = null;
      svg.on('mousemove', function() {
         if (!voronoi) voronoi = d3.voronoi().x(d => d.x).y(d => d.y)(nodes);
         let m = d3.mouse(this);
         let f = voronoi.find(m[0], m[1], 15 /* voronoi radius */);
         if (f) {
           tip(f.data);
         } else {
           tipg.selectAll("g").remove();
         }
      });

      
    });
</script>
</body>