首页 > 解决方案 > 当“.enter()”选择应该为空时,D3js 将重复数据附加到 SVG

问题描述

我编写了一个小应用程序,如果用户单击屏幕,将出现一个节点(圆圈)。如果用户随后从一个节点拖动到另一个节点,它们之间的边也会出现。这两个都在内部更新“nodeData”和“edgeData”数据结构。

节点似乎工作正常。如果用户点击屏幕,新节点将被添加到数据结构中,并且将调用函数“restart()”来更新可视化。然而,边缘并没有像我预期的那样工作。“restart()”函数不是更新当前边并将任何新边附加到可视化中,而是将边再次附加到可视化中,因此在“restart()”被调用几次之后,图形的边太多了比它应该的。

这是小提琴的链接: https ://jsfiddle.net/AlexMarshaall/srL3huk9/4/

    <!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">    
  <style>

    svg {
      background-color: #FFF;
      cursor: default;
      -webkit-user-select: none;
      -moz-user-select: none;
      -ms-user-select: none;
      -o-user-select: none;
      user-select: none;
    }

    svg:not(.active):not(.ctrl) {
      cursor: crosshair;
    }

    path.link {
      fill: none;
      stroke: #000;
      stroke-width: 4px;
      cursor: default;
    }

    svg:not(.active):not(.ctrl) path.link {
      cursor: pointer;
    }

    path.link.selected {
      stroke-dasharray: 10, 2;
    }

    path.link.dragline {
      pointer-events: none;
    }

    path.link.hidden {
      stroke-width: 0;
    }

    circle.node {
      stroke-width: 1.5px;
      cursor: pointer;
    }

    text {
      font: 12px sans-serif;
      pointer-events: none;
    }

    text.id {
      text-anchor: middle;
      font-weight: bold;
    }
  </style>

</head>

<body>
  <script src="/static/d3/d3.min.js"></script>
  <script>

    const svg = d3.select('body')
      .append('svg')
      .attr('width', 900)
      .attr('height', 500);

    let hoverOverNode = null;   // the node the mouse is currently hovering over
    let mousedownNode = null;   // the node that the mouse went down over

    function resetMouseVars() {
      mousedownNode = null;
    }

    var lastNodeId = 1;
    var node1 = {
      id: "n" + lastNodeId++,
      xVal: 50,
      yVal: 50
    };

    var node2 = {
      id: "n" + lastNodeId++,
      xVal: 100,
      yVal: 100
    }

    const nodesData = [node1,node2];

    const edgeData = [{
      source: node1,
      target: node2
    }];

    let paths = svg.append('svg:g').selectAll('path');
    let nodes = svg.append('svg:g').selectAll('g');

    const dragLine = svg.append('svg:path')
      .attr('class', 'link dragline hidden')
      .attr('d', 'M0,0L0,0');


    function restart() {

      nodes = nodes.data(nodesData, (d) => d.id); // Nodes is just the nodes to update

      nodes.selectAll('g')
        .style('fill', "DarkGreen");

      nodes.exit().remove();

      var newNodesToBeAdded = nodes.enter().append('svg:g');

      newNodesToBeAdded.attr('transform', (d) => `translate(${d.xVal},${d.yVal})`)
        .attr('id', (d) => d.id);

      newNodesToBeAdded.append('svg:circle')
        .attr('class', 'node')
        .attr('r', 12)
        .attr('stroke-width', 3)
        .attr('stroke', 'black')
        .style('fill', "DarkGreen")
        .on('mouseover', function(d) {
          hoverOverNode = d;
          d3.select(this).attr('transform', 'scale(1.1)');
        })
        .on('mouseout', function(d) {
          hoverOverNode = null;
          d3.select(this).attr('transform', '');
        })
        .on('mousedown', (d) => {
          if (d3.event.ctrlKey) return;
          mousedownNode = d;
          dragLine
            .classed('hidden', false)
            .attr('d', `M${mousedownNode.xVal},${mousedownNode.yVal}L${mousedownNode.xVal},${mousedownNode.yVal}`);
          restart();
        })
        .on('mouseup', (d) => {
          dragLine.classed('hidden', true);
        });

      newNodesToBeAdded.append('svg:text')
        .attr('x', 0)
        .attr('y', 4)
        .attr('class', 'id')
        .text((d) => d.id.substring(1));

      nodes = newNodesToBeAdded.merge(nodes);


      paths = paths.data(edgeData);

      paths.append('svg:path')
        .attr("class", "link")
        .attr("d", (d) => {
          const sourceX = d.source.xVal;
          const sourceY = d.source.yVal;
          const targetX = d.target.xVal;
          const targetY = d.target.yVal;
          return `M${sourceX},${sourceY}L${targetX},${targetY}`;
        });

      paths.exit().remove();

      var newPathsToBeAdded = paths.enter().append('svg:path');

      newPathsToBeAdded.attr('class','link')
        .attr("d", (d) => {
          const sourceX = d.source.xVal;
          const sourceY = d.source.yVal;
          const targetX = d.target.xVal;
          const targetY = d.target.yVal;
          return `M${sourceX},${sourceY}L${targetX},${targetY}`;
        });

    }

    function mousedown() {
      if (d3.event.ctrlKey || hoverOverNode != null) {
        return;
      }
      var coords = d3.mouse(this);
      var newNode = {
        id: "n" + lastNodeId++,
        xVal: coords[0],
        yVal: coords[1]
      };
      nodesData.push(newNode);
      restart();
    }

    function mousemove() {
      if (!mousedownNode) return; // if there's no mousedownNode then there's no need to do anything

      // update dragline
      dragLine.attr('d', `M${mousedownNode.xVal},${mousedownNode.yVal}L${d3.mouse(this)[0]},${d3.mouse(this)[1]}`);
      restart();
    }

    function mouseup() {
      // if there is a mousedown node, hide the dragline that's been drawn
      if (mousedownNode){
        dragLine
          .classed('hidden', true);
        if (hoverOverNode != null){
          edgeData.push({source:mousedownNode, target:hoverOverNode});
          resetMouseVars();
          restart();
        }
      }
    }

    // App starts here
    svg.on('mousedown', mousedown)
      .on('mousemove', mousemove)
      .on('mouseup', mouseup);
    restart()

  </script>
</body>
</html>

标签: javascriptd3.jsappend

解决方案


你有这些问题,因为你没有遵循正确的做法。

paths = paths.data(edgeData);

.data 方法返回一个更新选择,它真的不应该在调用之间保存。您应该创建一个局部变量,而不是重用路径。您有一个奇怪的行为,因为您没有将更新选择与输入选择合并 - 结果您创建的路径没有被捕获到更新选择中,并且每次相同的路径数据显示为 d3 的新数据。嗯,至少我是这么认为的。添加paths = newPathsToBeAdded.merge(paths);应该修复它。


如果您遵循规范循环,则将来遇到此类奇怪问题的可能性较小。我会这样做:

var paths = svg.select('g.pathcnt').selectAll('path.edge'); // add classes for selection reference
paths.exit().remove();
paths.enter()
   .append('path')
   .classed('edge', true)
//do not duplicate things; put it after the merge to set for both new and existing nodes
.merge(paths)
    .attr('d', function(d) { ... });

我不建议在调用之间保留选择;所有示例都根据需要重新选择,因此我不确定重用选择是否有效。


推荐阅读