关于 javascript:现有节点在新数据输入时冻结

Existing nodes freeze upon new data entering

将新链接和节点数据绑定到强制布局和 svg 元素后,旧节点和链接会冻结。 enter() 选择的新节点和链接也不会连接到现有节点。

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; });
}

代码修订

修改布局后需要运行force.start()。布局的所有配置都在 force.start() 中完成。

nodeslinks 不需要在每次更改数据时重新绑定。

我还将结构更改为一种模式,可以为您提供最大的控制力和灵活性。
使用此模式,您可以分别管理更新、进入和退出组件。

最后一周是使用

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();
}

背景

数据绑定

d3.layout.force 维护对 nodeslinks 数组的引用的闭包,因此您只需要将布局绑定到数组引用一次。
正如您在阅读代码时所看到的......

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 节点的 __data__ 成员上。

由于数组元素一般都是复杂的对象,

1
__data__ === nodesData[i] // true

对于选择的第 i 个成员,数据绑定到它。

selection.datum() 方法返回所选节点(或选择中的第一个非空节点)的 __data__ 成员的值,该值是对数据数组元素的引用。这当然意味着对数据数组元素成员的任何修改都会自动反映在选择的数据绑定以及引用节点的 __data__ 成员的任何内容中。

此时值得注意的是,

返回的对象

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

强制布局和选择引用相同的数据(以不同的方式!)但不相互引用。

力布局的作用是通过使用其内部动态设置来管理动画事件(帧)来计算每个tick的节点位置。每当发生数据结构事件时,都需要通过调用 force.start() 来通知强制布局(如果您想知道原因,请获取 d3 源和 RTFC)。

选择的作用是通过将数据绑定到它们并提供用于根据数据管理它们的 API 来管理 DOM 元素。无论何时发生数据结构事件,都需要通过更新/输入/追加循环来通知选择。

在 force layout API 提供的 tick 事件中,选择用于根据 force layout 计算的新数据来驱动可视化(DOM 元素)。

动态变化的力布局结构

如果你使用浏览器开发者工具查看 force.nodes() 返回的数组的元素,你会发现在原来的成员之上添加了很多状态,还有关闭状态d3.force 对象,例如距离、强度和电荷。所有这些都必须在某个地方设置,毫不奇怪,它是在 force.start() 中完成的。这就是为什么每次更改数据结构时都必须调用 force.start() 的原因。

一般模式

一般来说,这是最具防御性的模式……

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()

第一次通过 update 将是一个与数据具有相同结构的空数组。
.selectAll() 在这种情况下返回零长度选择并且没有任何用处。

在后续更新中,.selectAll不会为空,会与values进行比较,使用keys来判断哪些节点被更新,进入和退出节点。这就是为什么您需要在数据连接之前进行选择。

要理解的重要一点是它必须是 .enter().append(...),因此您在输入选择中附加元素。如果您将它们附加到更新选择(数据连接返回的那个)上,那么您将重新输入相同的元素并看到与您得到的类似的行为。

输入选择是 { __data__: data } 形式的简单对象数组
更新和退出选择是对 DOM 元素的引用数组。

d3 中的 data 方法对进入和退出选择保持一个闭包,这些选择由 update 上的 .enter().exit() 方法访问。两者都返回对象,其中包括二维数组(d3 中的所有选择都是组数组,其中组是节点数组。)。
enter 成员也被赋予了对 update 的引用,以便它可以合并两者。这样做是因为,在大多数情况下,对两组都做了同样的事情。