推送新节点时 D3.js 更新图 [英] D3.js update graph when pushing new nodes

查看:36
本文介绍了推送新节点时 D3.js 更新图的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试通过 nodes.push({id": item, group": 1, size": 30}); 动态地将新节点添加到 D3 图.但是当我这样做时,会出现一个视觉错误,其中存在重复项.每当我 update() 时,我都会得到已经存在的两倍.有人有什么建议吗?将不胜感激.

I am trying to add new nodes to a D3 graph dynamically by nodes.push({"id": item, "group": 1, "size": 30}); but when I do this there is a visual bug where there are duplicates. Anytime I update() I get a double of whatever was already there. Anyone have any advice? Would be appreciated.

    var node;
    var link;
    var circles
    var lables;

    function update(){
        node = svg.append("g")
            .attr("class", "nodes")
            .selectAll("g")
            .data(nodes)
            .enter().append("g")

        link = svg.append("g")
            .attr("class", "links")
            .selectAll("line")
            .data(links)
            .enter().append("line")
            .attr("stroke-width", function(d) { return Math.sqrt(d.value); });

        circles = node.append("circle")
            .attr("r", function(d) { return (d.size / 10) + 1})
            .attr("fill", function(d) { return color(3); })
            .call(d3.drag()
                .on("start", dragstarted)
                .on("drag", dragged)
                .on("end", dragended))
                .on("click", clicked);

        lables = node.append("text")
            .text(function(d) {
                return d.id;
            })
            .attr('x', 6)
            .attr('y', 3)
            .style("font-size", "20px");

        node.append("title")
            .text(function(d) { return d.id; });

        simulation
            .nodes(nodes)
            .on("tick", ticked);

        simulation.force("link")
            .links(links);
    }

    function ticked() {
        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 + ")";
            })
    }

推荐答案

只看节点(链接本质上是相同的问题),每次更新数据时:

Looking just at the nodes (the links are essentially the same issue), every time you update your data you:

  1. 创建一个新的父级 g (svg.append("g"))
  2. 选择该新父元素 g 的所有子 g 元素 (.selectAll("g")).由于这个新的父级 g 没有子级 - 您刚刚创建了它,因此没有选择任何内容.
  3. 将数据绑定到选择 (.data(nodes))
  4. 使用输入选择,为数据数组中的每个项目附加一个 g(因为选择中没有元素,所以输入了所有内容(输入选择在 DOM 中为每个元素创建一个元素)数据数组中在选择中不存在对应元素的项目.)
  5. 为每个新添加的 g 添加一个圆圈.(.enter().append("g"))
  1. Create a new parent g (svg.append("g"))
  2. Select all the child g elements of that new parent g (.selectAll("g")). Since this new parent g has no children - you just made it, nothing is selected.
  3. Bind data to the selection (.data(nodes))
  4. Using the enter selection, append a g for each item in the data array (as there are no elements in the selection, everything is entered (the enter selection creates an element in the DOM for every item in the data array for which no corresponding element exists in the selection.)
  5. Append a circle to each newly appended g. (.enter().append("g"))

您无处选择已经存在的节点 - 这些都被丢弃了.它们会被 tick 函数忽略,因为 linknode 指的是新创建的节点和链接的选择.您也不会删除旧的链接和节点 - 所以它们只是永远存在或直到您关闭浏览器.

Nowhere do you select the already existing nodes - these are just cast aside. They are ignored by the tick function because link and node refer to selections of newly created nodes and links. Neither do you remove the old links and nodes - so they just sit there for all eternity or until you close the browser.

规范的解决方案是:

  • 附加结构元素一次.我说结构是参考父 g 元素:它们不依赖于数据,它们是有组织的.它们应该在更新函数之外附加一次.

  • Append structural elements once. I say structural in reference to the parent g elements: they aren't data dependent, they're organizational. They should be appended once outside of the update function.

使用更新功能来管理(创建、更新、删除)依赖于数据的元素:节点和链接本身.任何依赖于数据的东西都需要在更新函数中修改,没有别的.

Use the update function to manage (create, update, remove) elements that are dependent on the data: the nodes and links themselves. Anything that depends on the data needs to be modified in the update function, nothing else.

所以我们可以在更新函数之外附加父 g 元素:

So we could append the parent g elements outside of the update function:

var nodeG = svg.append("g").attr("class", "nodes");
var linkG = svg.append("g").attr("class", "links");

然后在更新函数中我们可以使用这些选择来进行进入/更新/退出循环.这在您的情况和许多其他情况下有点复杂,因为我们有由带有子元素的 g 表示的节点.类似于以下内容:

Then in the update function we can use those selections to conduct the enter/update/exit cycle. This is slightly complicated in your case, and many others, because we have nodes represented by a g with child elements. Something like the following:

function update() {

    var node = nodeG.selectAll("g")
       .data(nodes)

    // remove excess nodes.
    node.exit().remove();

    // enter new nodes as required:
    var nodeEnter = node.enter().append("g")
      .attr(...

    // append  circles to new nodes:
    nodeEnter.append("circle")
      .attr(...

    // merge update and enter.
    node = nodeEnter.merge(node);

   // do enter/update/exit with lines.
    var link = linkG.selectAll("line")
       .attr(...

   link.exit().remove();

    var linkEnter = link.enter().append("line")
      .attr(...

    link = linkEnter.merge(link);

   ...

在您的情况下可能如下所示:

Which in your case may look like:

// Random data:
let graph = { nodes: [], links : [] }

function randomizeData(graph) {
  // generate nodes:
  let n = Math.floor(Math.random() * 10) + 6;

  let newNodes = [];
  for(let i = 0; i < n; i++)  {
    if (graph.nodes[i]) newNodes.push(graph.nodes[i]);
    else newNodes.push({ id: i, 
                        color: Math.floor(Math.random()*10), 
                        size: Math.floor(Math.random() * 10 + 2),
                        x: (Math.random() * width), 
                        y: (Math.random() * height)
                      })
  }
  // generate links
  let newLinks = [];
  let m = Math.floor(Math.random() * n) + 1;
  for(let i = 0; i < m; i++) {
    a = 0; b = 0;
    while (a == b) {
     a = Math.floor(Math.random() * n); 
     b = Math.floor(Math.random() * n);
    }
    newLinks.push({source: a, target: b})
    if(i < newNodes.length - 2) newLinks.push({source: i, target: i+1})
  }
  return { nodes: newNodes, links: newLinks }
}
// On with main code:

// Set up the structure:
const svg = d3.select("svg"),
   width = +svg.attr("width"),
   height = +svg.attr("height");
   
const color = d3.scaleOrdinal(d3.schemeCategory10);
         
const simulation = d3.forceSimulation()
  .force("link", d3.forceLink().id(function(d) { return d.id; }).strength(0.004))
  .force("charge", d3.forceManyBody())
  // to attract nodes to center, use forceX and forceY:
  .force("x", d3.forceX().x(width/2).strength(0.01))
  .force("y", d3.forceY().y(height/2).strength(0.01));
  
const nodeG = svg.append("g").attr("class","nodes")
const linkG = svg.append("g").attr("class", "links")

graph = randomizeData(graph);
update();

// Two variables to hold our links and nodes - declared outside the update function so that the tick function can access them.
var links; 
var nodes;

// Update based on data:
function update() {

   // Select all nodes and bind data:
   nodes = nodeG.selectAll("g")
      .data(graph.nodes);
      
   // Remove excess nodes:
   nodes.exit()
      .transition()
      .attr("opacity",0)
      .remove();
   
   // Enter new nodes:
   var newnodes = nodes.enter().append("g")
       .attr("opacity", 0)
       .call(d3.drag()
       .on("start", dragstarted)
       .on("drag", dragged)
       .on("end", dragended))
      
   // for effect:
   newnodes.transition()
       .attr("opacity",1)
       .attr("class", "nodes")

   newnodes.append("circle")
      .attr("r", function(d) { return (d.size * 2) + 1})
      .attr("fill", function(d) { return color(d.color); })

   newnodes.append("text")
      .text(function(d) {  return d.id; })
      .attr('x', 6)
      .attr('y', 3)
      .style("font-size", "20px");
      
   newnodes.append("title")
      .text(function(d) { return d.id; });
   
   // Combine new nodes with old nodes:
   nodes = newnodes.merge(nodes);
   
   // Repeat but with links:
   links = linkG.selectAll("line")
       .data(graph.links)
       
   // Remove excess links:
   links.exit()
      .transition()
      .attr("opacity",0)
      .remove();
   
   // Add new links:
   var newlinks = links.enter()
      .append("line")
      .attr("stroke-width", function(d) { return Math.sqrt(d.value); });
      
   // for effect:
   newlinks 
       .attr("opacity", 0)
       .transition()
       .attr("opacity",1)      

   // Combine new links with old:
   links = newlinks.merge(links);
         
         
   // Update the simualtion:
   simulation
      .nodes(graph.nodes) // the data array, not the selection of nodes.
      .on("tick", ticked)
      .force("link").links(graph.links)
      
   simulation.alpha(1).restart();
      
            
}
   
function ticked() {
   links // the selection of all links:
     .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; });
         
   nodes
     .attr("transform", function(d) {
        return "translate(" + d.x + "," + d.y + ")";
     })
}         
     
     
     function dragstarted(d) {
                if (!d3.event.active) simulation.alphaTarget(0.3).restart();
                d.fx = d.x;
                d.fy = d.y;
            }
         
            function dragged(d) {
                d.fx = d3.event.x;
                d.fy = d3.event.y;
            }
         
            function dragended(d) {
                if (!d3.event.active) simulation.alphaTarget(0);
                d.fx = null;
                d.fy = null;
            }
         
d3.select("button")
  .on("click", function() {
    graph = randomizeData(graph);
    update();
  
  })

        
                .links line {
                    stroke: #999;
                    stroke-opacity: 0.6;
                }
         
                .nodes circle {
                    stroke: #fff;
                    stroke-width: 1.5px;
                }
 

<button> Update</button>
<svg width="500" height="300"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

注意

我稍微更新了力参数以使用 forceX 和 forceY:将节点吸引到中心的力.定心力只保证重心是一个特定的值,而不是节点必须有多近.

I've updated the force paramaters a bit to use forceX and forceY: forces which attract the nodes to the center. The centering force only ensures the center of gravity is a specific value, not how close the nodes must be.

替代方法:

当然,您可以每次都删除所有内容并附加它:但这限制了从一个数据集过渡到下一个数据集的能力,并且通常不规范.

Of course, you could just remove everything and append it each time: but this limits ability to transition from one dataset to the next and is generally not canonical.

如果你只需要输入一次元素(在更新过程中不需要添加或删除元素)那么你可以避免使用完整的进入/更新/退出循环并在更新功能之外追加一次,更新节点/链接属性更新新数据,而不是使用上面代码片段中更复杂的进入/更新/退出循环.

If you only need to enter elements once (no elements need to be added or removed during updates) then you could avoid using the full enter/update/exit cycle and append once outside the update function, updating node/link attributes with new data on update rather than using the more involved enter/update/exit cycle in the snippet above.

这篇关于推送新节点时 D3.js 更新图的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆