d3强制定向图一般更新模式 - 退出()选择不起作用

时间:2018-06-14 16:35:24

标签: d3.js

我正在使用强制导向图,其中我的一般更新模式似乎不起作用。特别是当我尝试更新时,未使用的节点/链接不会按预期消失,并且仍在数据中的节点/链接不会转换为新数据。相反,旧图只是冻结,并在旧图上绘制新图。

我认为这是一个问题,我的选择没有正确处理数据,但我尝试了很多调整,但无法弄清楚我的问题。我尽可能地密切关注Bostock's example,但它没有按预期工作。运行可视化的Here's a codepen(注意:最简单的方法是使用并排编辑器查看可视化)。

提前致谢!

以下是代码:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Updating Graph</title>
  <style>
    .links line {
      stroke: #999;
      stroke-opacity: 0.6;
    }
    
    .nodes circle {
      stroke: #fff;
      stroke-width: 1.5px;
    }
  </style>
  <script src="https://d3js.org/d3.v4.min.js"></script>
</head>

<body>
  <button onclick=render(sp)>render new graph</button>
  <div class="chart"></div>
  <script>
    const cb = {
      "edges": [{
        "source": "a",
        "target": "b",
        "value": 1
      }, {
        "source": "a",
        "target": "c",
        "value": 1
      }, {
        "source": "b",
        "target": "c",
        "value": 1
      }, {
        "source": "c",
        "target": "d",
        "value": 1
      }],
      "nodes": [{
        "id": "a",
        "pop": 12.00328963067508,
        "size": 5
      }, {
        "id": "b",
        "pop": 12.391087593534877,
        "size": 5
      }, {
        "id": "c",
        "pop": 12.384324067681156,
        "size": 5
      }, {
        "id": "d",
        "pop": 13.991090521661292,
        "size": 6
      }]
    }
    const sp = {
      "edges": [{
        "source": "a",
        "target": "b",
        "value": 1
      }, {
        "source": "a",
        "target": "e",
        "value": 1
      }, {
        "source": "b",
        "target": "f",
        "value": 1
      }, {
        "source": "e",
        "target": "f",
        "value": 1
      }],
      "nodes": [{
        "id": "a",
        "pop": 12.00328963067508,
        "size": 5
      }, {
        "id": "b",
        "pop": 12.391087593534877,
        "size": 5
      }, {
        "id": "e",
        "pop": 13.063656176168433,
        "size": 6
      }, {
        "id": "f",
        "pop": 12.52608275807238,
        "size": 5
      }]
    }
    // set margins and canvas size
    const margin = {
      top: 10,
      right: 20,
      bottom: 30,
      left: 30
    };
    const width = 600 - margin.left - margin.right;
    const height = 500 - margin.top - margin.bottom;

    // set up canvas
    const svg = d3.select('.chart')
      .append('svg')
      .attr('width', width + margin.left + margin.right)
      .attr('height', height + margin.top + margin.bottom)
      .call(responsivefy)
      .append('g')
      .attr('transform', `translate(${margin.left}, ${margin.top})`);

    // set up selections
    let links = svg.append("g")
      .attr("class", "links")
      .selectAll("line");
    let nodes = svg.append("g")
      .attr("class", "nodes")
      .selectAll("circle");

    // set up color scale
    const color = d3.scaleSequential()
      .domain([8, 15])
      .interpolator(d3.interpolateInferno);

    // set up simulation basic parameters
    const simulation = d3.forceSimulation()
      .force("link", d3.forceLink().id(function(d) {
        return d.id;
      }))
      .force("charge", d3.forceManyBody())
      .force("center", d3.forceCenter(width / 2, height / 2));

    function render(graph) {
      // node selection and data handling
      let node = nodes
        .data(graph.nodes, function(d) {
          return d.id;
        });

      // node general update pattern
      node.exit()
        .transition()
        .attr("r", 0)
        .remove();

      node = node
        .enter().append("circle")
        .attr("r", function(d) {
          return d.size;
        })
        .attr("fill", function(d) {
          return color(d.pop);
        })
        .call(d3.drag()
          .on("start", dragstarted)
          .on("drag", dragged)
          .on("end", dragended))
        .merge(node);
      // give all nodes a title with their id for hover identification
      node.append("title")
        .text(function(d) {
          return d.id;
        });

      // link selection, data handling
      let link = links
        .data(graph.edges, function(d) {
          return d.source + "-" + d.target;
        });

      // link general update pattern with attrTween to keep links connected to disappearing nodes
      link
        .exit()
        .transition()
        .attr("stroke-opacity", 0)
        .attrTween("x1", function(d) {
          return function() {
            return d.source.x;
          };
        })
        .attrTween("x2", function(d) {
          return function() {
            return d.target.x;
          };
        })
        .attrTween("y1", function(d) {
          return function() {
            return d.source.y;
          };
        })
        .attrTween("y2", function(d) {
          return function() {
            return d.target.y;
          };
        })
        .remove();

      link = link
        .enter().append("line")
        .attr("stroke-width", function(d) {
          return Math.sqrt(d.value);
        })
        .merge(link);

      // add nodes and links to the siumlation
      simulation
        .nodes(graph.nodes)
        .on("tick", ticked);
      simulation.force("link")
        .links(graph.edges);
      // restart the simulation
      simulation.alpha(1).restart();

      // set the ticked function to constantly update node and link position
      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("cx", function(d) {
            return d.x;
          })
          .attr("cy", function(d) {
            return d.y;
          });
      }
    };

    // initial render
    render(cb)

    // dragging functions
    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;
    }

    // responsivefy from https://brendansudol.com/writing/responsive-d3
    function responsivefy(svg) {
      // get container + svg aspect ratio
      const container = d3.select(svg.node().parentNode),
        width = parseInt(svg.style("width")),
        height = parseInt(svg.style("height")),
        aspect = width / height;

      // add viewBox and preserveAspectRatio properties,
      // and call resize so that svg resizes on inital page load
      svg.attr("viewBox", "0 0 " + width + " " + height)
        .attr("preserveAspectRatio", "xMinYMid")
        .call(resize);

      // to register multiple listeners for same event type,
      // you need to add namespace, i.e., 'click.foo'
      // necessary if you call invoke this function for multiple svgs
      // api docs: https://github.com/mbostock/d3/wiki/Selections#on
      d3.select(window).on("resize." + container.attr("id"), resize);

      // get width of container and resize svg to fit it
      function resize() {
        const targetWidth = parseInt(container.style("width"));
        svg.attr("width", targetWidth);
        svg.attr("height", Math.round(targetWidth / aspect));
      }
    }
  </script>
</body>

</html>

1 个答案:

答案 0 :(得分:1)

问题在于您的选择nodeslinks是空的选择:

// set up selections
let links = svg.append("g")
  .attr("class", "links")
  .selectAll("line");
let nodes = svg.append("g")
  .attr("class", "nodes")
  .selectAll("circle");

此时没有圆圈线,D3选择是不可变的。这意味着无论何时调用nodes.data()links.data(),都会输入数据数组中的所有项目,因为选择中没有相应的元素可以更新或退出 - 选择仍为空(您可以运行节点。 size()每次更新都能看到这个)。

相反,您可以将linksnodes作为父g的选择:

// set up selections
let links = svg.append("g")
    .attr("class", "links");
let nodes = svg.append("g")
    .attr("class", "nodes");

并选择每次更新的所有链接/节点:

let node = nodes.selectAll("circle")
    .data(graph.nodes, function(d) {
        return d.id;
    });

(链接相同)

这样您就可以选择任何现有的链接/圈子,并且可以根据需要更新/退出/输入:

&#13;
&#13;
 const cb = {
      "edges": [{
        "source": "a",
        "target": "b",
        "value": 1
      }, {
        "source": "a",
        "target": "c",
        "value": 1
      }, {
        "source": "b",
        "target": "c",
        "value": 1
      }, {
        "source": "c",
        "target": "d",
        "value": 1
      }],
      "nodes": [{
        "id": "a",
        "pop": 12.00328963067508,
        "size": 5
      }, {
        "id": "b",
        "pop": 12.391087593534877,
        "size": 5
      }, {
        "id": "c",
        "pop": 12.384324067681156,
        "size": 5
      }, {
        "id": "d",
        "pop": 13.991090521661292,
        "size": 6
      }]
    }
    const sp = {
      "edges": [{
        "source": "a",
        "target": "b",
        "value": 1
      }, {
        "source": "a",
        "target": "e",
        "value": 1
      }, {
        "source": "b",
        "target": "f",
        "value": 1
      }, {
        "source": "e",
        "target": "f",
        "value": 1
      }],
      "nodes": [{
        "id": "a",
        "pop": 12.00328963067508,
        "size": 5
      }, {
        "id": "b",
        "pop": 12.391087593534877,
        "size": 5
      }, {
        "id": "e",
        "pop": 13.063656176168433,
        "size": 6
      }, {
        "id": "f",
        "pop": 12.52608275807238,
        "size": 5
      }]
    }
    // set margins and canvas size
    const margin = {
      top: 10,
      right: 20,
      bottom: 30,
      left: 30
    };
    const width = 600 - margin.left - margin.right;
    const height = 500 - margin.top - margin.bottom;

    // set up canvas
    const svg = d3.select('.chart')
      .append('svg')
      .attr('width', width + margin.left + margin.right)
      .attr('height', height + margin.top + margin.bottom)
      .call(responsivefy)
      .append('g')
      .attr('transform', `translate(${margin.left}, ${margin.top})`);

    // set up selections
    let links = svg.append("g")
      .attr("class", "links");
    let nodes = svg.append("g")
      .attr("class", "nodes");

    // set up color scale
    const color = d3.scaleSequential()
      .domain([8, 15])
      .interpolator(d3.interpolateInferno);

    // set up simulation basic parameters
    const simulation = d3.forceSimulation()
      .force("link", d3.forceLink().id(function(d) {
        return d.id;
      }))
      .force("charge", d3.forceManyBody())
      .force("center", d3.forceCenter(width / 2, height / 2));

    function render(graph) {
      // node selection and data handling
      let node = nodes.selectAll("circle")
        .data(graph.nodes, function(d) {
          return d.id;
        });

      // node general update pattern
      node.exit()
        .transition()
        .attr("r", 0)
        .remove();

      node = node
        .enter().append("circle")
        .attr("r", function(d) {
          return d.size;
        })
        .attr("fill", function(d) {
          return color(d.pop);
        })
        .call(d3.drag()
          .on("start", dragstarted)
          .on("drag", dragged)
          .on("end", dragended))
        .merge(node);
      // give all nodes a title with their id for hover identification
      node.append("title")
        .text(function(d) {
          return d.id;
        });

      // link selection, data handling
      let link = links.selectAll("line")
        .data(graph.edges, function(d) {
          return d.source + "-" + d.target;
        });

      // link general update pattern with attrTween to keep links connected to disappearing nodes
      link
        .exit()
        .transition()
        .attr("stroke-opacity", 0)
        .attrTween("x1", function(d) {
          return function() {
            return d.source.x;
          };
        })
        .attrTween("x2", function(d) {
          return function() {
            return d.target.x;
          };
        })
        .attrTween("y1", function(d) {
          return function() {
            return d.source.y;
          };
        })
        .attrTween("y2", function(d) {
          return function() {
            return d.target.y;
          };
        })
        .remove();

      link = link
        .enter().append("line")
        .attr("stroke-width", function(d) {
          return Math.sqrt(d.value);
        })
        .merge(link);

      // add nodes and links to the siumlation
      simulation
        .nodes(graph.nodes)
        .on("tick", ticked);
      simulation.force("link")
        .links(graph.edges);
      // restart the simulation
      simulation.alpha(1).restart();

      // set the ticked function to constantly update node and link position
      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("cx", function(d) {
            return d.x;
          })
          .attr("cy", function(d) {
            return d.y;
          });
      }
    };

    // initial render
    render(cb)

    // dragging functions
    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;
    }

    // responsivefy from https://brendansudol.com/writing/responsive-d3
    function responsivefy(svg) {
      // get container + svg aspect ratio
      const container = d3.select(svg.node().parentNode),
        width = parseInt(svg.style("width")),
        height = parseInt(svg.style("height")),
        aspect = width / height;

      // add viewBox and preserveAspectRatio properties,
      // and call resize so that svg resizes on inital page load
      svg.attr("viewBox", "0 0 " + width + " " + height)
        .attr("preserveAspectRatio", "xMinYMid")
        .call(resize);

      // to register multiple listeners for same event type,
      // you need to add namespace, i.e., 'click.foo'
      // necessary if you call invoke this function for multiple svgs
      // api docs: https://github.com/mbostock/d3/wiki/Selections#on
      d3.select(window).on("resize." + container.attr("id"), resize);

      // get width of container and resize svg to fit it
      function resize() {
        const targetWidth = parseInt(container.style("width"));
        svg.attr("width", targetWidth);
        svg.attr("height", Math.round(targetWidth / aspect));
      }
    }
&#13;
    .links line {
      stroke: #999;
      stroke-opacity: 0.6;
    }
    
    .nodes circle {
      stroke: #fff;
      stroke-width: 1.5px;
    }
&#13;
  <script src="https://d3js.org/d3.v4.min.js"></script>

  <button onclick=render(sp)>render new graph</button>
  <div class="chart"></div>
&#13;
&#13;
&#13;

或者,更新了pen