Actually I solved this like this(similar to previous but more sophisticated):
force.on("tick", function(e) {
node.attr("transform", function(d) {
//TODO move these constants to the header section
//center the center (root) node when graph is cooling down
if(d.index==0){
damper = 0.1;
d.x = d.x + (w/2 - d.x) * (damper + 0.71) * e.alpha;
d.y = d.y + (h/2 - d.y) * (damper + 0.71) * e.alpha;
}
//start is initiated when importing nodes from XML
if(d.start === true){
d.x = w/2;
d.y = h/2;
d.start = false;
}
r = d.name.length;
//these setting are used for bounding box, see [http://blockses.appspot.com/1129492][1]
d.x = Math.max(r, Math.min(w - r, d.x));
d.y = Math.max(r, Math.min(h - r, d.y));
return "translate("+d.x+","+d.y+")";
}
);
});
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>
Best Answer
All the answers above misunderstood Øystein Amundsen's question.
The only way to stabilize the force upon it starts is to set node.x and node.y a proper value. Please note that the node is the data of d3.js, not the represented DOM type.
For example, if you load
into
you have to set all .x and .y of all elements in array of nodes it will be like this ( in coffeescript )
then the nodes will start at the position when force.start(). this would avoid the chaos.