Beginning with this example (possibly not the best choice), I set about trying to develop an application to suit my purposes, and learn d3.js in the process. After a lot of naive tinkering, I managed to get a toy force layout for my test data that satisfied me in its appearance and behavior. Now I've set about trying to understand MB's General Update Pattern in the context of my particular example, so that users can interactively modify the graph. I clearly have not yet grasped the principle.
Starting small, I thought to create a function that would simply add a single additional link to the graph between the nodes labeled "Walteri" and "Roberti de Fonte" (there's a button that executes addedge()
, or you can execute it from the js console). In a broken sort of way, this had the desired result; however, the existing graph remained in place while a duplicate graph was generated containing the additional link. It's clear that there is something about the General Update Pattern that I'm still not understanding.
If anyone has a look and can offer any insight, I'd be grateful.
答案 0 :(得分:1)
管理更新
您遇到的主要问题是每次更新时都会重新附加组元素
你可以得到d3为你管理这个......
//Nodes bag
//UPDATE
var circles = svg.selectAll(".circles")
.data(["circles_g"]);
//ENTER
circles.enter()
.append("svg:g")
.attr("class", "circles");
你只需要组成一个单元素阵列来驱动它。关于它的好处是它将被放置在由{3}添加到__data__
元素的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中的数据方法对进入和退出选择保持关闭,这些选择由.enter()
上的.exit()
和update
方法访问。两者都返回对象,其中包括二维数组(d3中的所有选择都是组的数组,其中组是节点数组。)。
enter
成员也被赋予update
的引用,以便它可以合并这两者。之所以这样做,是因为在大多数情况下,对两个群体都做了同样的事情。
修订后的代码
有一个奇怪的错误,当添加边缘时,链接有时会消失,但是由于节点中NaN
和d.x
中的d.y
而导致链接消失了。
如果您不是每次都在showme
重建力布局,并且如果您这样做......
links.push({ "source": nodes[i], "target": nodes[j], "type": "is_a_tenant_of" });
force.start();
showme();
这个bug消失了,一切正常。
这是因为布局的内部状态不包括 额外链接,尤其是
中重新计算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的评论和我自己的启发,这里是原始代码的最小变化,具有相同的命名约定和差异注释。正确添加链接所需的模块未更改,未进行讨论......
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);
})
}