block by renecnielsen 5796a4d304f61974d49e7ef04b5f04fc

annotate a map (canvas + svg)

Full Screen

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

forked from Fil‘s block: annotate a map

forked from Fil‘s block: annotate a map (canvas + svg)

index.html

<!DOCTYPE html>

<head>
    <meta charset="utf-8">
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <script src="d3-annotation.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");

        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 = canvas; //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(canvas, nodes) {
          let context = canvas.node().getContext("2d");
          context.fillStyle = "#111144";
          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.html('').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>