首页 > 解决方案 > d3 Force Directed Graph General Update Pattern - exit() 选择不起作用

问题描述

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

我认为这是我的选择没有正确处理数据的问题,但是我尝试了很多调整并且无法弄清楚我的问题。我试图尽可能地效仿Bostock 的例子,但它没有按预期工作。这是一个运行可视化的代码笔(注意:使用并排编辑器查看可视化最容易)。

提前致谢!

这是代码:

<!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>

标签: d3.js

解决方案


问题是您的选择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()输入数据数组中的所有项目,因为选择中没有相应的元素要更新或退出 - 选择保持为空(您可以运行 nodes.size() 每次更新以查看此内容)。

相反,您可以离开linksnodes作为 parent 的选择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;
    });

(链接也一样)

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

 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));
      }
    }
    .links line {
      stroke: #999;
      stroke-opacity: 0.6;
    }
    
    .nodes circle {
      stroke: #fff;
      stroke-width: 1.5px;
    }
  <script src="https://d3js.org/d3.v4.min.js"></script>

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

或者,更新的


推荐阅读