向强制布局添加新节点

关于堆栈溢出的第一个问题,请耐心等待!我是 d3.js 的新手,但是我一直对其他人能够用它完成的事情感到惊讶... ... 而且几乎同样惊讶于我自己能够在这方面取得的进展是多么的微乎其微!很明显我没有兴奋过头所以我希望这里善良的灵魂能给我指明方向。

我的目的是创建一个可重用的 javascript 函数,它只执行以下操作:

  • 在指定的 DOM 元素中创建空白的力定向图
  • 允许您向该图表添加和删除带标签的、带图像的节点,并指定它们之间的连接

我将 http://bl.ocks.org/950642作为一个起点,因为这就是我想要创建的布局类型:

enter image description here

我的代码是这样的:

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="jquery.min.js"></script>
<script type="text/javascript" src="underscore-min.js"></script>
<script type="text/javascript" src="d3.v2.min.js"></script>
<style type="text/css">
.link { stroke: #ccc; }
.nodetext { pointer-events: none; font: 10px sans-serif; }
body { width:100%; height:100%; margin:none; padding:none; }
#graph { width:500px;height:500px; border:3px solid black;border-radius:12px; margin:auto; }
</style>
</head>
<body>
<div id="graph"></div>
</body>
<script type="text/javascript">


function myGraph(el) {


// Initialise the graph object
var graph = this.graph = {
"nodes":[{"name":"Cause"},{"name":"Effect"}],
"links":[{"source":0,"target":1}]
};


// Add and remove elements on the graph object
this.addNode = function (name) {
graph["nodes"].push({"name":name});
update();
}


this.removeNode = function (name) {
graph["nodes"] = _.filter(graph["nodes"], function(node) {return (node["name"] != name)});
graph["links"] = _.filter(graph["links"], function(link) {return ((link["source"]["name"] != name)&&(link["target"]["name"] != name))});
update();
}


var findNode = function (name) {
for (var i in graph["nodes"]) if (graph["nodes"][i]["name"] === name) return graph["nodes"][i];
}


this.addLink = function (source, target) {
graph["links"].push({"source":findNode(source),"target":findNode(target)});
update();
}


// set up the D3 visualisation in the specified element
var w = $(el).innerWidth(),
h = $(el).innerHeight();


var vis = d3.select(el).append("svg:svg")
.attr("width", w)
.attr("height", h);


var force = d3.layout.force()
.nodes(graph.nodes)
.links(graph.links)
.gravity(.05)
.distance(100)
.charge(-100)
.size([w, h]);


var update = function () {


var link = vis.selectAll("line.link")
.data(graph.links);


link.enter().insert("line")
.attr("class", "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; });


link.exit().remove();


var node = vis.selectAll("g.node")
.data(graph.nodes);


node.enter().append("g")
.attr("class", "node")
.call(force.drag);


node.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");


node.append("text")
.attr("class", "nodetext")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name });


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
.nodes(graph.nodes)
.links(graph.links)
.start();
}


// Make it all go
update();
}


graph = new myGraph("#graph");


// These are the sort of commands I want to be able to give the object.
graph.addNode("A");
graph.addNode("B");
graph.addLink("A", "B");


</script>
</html>

每次我添加一个新节点,它都会重新标记所有现有的节点; 这些节点叠加在一起,事情开始变得难看。我理解为什么会这样: 因为当我在添加新节点时调用 update()函数时,它会对整个数据集执行 node.append(...)。我不知道如何为 只有我添加的节点做到这一点... 显然,我只能使用 node.enter()来创建一个新元素,所以这对于我需要绑定到节点的其他元素不起作用。我该怎么补救?

感谢您在这个问题上给予的任何指导!

编辑是因为我很快修复了前面提到的其他几个 bug 的来源

35994 次浏览

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:

enter image description here

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>