Skip to content

Instantly share code, notes, and snippets.

@ericcoopey
Last active April 28, 2017 16:10
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save ericcoopey/6c602d7cb14b25c179a4 to your computer and use it in GitHub Desktop.
Save ericcoopey/6c602d7cb14b25c179a4 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>
@FatManiac
Copy link

Hi, i've modify your code a little bit but i have some links problem, i see the first link that i do but not others, they are present but i can't see them, when i say they are present, that's they are link when i move one the orther move but on consol i see only one link
Thx for what you do, and thx for helping

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment