Posted on

Forced Graph with 3D.js

cover-forced-graph

A very simple way to create a forced graph with 3D.js. There are two nodes and they are connected – yessss.

Here a more complex example with multiple nodes. The nice part over here is the grouping of nodes and especially that you can have nodes in multiple groups.

Just for reference the code:

var width = 1640,
    height = 1480;
var maxDistance = 200;

var minNodeSize = 1;
function radiusOf(element) {
    return (minNodeSize + (Math.sqrt(element.size))); 
}

var groupHullFill = '#ff0000';

var groupPath = function(d) {
    var fakePoints = [];     
    d.forEach(function(element) { fakePoints = fakePoints.concat([   // "0.7071" is the sine and cosine of 45 degree for corner points.
           [(element.x), (element.y + (radiusOf(element) - minNodeSize))],
           [(element.x + (0.7071 * (radiusOf(element) - minNodeSize))), (element.y + (0.7071 * (radiusOf(element) - minNodeSize)))],
           [(element.x + (radiusOf(element) - minNodeSize)), (element.y)],
           [(element.x + (0.7071 * (radiusOf(element) - minNodeSize))), (element.y - (0.7071 * (radiusOf(element) - minNodeSize)))],
           [(element.x), (element.y - (radiusOf(element) - minNodeSize))],
           [(element.x - (0.7071 * (radiusOf(element) - minNodeSize))), (element.y - (0.7071 * (radiusOf(element) - minNodeSize)))],
           [(element.x - (radiusOf(element) - minNodeSize)), (element.y)],
           [(element.x - (0.7071 * (radiusOf(element) - minNodeSize))), (element.y + (0.7071 * (radiusOf(element) - minNodeSize)))]
    ]); })
    return "M" + d3.geom.hull( fakePoints ).join("L") + "Z";
};

var graph = {
    "nodes": [
        { x: 300, y: 300, size: 200, "name": "0" },
        { x: 400, y: 400, size: 30, "name": "1" },
        { x: 200, y: 200, size: 50, "name": "2" },
        { x: 100, y: 200, size: 100, "name": "3" },
        { x: 400, y: 400, size: 100, "name": "4" },
        { x: 100, y: 200, size: 100, "name": "5" },
        { x: 150, y: 200, size: 100, "name": "6" },
        { x: 100, y: 250, size: 100, "name": "7" },
        { x: 250, y: 350, size: 100, "name": "8" },
        { x: 250, y: 350, size: 100, "name": "9" },
        { x: 250, y: 350, size: 100, "name": "10" }
    ],
    "links" : [
        { source: 0, target: 1, value: 10 },
        { source: 0, target: 2, value: 150 },
        { source: 0, target: 3, value: 20 },
        { source: 0, target: 4, value: 110 },
        { source: 0, target: 5, value: 5 },
        { source: 0, target: 6, value: 70 },
        { source: 0, target: 7, value: 80 },
        { source: 0, target: 8, value: 90 },
        { source: 0, target: 9, value: 90 },
        { source: 0, target: 10, value: 90 }
    ]
};
    
var groups = [[0,2,4],[0,2,3,4,5,6,7,8,9,10]];
var groupSize = [3, 5];

var groupNodes = groups.map(function(group,index){
    	return group.map(function(member){return graph.nodes[member] });
	});
  
var nodes = graph.nodes,
    links = graph.links;
    
var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height);
    
var force = d3.layout.force()
    .size([width, height])
    .nodes(nodes)
    .links(links)
    .linkDistance( function (thisLink) {
        var customLen = 100,
            theSource = thisLink.source, 
            theTarget = thisLink.target,
            customLen;
        customLen = maxDistance - thisLink.value;
        return customLen;
     })
    .charge(-1000)
    .friction(0.5);

var link = svg.selectAll('.link')
    .data(links)
    .enter().append('line')
    .attr('class', 'link')
    .attr('stroke-width', function(d) { return Math.sqrt(d.value); } );
    
var node = svg.selectAll('.node')
    .data(nodes)
    .enter().append('circle')
        .attr('class', 'node')
        .attr('r', function(d) {
            return radiusOf(d);
        })
    .call(force.drag);

node.append("title")
    .text(function(d) { return d.name; });

/*
node.append("text")
    .attr("x", 0)
    .attr("dy", ".35em")
    .attr("text-anchor", "middle")
    .text(function(d) { return d.name; });
*/

force.on('tick', function() {
    // this updates the convex hulls
    svg.selectAll("path").remove();
    svg.selectAll("circle").remove();
    svg.selectAll("line").remove();
            
    var circle = svg.insert("circle")
    .style('fill', '#0000ff')
        .style('opacity', 0.1)
        .attr("cx", nodes[0].x)
        .attr("cy", nodes[0].y)
        .attr("r", function(z) {
            var minDist = maxDistance;
            for (var key in links) {
                if (links.hasOwnProperty(key)) {
                    if (links[key].value < minDist) {
                        minDist = links[key].value ;
                    }
                }
            
            }
            
            return maxDistance-minDist;
        })
        .attr('id', "rad")
        .attr('title', function(z) {
            var maxSize=0;
            for (var key in links) {
                if (links.hasOwnProperty(key)) {
                    if (links[key].value > maxSize) {
                        maxSize = links[key].value ;
                    }
                }
            
            }
            return maxSize;
        })
        ;
        
    svg.selectAll("path#group")
      .data(groupNodes)
        .attr("d", groupPath)
      .enter().insert("path", "circle")
        .style("fill", groupHullFill)
        .style("stroke", groupHullFill)
        .style("stroke-width", function() { return 100; } )
        .style("stroke-linejoin", "round")
        .style("opacity", .1)
    	.attr("ID", function(d) { return "group"+JSON.stringify(d); } )
        .attr("d", groupPath);
                    
    // this redraws the links on top of the convex hulls
    var link = svg.selectAll(".link")
       .data(graph.links)
     .enter().append("line")
       .attr("class", "link")
       .style("stroke-width", function(d) { return Math.sqrt(d.value); });
       
    // this redraws the nodes on top of the links and convex hulls
    var node = svg.selectAll(".node")
      .data(graph.nodes)
    .enter().append("circle")
      .attr("class", "node")
      .attr("r", function(d) { return radiusOf(d); })
      .call(force.drag);
        
    node.attr('cx', function(d) { return d.x; })
        .attr('cy', function(d) { return d.y; });
    link.attr('x1', function(d) { return d.source.x; })
        .attr('y1', function(d) { return d.source.y; })
        .attr('x2', function(d) { return d.target.x; })
        .attr('y2', function(d) { return d.target.y; });
});
    
force.start();

Links

Understanding D3.js Force Layout
Nice example of grouping