如何在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
相关产品推荐
相关产品推荐

