【问题标题】:Need to shift link of child node to parent once parent collapsed in d3 graph一旦父节点在 d3 图中折叠,需要将子节点的链接转移到父节点
【发布时间】:2021-05-21 08:14:59
【问题描述】:

这是我想用 d3 js 实现的屏幕。

点击有子圈子的圈子可以切换(折叠/展开)。

我已尝试使用以下 sn-p。我唯一无法实现的是,一旦父圈子崩溃,我需要将子圈子的所有链接转移到其父圈子。任何线索都会很棒。

const data = {
    name: "root",
  children: [
    {
        name: "A",
      children: [
        {name: "A1", value: 7}, {name: "A2", value: 8}, {name: "A3", value: 9}, {name: "A4", value: 10}, {name: "A5", value: 10}
      ]
    },
    {
        name: "B",
      children: [
        {name: "B1", value: 11}, {name: "B2", value: 7}, {name: "B3", value: 8},
      ]
    },
    {
      name: "C",
      value: 10
    },
    {
      name: "D",
      value: 10
    },
    {
      name: "E",
      value: 10
    }
  ],
  links: [{from: "A3", to: "C"}, {from: "A2", to: "E"}, {from: "B1", to: "D"}, {from: "B2", to: "B3"}, {from: "B1", to: "C"}]
};

const findNode = (parent, name) => {
    if (parent.name === name)
    return parent;
  if (parent.children) {
    for (let child of parent.children) {
        const found = findNode(child, name);
      if (found) {
        return found;
      }
    }
  } 
  return null;
}

const svg = d3.select("svg");

const container = svg.append('g')
  .attr('transform', 'translate(0,0)')
  
const onClickNode = (e, d) => {
  e.stopPropagation();
  e.preventDefault();
  
  const node = findNode(data, d.data.name);
  if(node.children && !node._children) {
    node._children = node.children;
    node.children = undefined;
    node.value = 20;
    updateGraph(data);
  } else {
    if (node._children && !node.children) {
        node.children = node._children;
      node._children = undefined;
      node.value = undefined;
      updateGraph(data);
    }
  }
}  

const updateGraph = graphData => {
    const pack = data => d3.pack()
    .size([600, 600])
    .padding(0)
    (d3.hierarchy(data)
    .sum(d => d.value * 3.5)
    .sort((a, b) => b.value - a.value));

    const root = pack(graphData);    
    
    const nodes = root.descendants().slice(1);  
  console.log('NODES: ', nodes);

    const nodeElements = container
    .selectAll("g.node")
    .data(nodes, d => d.data.name);
    
    const addedNodes = nodeElements.enter()
    .append("g")
    .classed('node', true)
    .style('cursor', 'pointer')
    .on('click', (e, d) => onClickNode(e, d));
    
  addedNodes.append('circle')
    .attr('stroke', 'black')
  
  addedNodes.append("text")
    .text(d => d.data.name)
    .attr('text-anchor', 'middle')
    .attr('alignment-baseline', 'middle')
    .style('visibility', 'hidden')
    .style('fill', 'black');
  
  const mergedNodes = addedNodes.merge(nodeElements);
  mergedNodes
    .transition()
    .duration(500)
    .attr('transform', d => `translate(${d.x},${d.y})`);
    
  mergedNodes.select('circle')
    .attr("fill", d => d.children ? "#ffe0e0" : "#ffefef")
    .transition()
    .duration(1000)
    .attr('r', d => d.value)
    mergedNodes.select('text')
    .attr('dy', d => d.children ? d.value + 10 : 0)
    .transition()
    .delay(1000)
    .style('visibility', 'visible')
    
  const exitedNodes = nodeElements.exit()
  exitedNodes.select('circle')
    .transition()
    .duration(500)
    .attr('r', 1);
 exitedNodes.select('text')
   .remove();   
    
 exitedNodes   
    .transition()
    .duration(750)
    .remove();

    const linkPath = d => {
        const from = nodes.find(n => n.data.name === d.from);
        const to = nodes.find(n => n.data.name === d.to);
    if (!from || !to)
        return null;
      
        const length = Math.hypot(from.x - to.x, from.y - to.y);
        const fd = from.value / length;
        const fx = from.x + (to.x - from.x) * fd;
        const fy = from.y + (to.y - from.y) * fd;
 
        const td = to.value / length;
        const tx = to.x + (from.x - to.x) * td;
        const ty = to.y + (from.y - to.y) * td;
        return `M ${fx},${fy} L ${tx},${ty}`; 
    };  
  
  const linkElements = container.selectAll('path.link')
    .data(data.links.filter(linkPath));
  
  const addedLinks = linkElements.enter()
    .append('path')
    .classed('link', true)
    .attr('marker-end', 'url(#arrowhead-to)')
    .attr('marker-start', 'url(#arrowhead-from)');
    
    addedLinks.merge(linkElements)
        .transition()
    .delay(750)
    .attr('d', linkPath)
    
  linkElements.exit().remove();  
}  

updateGraph(data);
text {
  font-family: "Ubuntu";
  font-size: 12px;
}

.link {
  stroke: blue;
  fill: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>

<svg width="600" height="600">
  <defs>
    <marker id="arrowhead-to" markerWidth="10" markerHeight="7" 
    refX="10" refY="3.5" orient="auto">
      <polygon fill="blue" points="0 0, 10 3.5, 0 7" />
    </marker>
    <marker id="arrowhead-from" markerWidth="10" markerHeight="7" 
    refX="0" refY="3.5" orient="auto">
      <polygon fill="blue" points="10 0, 0 3.5, 10 7" />
    </marker>
  </defs>
</svg>

【问题讨论】:

    标签: javascript svg d3.js d3-graphviz


    【解决方案1】:

    替换折叠节点的链接:

    const links = data.links.map(link => {
      let from = nodes.find(n => n.data.name === link.from);
      if (!from) {
        const ancestors = findNodeAncestors(data, link.from);
        for (let index = 1; !from && index < ancestors.length  -1; index++) {
          from = nodes.find(n => n.data.name === ancestors[index].name)
        }
      }
      let to = nodes.find(n => n.data.name === link.to);
      if (!to) {
        const ancestors = findNodeAncestors(data, link.to);
        for (let index = 1; !to && index < ancestors.length  -1; index++) {
          to = nodes.find(n => n.data.name === ancestors[index].name)
        }
      }
      return {from, to};
    });
    

    这是findNodeAncestors 的代码:

    const findNodeAncestors = (parent, name) => {
      if (parent.name === name)
        return [parent];
      const children = parent.children || parent._children;   
      if (children) {
        for (let child of children) {
          const found = findNodeAncestors(child, name);
          if (found) {
            return [...found, parent];
          }
        }
      } 
      return null;
    }
    

    const data = {
        name: "root",
      children: [
        {
            name: "A",
          children: [
            {name: "A1", value: 7}, {name: "A2", value: 8}, {name: "A3", value: 9}, {name: "A4", value: 10}, {name: "A5", value: 10}
          ]
        },
        {
            name: "B",
          children: [
            {name: "B1", value: 11}, {name: "B2", value: 7}, {name: "B3", value: 8},
          ]
        },
        {
          name: "C",
          value: 10
        },
        {
          name: "D",
          value: 10
        },
        {
          name: "E",
          value: 10
        }
      ],
      links: [{from: "A3", to: "C"}, {from: "A2", to: "E"}, {from: "B1", to: "D"}, {from: "B2", to: "B3"}, {from: "B1", to: "C"}, {from: "B1", to: "A5"}]
    };
    
    const findNode = (parent, name) => {
        if (parent.name === name)
        return parent;
      if (parent.children) {
        for (let child of parent.children) {
            const found = findNode(child, name);
          if (found) {
            return found;
          }
        }
      } 
      return null;
    }
    
    const findNodeAncestors = (parent, name) => {
        if (parent.name === name)
        return [parent];
      const children = parent.children || parent._children;   
      if (children) {
        for (let child of children) {
            const found = findNodeAncestors(child, name);
          //console.log('FOUND: ', found);
          if (found) {
            return [...found, parent];
          }
        }
      } 
      return null;
    }
    
    const svg = d3.select("svg");
    
    const container = svg.append('g')
      .attr('transform', 'translate(0,0)')
      
    const onClickNode = (e, d) => {
      e.stopPropagation();
      e.preventDefault();
      
      const node = findNode(data, d.data.name);
      if(node.children && !node._children) {
        node._children = node.children;
        node.children = undefined;
        node.value = 20;
        updateGraph(data);
      } else {
        if (node._children && !node.children) {
            node.children = node._children;
          node._children = undefined;
          node.value = undefined;
          updateGraph(data);
        }
      }
    }  
    
    const updateGraph = graphData => {
        const pack = data => d3.pack()
        .size([600, 600])
        .padding(0)
        (d3.hierarchy(data)
        .sum(d => d.value * 3.5)
        .sort((a, b) => b.value - a.value));
    
        const root = pack(graphData);        
        const nodes = root.descendants().slice(1);  
    
        const nodeElements = container
        .selectAll("g.node")
        .data(nodes, d => d.data.name);
        
        const addedNodes = nodeElements.enter()
        .append("g")
        .classed('node', true)
        .style('cursor', 'pointer')
        .on('click', (e, d) => onClickNode(e, d));
        
      addedNodes.append('circle')
        .attr('stroke', 'black')
      
      addedNodes.append("text")
        .text(d => d.data.name)
        .attr('text-anchor', 'middle')
        .attr('alignment-baseline', 'middle')
        .style('visibility', 'hidden')
        .style('fill', 'black');
      
      const mergedNodes = addedNodes.merge(nodeElements);
      mergedNodes
        .transition()
        .duration(500)
        .attr('transform', d => `translate(${d.x},${d.y})`);
        
      mergedNodes.select('circle')
        .attr("fill", d => d.children ? "#ffe0e0" : "#ffefef")
        .transition()
        .duration(1000)
        .attr('r', d => d.value)
        mergedNodes.select('text')
        .attr('dy', d => d.children ? d.value + 10 : 0)
        .transition()
        .delay(1000)
        .style('visibility', 'visible')
        
      const exitedNodes = nodeElements.exit()
      exitedNodes.select('circle')
        .transition()
        .duration(500)
        .attr('r', 1);
     exitedNodes.select('text')
       .remove();   
        
     exitedNodes   
        .transition()
        .duration(750)
        .remove();
    
        const linkPath = d => {
            const length = Math.hypot(d.from.x - d.to.x, d.from.y - d.to.y);
            const fd = d.from.value / length;
            const fx = d.from.x + (d.to.x - d.from.x) * fd;
            const fy = d.from.y + (d.to.y - d.from.y) * fd;
     
            const td = d.to.value / length;
            const tx = d.to.x + (d.from.x - d.to.x) * td;
            const ty = d.to.y + (d.from.y - d.to.y) * td;
        
            return `M ${fx},${fy} L ${tx},${ty}`; 
        };
      
      const links = data.links.map(link => {
            let from = nodes.find(n => n.data.name === link.from);
        if (!from) {
            const ancestors = findNodeAncestors(data, link.from);
          for (let index = 1; !from && index < ancestors.length  -1; index++) {
            from = nodes.find(n => n.data.name === ancestors[index].name)
          }
        }
            let to = nodes.find(n => n.data.name === link.to);
        if (!to) {
            const ancestors = findNodeAncestors(data, link.to);
          for (let index = 1; !to && index < ancestors.length  -1; index++) {
            to = nodes.find(n => n.data.name === ancestors[index].name)
          }
        }
        return {from, to};
      });
      
      const linkElements = container.selectAll('path.link')
        .data(links.filter(l => l.from && l.to));
      
      const addedLinks = linkElements.enter()
        .append('path')
        .classed('link', true)
        .attr('marker-end', 'url(#arrowhead-to)')
        .attr('marker-start', 'url(#arrowhead-from)');
        
        addedLinks.merge(linkElements)
            .style('visibility', 'hidden')
        .transition()
        .delay(750)
        .attr('d', linkPath)
            .style('visibility', 'visible')
        
      linkElements.exit().remove();  
    }  
    
    updateGraph(data);
    text {
      font-family: "Ubuntu";
      font-size: 12px;
    }
    
    .link {
      stroke: blue;
      fill: none;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
    
    <svg width="600" height="600">
      <defs>
        <marker id="arrowhead-to" markerWidth="10" markerHeight="7" 
        refX="10" refY="3.5" orient="auto">
          <polygon fill="blue" points="0 0, 10 3.5, 0 7" />
        </marker>
        <marker id="arrowhead-from" markerWidth="10" markerHeight="7" 
        refX="0" refY="3.5" orient="auto">
          <polygon fill="blue" points="10 0, 0 3.5, 10 7" />
        </marker>
      </defs>
    </svg>

    【讨论】:

    • 感谢您的帮助。一切正常,但现在一个挑战是,如果 A 的内部节点和 B 的内部节点存在连接,并且两者都崩溃了,那么链接不会转移到 A 和 B。我会尝试从我这边调试同时,如果您也可以尝试更多地帮助我,那就太好了。再次感谢。
    • 已解决链接过滤出现问题。再次感谢您的帮助。
    • 很好的例子......为什么 A 子节点完全在 A 容器内,但 B 子节点与 B 容器重叠?
    • @RobinMackenzie 我使用标准的 D3 圆形包装。唯一要更改的参数是d3.pack 的大小、填充和总和。我考虑在我的项目中使用圆形包,并且可能会编写我自己的打包算法,而不是使用 D3 的。
    猜你喜欢
    • 2023-03-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多