管理更新
您遇到的主要问题是每次更新都会重新附加组元素。
你可以让 d3 像这样为你管理它......
//Nodes bag
//UPDATE
var circles = svg.selectAll(".circles")
.data(["circles_g"]);
//ENTER
circles.enter()
.append("svg:g")
.attr("class", "circles");
你只需要组成一个单元素数组来驱动它。这样做的好处是它将被放置在 __data__ 成员上,该成员由 d3 添加到 g 元素中,因此它也便于调试。
一般模式
一般来说,这是最具防御性的模式……
//UPDATE
var update = baseSelection.selectAll(elementSelector)
.data(values, key),
//ENTER
enter = update.enter().append(appendElement)
.call(initStuff),
//enter() has side effect of adding enter nodes to the update selection
//so anything you do to update now will include the enter nodes
//UPDATE+ENTER
updateEnter = update
.call(stuffToDoEveryTimeTheDataChanges);
//EXIT
exit = update.exit().remove()
第一次通过update 将是一个与数据具有相同结构的空数组。
在这种情况下,.selectAll() 返回一个零长度选择并且没有任何用处。
在后续更新中,.selectAll 不会为空,并将与values 进行比较,使用keys 来确定哪些节点是更新、进入和退出节点。这就是为什么您需要在数据连接之前进行选择。
要理解的重要一点是它必须是.enter().append(...),因此您要在输入选择中附加元素。如果您将它们附加到更新选择(数据连接返回的选择)上,那么您将重新输入相同的元素并看到与您得到的类似的行为。
输入选择是{ __data__: data }形式的简单对象数组
更新和退出选择是对 DOM 元素的引用数组。
d3 中的 data 方法对进入和退出选择保持一个闭包,这些选择由 update 上的 .enter() 和 .exit() 方法访问。两者都返回对象,其中包括二维数组(d3 中的所有选择都是组数组,其中组是节点数组。)。
enter 成员也被赋予了对update 的引用,以便它可以合并两者。这样做是因为在大多数情况下,对两组都做了相同的事情。
修改后的代码
有一个奇怪的错误,当添加不相关的边缘但由于节点中的d.x 和d.y 中的NaN 时,链接有时会消失。
如果你不每次都在showme 中重建强制布局并且如果你这样做......
links.push({ "source": nodes[i], "target": nodes[j], "type": "is_a_tenant_of" });
force.start();
showme();
错误消失了,一切正常。
这是因为布局的内部状态不包括
额外的链接,特别是 strengths 和 distances 数组。这
内部force.tick() 方法使用这些来计算新链接
长度,如果链接数多于这些数组的成员,则
他们将返回undefined 和链接,长度计算将
返回NaN 然后乘以节点x 和y
值来计算新的 d.x 和 d.y。
这都是在force.start()中重新计算的
此外,您可以将 force = d3.layout.force()....start(); 移动到单独的 function 中,并且只在开始时调用一次。
d3.json("force-directed-edges.json", function(error, data){
if (error) return console.warn(error)
nodes = data.nodes,
links = data.links,
predicates = data.predicates,
json = JSON.stringify(data, undefined, 2);
for (n in nodes) { // don't want to require incoming data to have links array for each node
nodes[n].links = []
}
links.forEach(function(link, i) {
// kept the 'Or' check, in case we're building the nodes only from the links
link.source = nodes[link.source] || (nodes[link.source] = {name: link.source});
link.target = nodes[link.target] || (nodes[link.target] = { name: link.target });
// To do any dijkstra searching, we'll need adjacency lists: node.links. (easier than I thought)
link.source.links.push(link);
link.target.links.push(link);
});
nodes = d3.values(nodes);
reStart()
showme();
});
function randomNode(i) {
var j;
do {
j = Math.round(Math.random() * (nodes.length - 1))
} while (j === (i ? i : -1))
return j
}
function addedge() {
var i = randomNode(), j = randomNode(i);
links.push({ "source": nodes[i], "target": nodes[j], "type": "is_a_tenant_of" });
force.start();
showme();
}
function reStart() {
force = d3.layout.force()
.nodes(nodes)
.links(links)
.size([w, h])
.linkDistance(function (link) {
var wt = link.target.weight;
return wt > 2 ? wt * 10 : 60;
})
.charge(-600)
.gravity(.01)
.friction(.75)
//.theta(0)
.on("tick", tick)
.start();
}
function showme() {
//Marker Types
var defs = svg.selectAll("defs")
.data(["defs"], function (d) { return d }).enter()
.append("svg:defs")
.selectAll("marker")
.data(predicates)
.enter().append("svg:marker")
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 30)
.attr("refY", 0)
.attr("markerWidth", 4)
.attr("markerHeight", 4)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5"),
//Link bag
//UPDATE
paths = svg.selectAll(".paths")
.data(["paths_g"]);
//ENTER
paths.enter()
.append("svg:g")
.attr("class", "paths");
//Links
//UPDATE
path = paths.selectAll("path")
.data(links);
//ENTER
path.enter()
.append("svg:path");
//UPDATE+ENTER
path
.attr("indx", function (d, i) { return i })
.attr("id", function (d) { return d.source.index + "_" + d.target.index; })
.attr("class", function (d) { return "link " + d.type; })
.attr("marker-end", function (d) { return "url(#" + d.type + ")"; });
//EXIT
path.exit().remove();
//Link labels bag
//UPDATE
var path_labels = svg.selectAll(".labels")
.data(["labels_g"]);
//ENTER
path_labels.enter()
.append("svg:g")
.attr("class", "labels");
//Link labels
//UPDATE
var path_label = path_labels.selectAll(".path_label")
.data(links);
//ENTER
path_label.enter()
.append("svg:text")
.append("svg:textPath")
.attr("startOffset", "50%")
.attr("text-anchor", "middle")
.style("fill", "#000")
.style("font-family", "Arial");
//UPDATE+ENTER
path_label
.attr("class", function (d, i) { return "path_label " + i })
//EDIT*******************************************************************
.selectAll('textPath')
//EDIT*******************************************************************
.attr("xlink:href", function (d) { return "#" + d.source.index + "_" + d.target.index; })
.text(function (d) { return d.type; }),
//EXIT
path_label.exit().remove();
//Nodes bag
//UPDATE
var circles = svg.selectAll(".circles")
.data(["circles_g"]);
//ENTER
circles.enter()
.append("svg:g")
.attr("class", "circles");
//Nodes
//UPDATE
circle = circles.selectAll(".nodes")
.data(nodes);
//ENTER
circle.enter().append("svg:circle")
.attr("class", function (d) { return "nodes " + d.index })
.attr("stroke", "#000");
//UPDATE+ENTER
circle
.on("click", clicked)
.on("dblclick", dblclick)
.on("contextmenu", cmdclick)
.attr("fill", function (d, i) {
console.log(i + " " + d.types[0] + " " + node_colors[d.types[0]])
return node_colors[d.types[0]];
})
.attr("r", function (d) { return d.types.indexOf("Document") == 0 ? 24 : 12; })
.call(force.drag);
//EXIT
circle.exit().remove();
//Anchors bag
//UPDATE
var textBag = svg.selectAll(".anchors")
.data(["anchors_g"]);
//ENTER
textBag.enter()
.append("svg:g")
.attr("class", "anchors"),
//Anchors
//UPDATE
textUpdate = textBag.selectAll("g")
.data(nodes, function (d) { return d.name; }),
//ENTER
textEnter = textUpdate.enter()
.append("svg:g")
.attr("text-anchor", "middle")
.attr("class", function (d) { return "anchors " + d.index });
// A copy of the text with a thick white stroke for legibility.
textEnter.append("svg:text")
.attr("x", 8)
.attr("y", ".31em")
.attr("class", "shadow")
.text(function (d) { return d.name; });
textEnter.append("svg:text")
.attr("x", 8)
.attr("y", ".31em")
.text(function (d) { return d.name; });
textUpdate.exit().remove();
text = textUpdate;
// calling force.drag() here returns the drag _behavior_ on which to set a listener
// node element event listeners
force.drag().on("dragstart", function (d) {
d3.selectAll(".dbox").style("z-index", 0);
d3.select("#dbox" + d.index).style("z-index", 1);
})
}
编辑
针对@jjon 下面的评论和我自己的启发,这里是对具有相同命名约定和不同 cmets 的原始代码的最小更改。正确添加链接所需的模组没有改变,也没有讨论...
function showme() {
svg
/////////////////////////////////////////////////////////////////////////////////////
//Problem
// another defs element is added to the document every update
//Solution:
// create a data join on defs
// append the marker definitions on the resulting enter selection
// this will only be appended once
/////////////////////////////////////////////////////////////////////////////////////
//ADD//////////////////////////////////////////////////////////////////////////////////
.selectAll("defs")
.data(["defs"], function (d) { return d }).enter()
///////////////////////////////////////////////////////////////////////////////////////
.append("svg:defs")
.selectAll("marker")
.data(predicates)
.enter().append("svg:marker")
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 30)
.attr("refY", 0)
.attr("markerWidth", 4)
.attr("markerHeight", 4)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
/////////////////////////////////////////////////////////////////////////////////////
//Problem
// another g element is added to the document every update
//Solution:
// create a data join on the g and class it .paths
// append the path g on the resulting enter selection
// this will only be appeneded once
/////////////////////////////////////////////////////////////////////////////////////
//ADD//////////////////////////////////////////////////////////////////////////////////
//Link bag
//UPDATE
paths = svg
.selectAll(".paths")
.data(["paths_g"]);
//ENTER
paths.enter()
///////////////////////////////////////////////////////////////////////////////////////
.append("svg:g")
//ADD//////////////////////////////////////////////////////////////////////////////////
.attr("class", "paths");
///////////////////////////////////////////////////////////////////////////////////////
//Links
//UPDATE
path = paths //Replace svg with paths///////////////////////////////////////////////
.selectAll("path")
.data(links);
path.enter().append("svg:path")
.attr("id", function (d) { return d.source.index + "_" + d.target.index; })
.attr("class", function (d) { return "link " + d.type; })
.attr("marker-end", function (d) { return "url(#" + d.type + ")"; });
path.exit().remove();
/////////////////////////////////////////////////////////////////////////////////////
//Problem
// another g structure is added every update
//Solution:
// create a data join on the g and class it .labels
// append the labels g on the resulting enter selection
// this will only be appeneded once
// include .exit().remove() to be defensive
//Note:
// don't chain .enter() on the object assigned to path_label
// .data(...) returns an update selection which includes enter() and exit() methods
// .enter() returns a standard selection which doesn't have a .exit() member
// this will be needed if links are removed or even if the node indexing changes
/////////////////////////////////////////////////////////////////////////////////////
//ADD//////////////////////////////////////////////////////////////////////////////////
//Link labels bag
//UPDATE
var path_labels = svg.selectAll(".labels")
.data(["labels_g"]);
//ENTER
path_labels.enter()
///////////////////////////////////////////////////////////////////////////////////////
.append("svg:g")
//ADD//////////////////////////////////////////////////////////////////////////////////
.attr("class", "labels");
///////////////////////////////////////////////////////////////////////////////////////
//Link labels
//UPDATE
var path_label = path_labels
.selectAll(".path_label")
.data(links);
//ENTER
path_label
.enter().append("svg:text")
.attr("class", "path_label")
.append("svg:textPath")
.attr("startOffset", "50%")
.attr("text-anchor", "middle")
.attr("xlink:href", function (d) { return "#" + d.source.index + "_" + d.target.index; })
.style("fill", "#000")
.style("font-family", "Arial")
.text(function (d) { return d.type; });
//ADD//////////////////////////////////////////////////////////////////////////////////
path_label.exit().remove();
///////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////
//Problem
// another g structure is added every update
//Solution:
// create a data join on the g and class it .circles
// append the labels g on the resulting enter selection
// this will only be appeneded once
// include .exit().remove() to be defensive
/////////////////////////////////////////////////////////////////////////////////////
//ADD//////////////////////////////////////////////////////////////////////////////////
//Nodes bag
//UPDATE
var circles = svg.selectAll(".circles")
.data(["circles_g"]);
//ENTER
circles.enter()
///////////////////////////////////////////////////////////////////////////////////////
.append("svg:g")
//ADD//////////////////////////////////////////////////////////////////////////////////
.attr("class", "circles");
///////////////////////////////////////////////////////////////////////////////////////
//Nodes
//UPDATE
circle = circles
.selectAll(".node") //select on class instead of tag name//////////////////////////
.data(nodes);
circle //don't chain in order to keep the update selection////////////
.enter().append("svg:circle")
.attr("class", "node")
.attr("fill", function (d, i) {
return node_colors[d.types[0]];
})
.attr("r", function (d) { return d.types.indexOf("Document") == 0 ? 24 : 12; })
.attr("stroke", "#000")
.on("click", clicked)
.on("dblclick", dblclick)
.on("contextmenu", cmdclick)
.call(force.drag);
//ADD//////////////////////////////////////////////////////////////////////////////////
circle.exit().remove();
///////////////////////////////////////////////////////////////////////////////////////
//ADD//////////////////////////////////////////////////////////////////////////////////
//Anchors bag
//UPDATE
var textBag = svg.selectAll(".anchors")
.data(["anchors_g"]);
//ENTER
textBag.enter()
///////////////////////////////////////////////////////////////////////////////////////
.append("svg:g")
//ADD//////////////////////////////////////////////////////////////////////////////////
.attr("class", "anchors");
//Anchors
//UPDATE
text = textBag
///////////////////////////////////////////////////////////////////////////////////////
.selectAll(".anchor")
.data(nodes, function (d) { return d.name});
var textEnter = text //don't chain in order to keep the update selection//////////
.enter()
.append("svg:g")
.attr("class", "anchor")
.attr("text-anchor", "middle");
//ADD//////////////////////////////////////////////////////////////////////////////////
text.exit().remove;
///////////////////////////////////////////////////////////////////////////////////////
// A copy of the text with a thick white stroke for legibility.
textEnter.append("svg:text")
.attr("x", 8)
.attr("y", ".31em")
.attr("class", "shadow")
.text(function (d) { return d.name; });
textEnter.append("svg:text")
.attr("x", 8)
.attr("y", ".31em")
.text(function (d) { return d.name; });
// calling force.drag() here returns the drag _behavior_ on which to set a listener
// node element event listeners
force.drag().on("dragstart", function (d) {
d3.selectAll(".dbox").style("z-index", 0);
d3.select("#dbox" + d.index).style("z-index", 1);
})
}