You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在D3.js Circle Pack布局中固定单个外层圆圈位置且不影响其他节点

D3.js Circle Pack布局固定单个节点位置实现方案

可以实现该需求,核心思路是在D3 pack布局完成原生坐标计算后,通过全局偏移校准将指定节点移动到目标位置,其余节点保持原有相对位置完全不变,无需修改布局原生计算逻辑,支持动态切换固定节点。

核心改动点

  • 新增固定节点全局配置,存储当前需要固定的节点名称和目标坐标
  • 点击节点即可将其设为固定节点,再次点击可取消固定
  • 布局计算完成后统一校准所有节点坐标,保证指定节点落在目标位置

完整实现代码

HTML

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

CSS

text {
  font-family: "Ubuntu";
  font-size: 12px;
}

.link {
  stroke: blue;
  fill: none;
}

JS

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"}]
};

// 新增固定节点配置:name为固定节点名称,x、y为目标固定坐标(示例为左上角位置)
let fixedNode = {name: null, x: 50, y: 50};

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();
  
  // 新增固定节点切换逻辑:点击节点设为固定,再次点击取消
  if (fixedNode.name === d.data.name) {
    fixedNode.name = null;
  } else {
    fixedNode.name = d.data.name;
  }
  
  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);    

    // 新增坐标偏移校准逻辑
    if (fixedNode.name) {
      const targetNode = root.descendants().find(d => d.data.name === fixedNode.name);
      if (targetNode) {
        // 计算需要偏移的距离
        const offsetX = fixedNode.x - targetNode.x;
        const offsetY = fixedNode.y - targetNode.y;
        // 所有节点统一偏移,保持相对位置不变
        root.descendants().forEach(d => {
          d.x += offsetX;
          d.y += offsetY;
        });
      }
    }
    
    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();  
}  

// 初始化默认固定C节点,不需要的话注释即可
fixedNode.name = "C";
updateGraph(data);

你可以按需修改fixedNode的x和y属性调整固定位置,也可以根据业务逻辑修改触发固定节点的规则,不需要绑定点击事件。

内容的提问来源于stack exchange,提问作者ranjeet kumar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.19 17:54:05