block by bycoffe 8672107

Force layout with collision detection, sorting and variable-radius circles

Full Screen

index.html

<!doctype html>
<meta charset="utf-8">
<html>
  <head>

    <style type="text/css">
      #canvas {
        width: 900px;
        height: 500px;
      }
      #canvas text {
        text-anchor: middle;
        font-family: Arial, sans-serif;
        font-size: 12px;
      }
    </style>


  </head>

  <body>

    <div class="buttons">
      Sort by:
      <button value="positive">Least to greatest</button>
      <button value="negative">Greatest to least</button>
      <button value="color">Color</button>
    </div>

    <div id="canvas"></div>

    <script src="//d3js.org/d3.v3.min.js"></script>
    <script src="main.js"></script>

  </body>
</html>

main.js

;(function() {

  var g,
      colorId,
      numNodes = 50,
      nodes = [],
      width = 900,
      height = 500,
      padding = 10,
      minR = 5,
      maxR = 40,
      position = "positive",
      scales = {
        x: d3.scale.linear()
                  .domain([0, numNodes])
                  .range([((width/12.5)-10)*-1, (width-20)/12.5]),
        colorX: d3.scale.linear()
                  .domain([0, 10])
                  .range([((width/12.5)-10)*-1, (width-20)/12.5]),
        y: d3.scale.linear()
                  .domain([0, numNodes])
                  .range([(height/25)*-1, height/25]),
        r: d3.scale.sqrt()
                  .domain([1, 10])
                  .range([minR, maxR])
      };

  function randomRadius() {
    return scales.r(Math.floor(Math.random() * 10) + 1);
  }

  for (i=0; i<numNodes; i++) {
    colorId = Math.random() * 10;
    nodes.push({
      id: i,
      r: randomRadius(),
      cx: scales.x(i),
      cy: scales.y(Math.floor(Math.random()*numNodes)),
      colorId: colorId,
      color: d3.scale.category10().range()[Math.floor(colorId)]
    });
  }

  var svg = d3.select("#canvas").append("svg")
                .attr({
                  width: width,
                  height: height
                }),

      force = d3.layout.force()
                    .nodes(nodes)
                    .links([])
                    .size([width, height])
                    .charge(function(d) {
                      return -1 * (Math.pow(d.r * 5.0, 2.0) / 8);
                    })
                    .gravity(2.75)
                    .on("tick", tick);

  function update(nodes) {
    g = svg.selectAll("g.node")
              .data(nodes, function(d, i) {
                return d.id;
              });

    g.enter().append("g")
                .attr({
                  "class": "node"
                });

    if (g.selectAll("circle").empty()) {
      circle = g.append("circle")
                  .attr({
                    r: function(d) {
                      return d.r;
                    }
                  })
                  .style({
                    fill: function(d) {
                      return d.color;
                    }
                  });

      label = g.append("text")
                  .attr({
                    x: 0,
                    y: 3,
                  })
                  .text(function(d) {
                    return d.id;
                  });

    } else {
      circle.transition()
          .duration(1000)
          .attr({
            r: function(d) {
              return d.r;
            }
          })
    }

    g.exit().remove();

    force.nodes(nodes).start();
  }

  // Adapted from http://bl.ocks.org/3116713
  function collide(alpha, nodes, scale) {
    quadtree = d3.geom.quadtree(nodes);
    return function(d) {
      r = d.r + scale.domain()[1] + padding
      nx1 = d.x - r;
      nx2 = d.x + r;
      ny1 = d.y - r;
      ny2 = d.y + r;
      return quadtree.visit(function(quad, x1, y1, x2, y2) {
        var l, x, y;
        if (quad.point && quad.point !== d) {
          x = d.x - quad.point.x;
          y = d.y - quad.point.y;
          l = Math.sqrt(x * x + y * y);
          r = d.r + quad.point.r + padding;
          if (l < r) {
            l = (l - r) / l * alpha;
            d.x -= x *= l;
            d.y -= y *= l;
            quad.point.x += x;
            quad.point.y += y;
          }
        }
        return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
      });
    };
  };

  // See https://github.com/mbostock/d3/wiki/Force-Layout
  function tick(e) {
    k = 10 * e.alpha;
    nodes.forEach(function(d, i) {
      d.x += k * d.cx;
      d.y += k * d.cy;
    });

    g.each(collide(.1, nodes, scales.r))
      .attr({
        transform: function(d, i) {
          return "translate(" + d.x + "," + d.y + ")";
        }
      });
  };

  update(nodes);

  d3.selectAll("button").on("click", function() {
    var sort = this.getAttribute("value");
    nodes.forEach(function(node) {
      if (sort === "positive") {
        node.cx = scales.x(node.id);
      }
      else if (sort === "negative") {
        node.cx = scales.x(numNodes-node.id);
      }
      else if (sort === "color") {
        node.cx = scales.colorX(node.colorId);
      }
      node.r = randomRadius();
    });
    update(nodes);
  });

}());