First question on Stack Overflow, so bear with me! I am new to d3.js, but have been consistently amazed by what others are able to accomplish with it... and almost as amazed
After many long hours of being unable to get this working, I finally stumbled across a demo that I don't think is linked any of the documentation: http://bl.ocks.org/1095795:
This demo contained the keys which finally helped me crack the problem.
Adding multiple objects on an enter()
can be done by assigning the enter()
to a variable, and then appending to that. This makes sense. The second critical part is that the node and link arrays must be based on the force()
-- otherwise the graph and model will go out of synch as nodes are deleted and added.
This is because if a new array is constructed instead, it will lack the following attributes:
- index - the zero-based index of the node within the nodes array.
- x - the x-coordinate of the current node position.
- y - the y-coordinate of the current node position.
- px - the x-coordinate of the previous node position.
- py - the y-coordinate of the previous node position.
- fixed - a boolean indicating whether node position is locked.
- weight - the node weight; the number of associated links.
These attributes are not strictly needed for the call to force.nodes(), but if these are not present, then they would be randomly initialised by force.start() on the first call.
If anybody is curious, the working code looks like this:
<script type="text/javascript">
function myGraph(el) {
// 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++;
}
var index = findNodeIndex(id);
if(index !== undefined) {
nodes.splice(index, 1);
update();
}
}
this.addLink = function (sourceId, targetId) {
var sourceNode = findNode(sourceId);
var targetNode = findNode(targetId);
if((sourceNode !== undefined) && (targetNode !== undefined)) {
links.push({"source": sourceNode, "target": targetNode});
update();
}
}
var findNode = function (id) {
for (var i=0; i < nodes.length; i++) {
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 = $(el).innerWidth(),
h = $(el).innerHeight();
var vis = this.vis = d3.select(el).append("svg:svg")
.attr("width", w)
.attr("height", h);
var force = d3.layout.force()
.gravity(.05)
.distance(100)
.charge(-100)
.size([w, h]);
var nodes = force.nodes(),
links = force.links();
var update = function () {
var link = vis.selectAll("line.link")
.data(links, function(d) { return d.source.id + "-" + d.target.id; });
link.enter().insert("line")
.attr("class", "link");
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("image")
.attr("class", "circle")
.attr("xlink:href", "https://d3nwyuy0nl342s.cloudfront.net/images/icons/public.png")
.attr("x", "-8px")
.attr("y", "-8px")
.attr("width", "16px")
.attr("height", "16px");
nodeEnter.append("text")
.attr("class", "nodetext")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) {return d.id});
node.exit().remove();
force.on("tick", function() {
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; });
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
});
// Restart the force layout.
force.start();
}
// Make it all go
update();
}
graph = new myGraph("#graph");
// You can do this from the console as much as you like...
graph.addNode("Cause");
graph.addNode("Effect");
graph.addLink("Cause", "Effect");
graph.addNode("A");
graph.addNode("B");
graph.addLink("A", "B");
</script>