block by kenpenn 3a236adf6f44c27f2d4f773ea1f43330

Canvas Force-Directed Graph

Full Screen

The force-directed graph rendered in canvas instead of SVG.

forked from syntagmatic‘s block: Canvas Force-Directed Graph

Graph uses sample server data with 3149 nodes.

An excellent example of what @tonyhschu characterized as “That’s really cool - what is it?”

index.html

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>force-directed graph on canvas</title>
  <style>
    #canvas-box {
      border: 1px solid #e1e2e3;
      margin: 20px auto;
      width: 960px;
      height: 500px;
    }
  </style>
  <!-- total nodes:  3149  -->
</head>
<body style="margin:0">
<div id="canvas-box"></div>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="fd-canvas.js"></script>
</body>
</html>

fd-canvas.js

/*
 * force-directed graph example using canvas
 * simulates displaying server data
 */
(function () {
  "use strict";

  var fd = (function () {
       fd = {
          width   : 960,
          height  : 500,
          canvas  : '',
          ctx     : '',
          color   : d3.scale.category20(),
          init    : function (error, clusterData) {
            var graph, clusterTree;

            if (error) {
              console.log('oops! bad data');
              return;
            }

            clusterTree = fd.toTree(clusterData);
            graph = fd.flatten(clusterTree);

            fd.canvas = d3.select("#canvas-box").append("canvas")
                            .attr("width", fd.width)
                            .attr("height", fd.height)
                            .node();

            fd.render(graph);
          },

          nestHosts : function (instances) {
            // given an array of instances, returns an object with a children array of hosts
            // with props suitable for tree display
            var hosts = { name : 'hosts', children : [] };
            var nest = d3.nest()
                          .key(function(d) { return d.host; })
                          .key(function(d) { return d.port; })
                          .entries(instances);

            nest.forEach(function (mbr) {
              var host = { name : mbr.key, children : [] };
              mbr.values.forEach(function (instance) {
                instance.values[0].name = instance.values[0].host.replace('.somecorp.com','') + ':' +
                                          instance.values[0].port;

                host.children.push(instance.values[0]);
              });
              hosts.children.push(host);
            });

            return hosts;
          },

          nestPods : function (dbs) {
            // given an array of databases, returns an array of pods
            // with props suitable for tree display
            var pods = { name : 'pods', children : [] };
            var nest = d3.nest()
                          .key(function(d) { return d.pod; })
                          .entries(dbs);

            nest.forEach(function (mbr) {
              var pod = { name : mbr.key, children : [] };
              mbr.values.forEach(function (db) {
                db.name = db.pod + ':' + db.label;
                pod.children.push(db);
              });
              pods.children.push(pod);
            });

            return pods;

          },

          flatten   : function (root) {
            var nodes = [];
            var links = [];
            var ndx   = 0;
            var gdx   = 0;
            var buildlink = function(source, target) {
              return { source: source.id, target: target.id, value: source.id };
            };
            var randRange = function (min, max) {
              return parseInt(Math.round(min + Math.random() * (max - min)));
            };

            var buildNode = function(node) {
              var colors = groupColor(node.group)
              var x = randRange(1, fd.width)
              var y = randRange(1, fd.height)
              return { name: node.name, group: node.group, id: ndx, fill: colors.fill,
                       stroke: colors.stroke,  x: x, y: y};
            };
            var groupColor = function(node) {
              var fill = fd.color(node.group);
              var stroke = d3.rgb(fill).darker(0.4).toString();
              return { fill: fill, stroke: stroke };
            };
            var recurse = function (node, parent) {
              var colors;
              node.id = ndx;
              ndx += 1;
              node.group = gdx;

              if (parent) {
                links.push(buildlink(node, parent));
              }
              if (node.children) {
                nodes.push(buildNode(node));
                gdx += 1;
                node.children.forEach(function (child) {
                  recurse(child, node)
                });
              } else {
                colors = groupColor(node);
                node.leaf = true;
                node.fill = colors.fill;
                node.stroke = colors.stroke;
                node.x = randRange(1, fd.width)
                node.y = randRange(1, fd.height)
                nodes.push(node);
              }
            }

            recurse(root);

            console.log('nodes: ' + nodes.length)

            return { nodes: nodes, links: links };
          },

          toTree : function (clusterData) {
            var clusters = { name: 'clusters', children : [] }

            clusterData.forEach(function(clusterDatum) {
              var cluster = { name : clusterDatum.name, children : [] };
              var hosts = fd.nestHosts(clusterDatum.instances);
              var pods  = fd.nestPods(clusterDatum.databases);
              cluster.children.push(hosts);
              cluster.children.push(pods);
              clusters.children.push(cluster);
            });

            return clusters;
          },

          render: function(graph) {
            var ctx = fd.canvas.getContext("2d");
            var fit = Math.sqrt(graph.nodes.length / (fd.width * fd.height));
            var charge = (-1 / fit);
            var gravity = (8 * fit);

            var force = d3.layout.force()
                          .charge(charge)
                          .gravity(gravity)
                          .linkDistance(2)
                          .size([fd.width, fd.height]);

            force.nodes(graph.nodes)
                 .links(graph.links)
                 .start();


            force.on("tick", function() {
              ctx.clearRect(0,0,fd.canvas.width,fd.canvas.height);

              ctx.strokeStyle = "rgba(150,150,150,0.6)";
              ctx.lineWidth = 1;
              ctx.beginPath();
              graph.links.forEach(function(d) {
                ctx.moveTo(d.source.x,d.source.y);
                ctx.lineTo(d.target.x,d.target.y);
              });
              ctx.stroke();

              ctx.lineWidth = 1.5;
              graph.nodes.forEach(function(d) {

                ctx.fillStyle = d.fill;
                ctx.strokeStyle = d.stroke;
                ctx.beginPath();
                ctx.arc(d.x,d.y,5,0,2*Math.PI);
                ctx.fill();
                ctx.stroke();
              });
            });
        }
      };

    return fd;
  }()); // end fd iife

  // kick the whole thing off
  d3.json('clusters.json', fd.init);

}());