Skip to content

Instantly share code, notes, and snippets.

@hugolpz
Forked from ericcoopey/README.md
Created April 28, 2017 16:10
Show Gist options
  • Save hugolpz/cd89b50d834828261b45af9e7a5ce926 to your computer and use it in GitHub Desktop.
Save hugolpz/cd89b50d834828261b45af9e7a5ce926 to your computer and use it in GitHub Desktop.
Animating Changes in Force Diagram

Demonstrates how to dynamically add and remove nodes to a force diagram. This was functionality that should seemingly be easy, but was surprisingly hard. Many people were asking how to do this in forums, and this final product is the amalgamation of several commenters half baked (but not really functioning) solutions.

This one demonstrates changes in a social network diagram as relationships are added, removed, and updated. Add nodes by name, add links complete with custom lengths.

Also shows how to add labels to a force diagram.

<!DOCTYPE html>
<html>
<head>
<title>Animating Changes in Force Diagram</title>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<style>
.link {
stroke: #2E2E2E;
stroke-width: 2px;
}
.node {
stroke: #fff;
stroke-width: 2px;
}
.textClass {
stroke: #323232;
font-family: "Lucida Grande", "Droid Sans", Arial, Helvetica, sans-serif;
font-weight: normal;
stroke-width: .5;
font-size: 14px;
}
</style>
</head>
<body>
<button onclick="addNodes()">Restart Animation</button>
<script>
var graph;
function myGraph() {
// Add and remove elements on the graph object
this.addNode = function (id) {
nodes.push({"id": id});
update();
};
this.removeNode = function (id) {
var i = 0;
var n = findNode(id);
while (i < links.length) {
if ((links[i]['source'] == n) || (links[i]['target'] == n)) {
links.splice(i, 1);
}
else i++;
}
nodes.splice(findNodeIndex(id), 1);
update();
};
this.removeLink = function (source, target) {
for (var i = 0; i < links.length; i++) {
if (links[i].source.id == source && links[i].target.id == target) {
links.splice(i, 1);
break;
}
}
update();
};
this.removeallLinks = function () {
links.splice(0, links.length);
update();
};
this.removeAllNodes = function () {
nodes.splice(0, links.length);
update();
};
this.addLink = function (source, target, value) {
links.push({"source": findNode(source), "target": findNode(target), "value": value});
update();
};
var findNode = function (id) {
for (var i in nodes) {
if (nodes[i]["id"] === id) return nodes[i];
}
;
};
var findNodeIndex = function (id) {
for (var i = 0; i < nodes.length; i++) {
if (nodes[i].id == id) {
return i;
}
}
;
};
// set up the D3 visualisation in the specified element
var w = 960,
h = 450;
var color = d3.scale.category10();
var vis = d3.select("body")
.append("svg:svg")
.attr("width", w)
.attr("height", h)
.attr("id", "svg")
.attr("pointer-events", "all")
.attr("viewBox", "0 0 " + w + " " + h)
.attr("perserveAspectRatio", "xMinYMid")
.append('svg:g');
var force = d3.layout.force();
var nodes = force.nodes(),
links = force.links();
var update = function () {
var link = vis.selectAll("line")
.data(links, function (d) {
return d.source.id + "-" + d.target.id;
});
link.enter().append("line")
.attr("id", function (d) {
return d.source.id + "-" + d.target.id;
})
.attr("stroke-width", function (d) {
return d.value / 10;
})
.attr("class", "link");
link.append("title")
.text(function (d) {
return d.value;
});
link.exit().remove();
var node = vis.selectAll("g.node")
.data(nodes, function (d) {
return d.id;
});
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.call(force.drag);
nodeEnter.append("svg:circle")
.attr("r", 12)
.attr("id", function (d) {
return "Node;" + d.id;
})
.attr("class", "nodeStrokeClass")
.attr("fill", function(d) { return color(d.id); });
nodeEnter.append("svg:text")
.attr("class", "textClass")
.attr("x", 14)
.attr("y", ".31em")
.text(function (d) {
return d.id;
});
node.exit().remove();
force.on("tick", function () {
node.attr("transform", function (d) {
return "translate(" + d.x + "," + 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;
});
});
// Restart the force layout.
force
.gravity(.01)
.charge(-80000)
.friction(0)
.linkDistance( function(d) { return d.value * 10 } )
.size([w, h])
.start();
};
// Make it all go
update();
}
function drawGraph() {
graph = new myGraph("#svgdiv");
graph.addNode('Sophia');
graph.addNode('Daniel');
graph.addNode('Ryan');
graph.addNode('Lila');
graph.addNode('Suzie');
graph.addNode('Riley');
graph.addNode('Grace');
graph.addNode('Dylan');
graph.addNode('Mason');
graph.addNode('Emma');
graph.addNode('Alex');
graph.addLink('Alex', 'Ryan', '20');
graph.addLink('Sophia', 'Ryan', '20');
graph.addLink('Daniel', 'Ryan', '20');
graph.addLink('Ryan', 'Lila', '30');
graph.addLink('Lila', 'Suzie', '20');
graph.addLink('Suzie', 'Riley', '10');
graph.addLink('Suzie', 'Grace', '30');
graph.addLink('Grace', 'Dylan', '10');
graph.addLink('Dylan', 'Mason', '20');
graph.addLink('Dylan', 'Emma', '20');
graph.addLink('Emma', 'Mason', '10');
keepNodesOnTop();
// callback for the changes in the network
var step = -1;
function nextval()
{
step++;
return 2000 + (1500*step); // initial time, wait time
}
setTimeout(function() {
graph.addLink('Alex', 'Sophia', '20');
keepNodesOnTop();
}, nextval());
setTimeout(function() {
graph.addLink('Sophia', 'Daniel', '20');
keepNodesOnTop();
}, nextval());
setTimeout(function() {
graph.addLink('Daniel', 'Alex', '20');
keepNodesOnTop();
}, nextval());
setTimeout(function() {
graph.addLink('Suzie', 'Daniel', '30');
keepNodesOnTop();
}, nextval());
setTimeout(function() {
graph.removeLink('Dylan', 'Mason');
graph.addLink('Dylan', 'Mason', '8');
keepNodesOnTop();
}, nextval());
setTimeout(function() {
graph.removeLink('Dylan', 'Emma');
graph.addLink('Dylan', 'Emma', '8');
keepNodesOnTop();
}, nextval());
}
drawGraph();
// because of the way the network is created, nodes are created first, and links second,
// so the lines were on top of the nodes, this just reorders the DOM to put the svg:g on top
function keepNodesOnTop() {
$(".nodeStrokeClass").each(function( index ) {
var gnode = this.parentNode;
gnode.parentNode.appendChild(gnode);
});
}
function addNodes() {
d3.select("svg")
.remove();
drawGraph();
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment