d3 树状图:替换根保留旧树

d3 Dendrogram: Replacing root retains old tree

我有一个使用 d3.hierarchy 的树状图/聚类图的根。我正在尝试用一个选定的节点更新根,该节点应该成为新的头部,并在顶部绘制一棵用该节点绘制的新树。这应该取代旧树。步骤如下:

  1. 读入平面数据
  2. 使用 d3.stratify
  3. 转换为层次结构
  4. 将其转换为集群(带坐标等)
  5. 使用新的 select.join 绘制(不再需要显式退出/删除)
  6. 用户点击节点圈子
  7. 更新层次结构,将所选节点作为新根并删除父节点
  8. 重新绘制,节点不再出现在数据(父级及以上)中,由 join
  9. 移除

然而,它重新绘制了新的、更小的根和从属,但所有旧的 SVG 仍然存在。我已经尝试明确添加退出/删除,但这没有帮助。

我做错了什么?

可以在此处查看一个简化的、可重现的示例。我还在 https://jsfiddle.net/colourblue/zp7ujra3/9/

创建了一个 fiddle
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://d3js.org/d3.v6.js"></script>
</head>

<body>
    <div id="vis"></div>
    <script>
      let treeData = []
      let currentTreeData = []

      var flatData = [
        { "ID" : 1000, "name" : "The Root", "parentID":null},
        { "ID" : 1100, "name" : "Child 1", "parentID":1000 },
        { "ID" : 1110, "name" : "G.Child 1.1", "parentID":1100 },
        { "ID" : 1120, "name" : "G.Child 1.2", "parentID":1100 },
        { "ID" : 1130, "name" : "G.Child 1.3", "parentID":1100 },
        { "ID" : 1200, "name" : "Child 2", "parentID":1000 },
        { "ID" : 1210, "name" : "G.Child 2.1", "parentID":1200 },
        { "ID" : 1211, "name" : "G.G.Child 2.1.1", "parentID":1210 },
        { "ID" : 1212, "name" : "G.G.Child 2.2.2", "parentID":1210 },
        { "ID" : 12111, "name" : "G.G.G.Child 2.1.1.1", "parentID":1211 },
        { "ID" : 1300, "name" : "Child 3", "parentID":1000 }
      ];

        function chart(thisTreeData) {
          let root = clusterise(thisTreeData)
          // Add nodes (links)
          svg.append("g")
            .attr("class", "node")
            .attr("fill", "none")
            .attr("stroke", "#555")
            .attr("stroke-opacity", 0.3)
            .selectAll("path")
            .data(root.links(), function(d) { return "Link" + ":" + d.target.data.id })
            .join("path")
            .attr("d", d3.linkRadial()
                  .angle(d => d.x)
                  .radius(d => d.y));

          // Add circles
          svg.append("g")
            .attr("class", "node")
            .selectAll("circle")
            .data(root.descendants(), function(d) { return "Circle" + d.data.id; })
            .join("circle")
            .attr("transform", d => `
  rotate(${d.x * 180 / Math.PI - 90})
  translate(${d.y},0)
  `)
            .attr("r", 3)
            .on('click', click);

          // Add text
          svg.append("g")
            .attr("class", "node")
            .selectAll("text")
            .data(root.descendants(), function(d) { return "Text" + d.data.id; })
            .join("text")
            .attr("transform", d => `
  rotate(${d.x * 180 / Math.PI - 90}) 
  translate(${d.y},0) 
  rotate(${d.x >= Math.PI ? 180 : 0})
  `)
            .attr("text-anchor", d => d.x < Math.PI === !d.children ? "start" : "end")
            .text(d => d.data.data.name);
        }

        // Switch tree on click so centre is now selected node
        function click(event,d) {
          currentTreeData = findNode(treeData, d.data.id)
          chart(currentTreeData);
        }

        // HELPER FUNCTIONS
        // ----------------
        // Function to Strafify flat CSV data into a tree
        function convertToHierarchy(data) {
          var stratify = d3.stratify()
          .parentId(function (d) {
            return d.parentID;
          })
          .id(function (d) {
            return d.ID;
          });
          let treeData = stratify(data);
          return (treeData)
        }

        // Function to Create d3 cluster with coordinates etc from stratified data
        function clusterise(treeData) {
          tree = d3.cluster().size([2 * Math.PI, radius - 100])
          let root = tree(d3.hierarchy(treeData)
                          .sort((a, b) => d3.ascending(a.name, b.name)));
          return (root)
        }

        function findNode(root, id) {
          console.log(root);
          let selected = root.find(obj => obj.id === id);
          selected.parent= null;
          console.log(selected);
          return(selected)
        }

        width = 800
        height = 600 
        radius = width / 2

        let svg = d3.select("#vis")
        .append('svg')
        .attr('width', width)
        .attr('height', height)
        .append('g')
        .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');

        treeData = convertToHierarchy(flatData)
        currentTreeData = treeData
        chart(currentTreeData);
    </script>
</body>
</html>

这是一个更复杂的示例,它使用新的 .join 方法正确处理 enterupdateexit 模式。这确实允许您添加过渡。请注意,我删除了您的内部包装 g 节点。由于每次点击都会附加一个新的点击,这会弄乱您对可见节点(路径、圆圈和文本)的选择。

<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://d3js.org/d3.v6.js"></script>
  </head>

  <body>
    <div id="vis"></div>
    <script>
      let treeData = [];
      let currentTreeData = [];

      var flatData = [
        { ID: 1000, name: 'The Root', parentID: null },
        { ID: 1100, name: 'Child 1', parentID: 1000 },
        { ID: 1110, name: 'G.Child 1.1', parentID: 1100 },
        { ID: 1120, name: 'G.Child 1.2', parentID: 1100 },
        { ID: 1130, name: 'G.Child 1.3', parentID: 1100 },
        { ID: 1200, name: 'Child 2', parentID: 1000 },
        { ID: 1210, name: 'G.Child 2.1', parentID: 1200 },
        { ID: 1211, name: 'G.G.Child 2.1.1', parentID: 1210 },
        { ID: 1212, name: 'G.G.Child 2.2.2', parentID: 1210 },
        { ID: 12111, name: 'G.G.G.Child 2.1.1.1', parentID: 1211 },
        { ID: 1300, name: 'Child 3', parentID: 1000 },
      ];

      function chart(thisTreeData) {
        let root = clusterise(thisTreeData);

        // Add nodes (links)
        svg
          .selectAll('.line')
          .data(root.links(), function (d) {
            return 'Link' + ':' + d.target.data.id;
          })
          .join(
            function (enter) {
              return enter
                .append('path')
                .attr('class', 'line')
                .attr(
                  'd',
                  d3
                    .linkRadial()
                    .angle((d) => d.x)
                    .radius((d) => d.y)
                )
                .attr('fill', 'none')
                .attr('stroke', '#555')
                .attr('stroke-opacity', 0.3);
            },
            function (update) {
              update
                .transition()
                .duration(1000)
                .attr(
                  'd',
                  d3
                    .linkRadial()
                    .angle((d) => d.x)
                    .radius((d) => d.y)
                );
              return update;
            },
            function (exit) {
              return exit.remove();
            }
          );

        // Add text
        svg
          .selectAll('.word')
          .data(root.descendants(), function (d) {
            return 'Text' + d.data.id;
          })
          .join(
            function (enter) {
              return enter
                .append('text')
                .attr('class', 'word')
                .attr(
                  'transform',
                  (d) => `
      rotate(${(d.x * 180) / Math.PI - 90})
      translate(${d.y},0)
      rotate(${d.x >= Math.PI ? 180 : 0})
      `
                )
                .attr('text-anchor', (d) =>
                  d.x < Math.PI === !d.children ? 'start' : 'end'
                )
                .text((d) => d.data.data.name);
            },
            function (update) {
              update
                .transition()
                .duration(1000)
                .attr(
                  'transform',
                  (d) => `
      rotate(${(d.x * 180) / Math.PI - 90})
      translate(${d.y},0)
      rotate(${d.x >= Math.PI ? 180 : 0})
      `
                );
              return update;
            },
            function (exit) {
              return exit.remove();
            }
          );

        // Add circles
        svg
          .selectAll('.round')
          .data(root.descendants(), function (d) {
            return 'circle' + d.data.id;
          })
          .join(
            function (enter) {
              return enter
                .append('circle')
                .attr('class', 'round')
                .attr(
                  'transform',
                  (d) => `
      rotate(${(d.x * 180) / Math.PI - 90})
      translate(${d.y},0)
      `
                )
                .attr('r', 5)
                .on('click', click);
            },
            function (update) {
              update
                .transition()
                .duration(1000)
                .attr(
                  'transform',
                  (d) => `
      rotate(${(d.x * 180) / Math.PI - 90})
      translate(${d.y},0)
      `
                );
              return update;
            },
            function (exit) {
              return exit.remove();
            }
          );
      }

      // Switch tree on click so centre is now selected node
      function click(event, d) {
        currentTreeData = findNode(treeData, d.data.id);
        chart(currentTreeData);
      }

      // HELPER FUNCTIONS
      // ----------------
      // Function to Strafify flat CSV data into a tree
      function convertToHierarchy(data) {
        var stratify = d3
          .stratify()
          .parentId(function (d) {
            return d.parentID;
          })
          .id(function (d) {
            return d.ID;
          });
        let treeData = stratify(data);
        return treeData;
      }

      // Function to Create d3 cluster with coordinates etc from stratified data
      function clusterise(treeData) {
        tree = d3.cluster().size([2 * Math.PI, radius - 100]);
        let root = tree(
          d3.hierarchy(treeData).sort((a, b) => d3.ascending(a.name, b.name))
        );
        return root;
      }

      function findNode(root, id) {
        //console.log(root);
        let selected = root.find((obj) => obj.id === id);
        selected.parent = null;
        //console.log(selected);
        return selected;
      }

      width = 800;
      height = 600;
      radius = width / 2;

      let svg = d3
        .select('#vis')
        .append('svg')
        .attr('width', width)
        .attr('height', height)
        .append('g')
        .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');

      treeData = convertToHierarchy(flatData);
      currentTreeData = treeData;
      chart(currentTreeData);
    </script>
  </body>
</html>