block by emeeks 625641430adead4bd7dbc9c1ab3f5102

Network Annotation with Collision Detection

Full Screen

Using collision detection with network visualization labels

This demonstrates how to use d3-annotation() with bboxCollide to procedurally place node labels. After using the nodes data to create a network visualization of the Les Miserables play, we filter the nodes to leave out the side characters and pass that array to d3-annotation. We then create a second forceSimulation, this time using the size of the notes as the property in our bounding box collision detection, to move the labels out of each others’ way.

d3-annotation by Susie Lu.

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <link href='https://fonts.googleapis.com/css?family=Lato:300,900' rel='stylesheet' type='text/css'>

    <style>

    :root {
      --annotation-color: #e91e56;
    }
     body{
        background-color: whitesmoke;
     }

     svg {
        background-color: white;
        font-family: 'Lato';
        overflow: visible;
     }

     line {
       stroke:#e3e3e3;
     }

     .editable .annotation-subject, .editable .annotation-textbox {
       cursor: move;
     }

     .line {
        fill: none;
        stroke: black;
        stroke-width: 1px;
      }

      .annotation path {
        stroke: var(--annotation-color);
        fill: rgba(0,0,0,0);
      }

      .annotation path.connector-arrow{
        fill: var(--annotation-color);
      }

      .annotation text {
        fill: var(--annotation-color);
      }

      .annotation-title {
        font-weight: bold;
      }

      .annotation .annotation-subject circle.handle {
        display: none;
      }

      .annotation-note-bg {
        fill: rgba(255, 255, 255, 0);
      }

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

        stroke-opacity: .4;
      }

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

      .annotation.major {
        font-weight: 900;
        font-size: 1em;
      }

      .annotation-note-label tspan {
        text-anchor: middle;
      }

    </style>
</head>
<body>
    <svg width=1000 height=650></svg>
    <script src="https://d3js.org/d3.v4.js"></script>
    <script src="bboxCollide.js"></script>
    <script src="https://cdn.rawgit.com/susielu/d3-annotation/master/d3-annotation.js"></script> 

    <script>

    var svg = d3.select("svg"),
        width = +svg.attr("width"),
        height = +svg.attr("height");
        
    var color = d3.scaleOrdinal(d3.schemeCategory20)
      .range(["#e91e56", "#00965f", "#00bcd4", "#3f51b5", "#9c27b0", "#ff5722", "#cddc39", "#607d8b", "#8bc34a"]);
    var simulation = d3.forceSimulation()
        .force("link", d3.forceLink().id( d => d.id ))
        .force("charge", d3.forceManyBody().strength(-80))
        .force("center", d3.forceCenter(width / 2, height / 2));
    d3.json("miserables.json", function(error, graph) {

      if (error) throw error;
      var link = svg.append("g")
          .attr("class", "links")
        .selectAll("line")
        .data(graph.links)
        .enter().append("line")
          .attr("stroke-width", d => Math.sqrt(d.value));

      var node = svg.append("g")
          .attr("class", "nodes")
        .selectAll("circle")
        .data(graph.nodes)
        .enter().append("circle")
          .attr("r", d => d.type === "major" ? 9 : 3)
          .style("fill", d => d3.hsl(color(d.group)).darker())
          .style("fill-opacity", d => d.type === "other" ? 0.5 : 1)

      node.append("title")
          .text(d => d.id);

      window.collide = d3.bboxCollide((a) => {
        return [[a.offsetCornerX - 5, a.offsetCornerY - 10],[a.offsetCornerX + a.width + 5, a.offsetCornerY + a.height+ 5]]
      })
     .strength(0.5)
     .iterations(1)

      window.yScale = d3.scaleLinear()

      simulation
          .nodes(graph.nodes)
          .on("tick", ticked)
          .on("end", function() {

            const noteBoxes = makeAnnotations.collection().noteNodes

             window.labelForce = d3.forceSimulation(noteBoxes)
              .force("x", d3.forceX(a => a.positionX).strength(a => Math.max(0.25, Math.min(3, Math.abs(a.x - a.positionX) / 20))))
              .force("y", d3.forceY(a => a.positionY).strength(a => Math.max(0.25, Math.min(3, Math.abs(a.x - a.positionX) / 20))))
             .force("collision", window.collide)
              .alpha(0.5)
              .on('tick', d => {
                  makeAnnotations.annotations()
                  .forEach((d, i) => {
                    const match = noteBoxes[i]	
                          d.dx = match.x - match.positionX
                          d.dy = match.y - match.positionY
                  })
                
                  makeAnnotations.update()
              })
            
          })
      const nonOtherNodes = graph.nodes
        .filter(d => d.type !== "other")

      simulation.force("link")
          .links(graph.links);
      function ticked() {
        link
            .attr("x1", d => d.source.x)
            .attr("y1", d => d.source.y)
            .attr("x2", d => d.target.x)
            .attr("y2", d => d.target.y);
        node
            .attr("cx", d => d.x)
            .attr("cy", d => d.y);

        makeAnnotations.annotations()
          .forEach((d, i) => {
            d.position = nonOtherNodes[i]
          })
      }

      window.makeAnnotations = d3.annotation()
        .type(d3.annotationLabel)
        .annotations(nonOtherNodes
        .map((d,i) => {
          return {
            data: {x: d.x, y: d.y, group: d.group},
            note: { label: d.id,
              align: "middle",
              orientation: "fixed" },
            connector: { type: "elbow"},
            className: d.type
          }
        }))
        .accessors({ x: d => d.x , y: d => d.y})

      svg.append("g")
        .attr("class", "annotation-test")
        .call(makeAnnotations)

      svg.selectAll(".annotation-note text")
        .style("fill", d => color(d.data.group))

      svg.selectAll(".annotation-connector > path")
        .style("stroke", (d,i) => color(d.data.group))
      
    });
    </script>
</body>
</html>

bboxCollide.js

(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-quadtree')) :
  typeof define === 'function' && define.amd ? define(['exports', 'd3-quadtree'], factory) :
  (factory((global.d3 = global.d3 || {}),global.d3));
}(this, function (exports,d3Quadtree) { 'use strict';

  function bboxCollide (bbox) {

    function x (d) {
      return d.x + d.vx;
    }

    function y (d) {
      return d.y + d.vy;
    }

    function constant (x) {
      return function () {
        return x;
      };
    }

    var nodes,
        boundingBoxes,
        strength = 1,
        iterations = 1;

        if (typeof bbox !== "function") {
          bbox = constant(bbox === null ? [[0,0][1,1]] : bbox)
        }

        function force () {
          var i,
              tree,
              node,
              xi,
              yi,
              bbi,
              nx1,
              ny1,
              nx2,
              ny2

              var cornerNodes = []
              nodes.forEach(function (d, i) {
                cornerNodes.push({node: d, vx: d.vx, vy: d.vy, x: d.x + (boundingBoxes[i][1][0] + boundingBoxes[i][0][0]) / 2, y: d.y + (boundingBoxes[i][0][1] + boundingBoxes[i][1][1]) / 2})
                cornerNodes.push({node: d, vx: d.vx, vy: d.vy, x: d.x + boundingBoxes[i][0][0], y: d.y + boundingBoxes[i][0][1]})
                cornerNodes.push({node: d, vx: d.vx, vy: d.vy, x: d.x + boundingBoxes[i][0][0], y: d.y + boundingBoxes[i][1][1]})
                cornerNodes.push({node: d, vx: d.vx, vy: d.vy, x: d.x + boundingBoxes[i][1][0], y: d.y + boundingBoxes[i][0][1]})
                cornerNodes.push({node: d, vx: d.vx, vy: d.vy, x: d.x + boundingBoxes[i][1][0], y: d.y + boundingBoxes[i][1][1]})
              })
              var cn = cornerNodes.length

          for (var k = 0; k < iterations; ++k) {
            tree = d3Quadtree.quadtree(cornerNodes, x, y).visitAfter(prepareCorners);

            for (i = 0; i < cn; ++i) {
              var nodeI = ~~(i / 5);
              node = nodes[nodeI]
              bbi = boundingBoxes[nodeI]
              xi = node.x + node.vx
              yi = node.y + node.vy
              nx1 = xi + bbi[0][0]
              ny1 = yi + bbi[0][1]
              nx2 = xi + bbi[1][0]
              ny2 = yi + bbi[1][1]
              tree.visit(apply);
            }
          }

          function apply (quad, x0, y0, x1, y1) {
              var data = quad.data
              if (data) {
                var bWidth = bbLength(bbi, 0),
                bHeight = bbLength(bbi, 1);

                if (data.node.index !== nodeI) {
                  var dataNode = data.node
                  var bbj = boundingBoxes[dataNode.index],
                    dnx1 = dataNode.x + dataNode.vx + bbj[0][0],
                    dny1 = dataNode.y + dataNode.vy + bbj[0][1],
                    dnx2 = dataNode.x + dataNode.vx + bbj[1][0],
                    dny2 = dataNode.y + dataNode.vy + bbj[1][1],
                    dWidth = bbLength(bbj, 0),
                    dHeight = bbLength(bbj, 1)

                  if (nx1 <= dnx2 && dnx1 <= nx2 && ny1 <= dny2 && dny1 <= ny2) {

                    var xSize = [Math.min.apply(null, [dnx1, dnx2, nx1, nx2]), Math.max.apply(null, [dnx1, dnx2, nx1, nx2])]
                    var ySize = [Math.min.apply(null, [dny1, dny2, ny1, ny2]), Math.max.apply(null, [dny1, dny2, ny1, ny2])]

                    var xOverlap = bWidth + dWidth - (xSize[1] - xSize[0])
                    var yOverlap = bHeight + dHeight - (ySize[1] - ySize[0])

                    var xBPush = xOverlap * strength * (yOverlap / bHeight)
                    var yBPush = yOverlap * strength * (xOverlap / bWidth)

                    var xDPush = xOverlap * strength * (yOverlap / dHeight)
                    var yDPush = yOverlap * strength * (xOverlap / dWidth)

                    if ((nx1 + nx2) / 2 < (dnx1 + dnx2) / 2) {
                      node.vx -= xBPush
                      dataNode.vx += xDPush
                    }
                    else {
                      node.vx += xBPush
                      dataNode.vx -= xDPush
                    }
                    if ((ny1 + ny2) / 2 < (dny1 + dny2) / 2) {
                      node.vy -= yBPush
                      dataNode.vy += yDPush
                    }
                    else {
                      node.vy += yBPush
                      dataNode.vy -= yDPush
                    }
                  }

                }
                return;
              }

              return x0 > nx2 || x1 < nx1 || y0 > ny2 || y1 < ny1;
          }

        }

        function prepareCorners (quad) {

          if (quad.data) {
            return quad.bb = boundingBoxes[quad.data.node.index]
          }
            quad.bb = [[0,0],[0,0]]
            for (var i = 0; i < 4; ++i) {
              if (quad[i] && quad[i].bb[0][0] < quad.bb[0][0]) {
                quad.bb[0][0] = quad[i].bb[0][0]
              }
              if (quad[i] && quad[i].bb[0][1] < quad.bb[0][1]) {
                quad.bb[0][1] = quad[i].bb[0][1]
              }
              if (quad[i] && quad[i].bb[1][0] > quad.bb[1][0]) {
                quad.bb[1][0] = quad[i].bb[1][0]
              }
              if (quad[i] && quad[i].bb[1][1] > quad.bb[1][1]) {
                quad.bb[1][1] = quad[i].bb[1][1]
              }
          }
        }

        function bbLength (bbox, heightWidth) {
          return bbox[1][heightWidth] - bbox[0][heightWidth]
        }

        force.initialize = function (_) {
          var i, n = (nodes = _).length; boundingBoxes = new Array(n);
          for (i = 0; i < n; ++i) boundingBoxes[i] = bbox(nodes[i], i, nodes);
        };

        force.iterations = function (_) {
          return arguments.length ? (iterations = +_, force) : iterations;
        };

        force.strength = function (_) {
          return arguments.length ? (strength = +_, force) : strength;
        };

        force.bbox = function (_) {
          return arguments.length ? (bbox = typeof _ === "function" ? _ : constant(+_), force) : bbox;
        };

        return force;
  }

  exports.bboxCollide = bboxCollide;

  Object.defineProperty(exports, '__esModule', { value: true });

}));