Existing nodes freeze upon new data entering
将新链接和节点数据绑定到强制布局和 svg 元素后,旧节点和链接会冻结。
jsFiddle
为什么会出现这个问题?
我浏览了各种类似的问题,但没有一个给出令人满意的答案。请注意,我还彻底阅读了经常被引用的"thinking with joins",进入/更新/退出选择文章。不过,这里没有点击。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | start(graph); 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("cx", function(d) { return d.x; }) .attr("cy", function(d) { return d.y; }); }); window.setTimeout(function(){ graph.nodes.push({"name":"Westby","group":2}) graph.links.push({"source":5,"target":2,"value":1}) start(graph); }, 2000); function start(graph){ force .nodes(graph.nodes) .links(graph.links) .start(); link = svg.selectAll(".link") .data(graph.links) .enter().append("line") .attr("class","link") .style("stroke-width", function(d) { return Math.sqrt(d.value); }); node = svg.selectAll(".node") .data(graph.nodes) .call(force.drag) .enter().append("circle") .attr("class","node") .attr("r", 5) .style("fill", function(d) { return color(d.group); }) .call(force.drag); node.append("title") .text(function(d) { return d.name; }); } |
代码修订
修改布局后需要运行
我还将结构更改为一种模式,可以为您提供最大的控制力和灵活性。
使用此模式,您可以分别管理更新、进入和退出组件。
最后一周是使用
1 | link.enter().insert("line","circle.node") |
而不是
1 | link.enter().append("line") |
这是为了确保链接呈现在圆圈后面。
修改后的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | force //you only need to do this once/////////// .nodes(graph.nodes) .links(graph.links) ////////////////////////////////////////// .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("cx", function (d) { return d.x; }) .attr("cy", function (d) { return d.y; }); }); start(graph); window.setTimeout(function () { graph.nodes.push({"name":"Westby","group": 2 }) graph.links.push({"source": 5,"target": 2,"value": 1 }) start(graph); }, 2000); function start(graph) { //UPDATE pre-existing nodes to be re-cycled link = svg.selectAll(".link") .data(graph.links); //ENTER new nodes to be created link.enter().insert("line","circle.node") //insert before node! .attr("class","link") //UPDATE+ENTER .enter also merges update and enter, link is now both link.style("stroke-width", function (d) { return Math.sqrt(d.value); }); //EXIT link.exit().remove() //UPDATE node = svg.selectAll(".node") .data(graph.nodes) //ENTER node.enter().append("circle") .attr("class","node") .attr("r", 5) .call(force.drag); //UPDATE+ENTER .enter also merges update and enter, link is now both node.style("fill", function (d) { return color(d.group); }) //EXIT node.exit().remove(); node.append("title") .text(function (d) { return d.name; }); force.start(); } |
背景
数据绑定
正如您在阅读代码时所看到的......
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | d3.layout.force = function () { var force = {}, //... nodes = [], links = [], distances, strengths, charges; //... force.nodes = function (x) { if (!arguments.length) return nodes; nodes = x; return force; }; force.links = function (x) { if (!arguments.length) return links; links = x; return force; }; //... }; |
所以我们有
1 2 3 4 5 | force().nodes(nodesData); force().links(linksData); force().nodes() === nodesData // true force().links() === linksData // true |
此外,由于数据绑定在 d3 中的工作方式,选择结构中的每个 DOM 节点都有对其各自数据数组的一个元素的引用。这存储在 d3 添加到 DOM 节点的
由于数组元素一般都是复杂的对象,
1 | __data__ === nodesData[i] // true |
对于选择的第 i 个成员,数据绑定到它。
此时值得注意的是,
返回的对象
1 | update = selection.data(values) |
是一个新数组,所以我们有
1 2 | update.data() === values // false update.data()[i] === values[i] // true |
角色和参考(摘要)
节点和链接数据存储为对象数组。数组元素的成员是通知可视化所需的信息 - 节点或链接的类型、它们的标签、分组信息等。
通过引用数据数组将强制布局绑定到数据:
1 2 3 4 5 | force().nodes(nodesData); force().links(linksData); force().nodes() === nodesData // true force().links() === linksData // true |
通过引用数据数组元素将选择绑定到数据:
1 2 3 4 5 6 7 8 | nodes = selection.data(nodesData); links.enter().append(nodeSelector) links = selection.data(linksData); links.enter().append(linkSelector) nodes === nodesData //false - nodes is a selection, nodesData is an array nodes.data() === nodesData //false - nodes.data() returns a new array nodes.data()[i] === nodesData[i] //true! - the elements of the data array are coppied to the new array that is returned by the selection //similar for links |
强制布局和选择引用相同的数据(以不同的方式!)但不相互引用。
力布局的作用是通过使用其内部动态设置来管理动画事件(帧)来计算每个
选择的作用是通过将数据绑定到它们并提供用于根据数据管理它们的 API 来管理 DOM 元素。无论何时发生数据结构事件,都需要通过更新/输入/追加循环来通知选择。
在 force layout API 提供的 tick 事件中,选择用于根据 force layout 计算的新数据来驱动可视化(DOM 元素)。
动态变化的力布局结构
如果你使用浏览器开发者工具查看 force.nodes() 返回的数组的元素,你会发现在原来的成员之上添加了很多状态,还有关闭状态d3.force 对象,例如距离、强度和电荷。所有这些都必须在某个地方设置,毫不奇怪,它是在
一般模式
一般来说,这是最具防御性的模式……
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | //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() |
第一次通过
在后续更新中,
要理解的重要一点是它必须是
输入选择是
更新和退出选择是对 DOM 元素的引用数组。
d3 中的 data 方法对进入和退出选择保持一个闭包,这些选择由