在已启用拖拽、缩放和平移的D3力导向图中自定义节点
解决方案
要替换自定义节点,核心是将原来创建圆形节点的逻辑改为创建<g>容器,在容器内添加头像、文本等元素,同时调整力模拟的位置更新逻辑,因为<g>使用transform定位而非cx/cy。
修改后完整代码
const ForceGraph = () => { const data = { name: "CEO", color: "#123", other_details: "test", url: "https://t4.ftcdn.net/jpg/01/43/42/83/360_F_143428338_gcxw3Jcd0tJpkvvb53pfEztwtU9sxsgT.jpg", children: [{ name: "Manager 1", color: "#312", other_details: "test2", url: "https://t4.ftcdn.net/jpg/01/43/42/83/360_F_143428338_gcxw3Jcd0tJpkvvb53pfEztwtU9sxsgT.jpg", children: [{ name: "Client 1", color: "#a41", other_details: "test4", url: "https://t4.ftcdn.net/jpg/01/43/42/83/360_F_143428338_gcxw3Jcd0tJpkvvb53pfEztwtU9sxsgT.jpg", children: [] },{ name: "Client 2", color: "#943", other_details: "test5", url: "https://t4.ftcdn.net/jpg/01/43/42/83/360_F_143428338_gcxw3Jcd0tJpkvvb53pfEztwtU9sxsgT.jpg", children: [] }] },{ name: "Manager 2", color: "#a41", other_details: "test3", url: "https://t4.ftcdn.net/jpg/01/43/42/83/360_F_143428338_gcxw3Jcd0tJpkvvb53pfEztwtU9sxsgT.jpg", children: [{ name: "Client 3", color: "#921", other_details: "test6", url: "https://t4.ftcdn.net/jpg/01/43/42/83/360_F_143428338_gcxw3Jcd0tJpkvvb53pfEztwtU9sxsgT.jpg", children: [] }] }] }; const svgRef = React.useRef(null); React.useEffect(() => { const width = 928; const height = 200; const drag = simulation => { function dragstarted(event, d) { if (!event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(event, d) { d.fx = event.x; d.fy = event.y; } function dragended(event, d) { if (!event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; } return d3.drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended); }; const root = d3.hierarchy(data); const links = root.links(); const nodes = root.descendants(); const simulation = d3.forceSimulation(nodes) .force("link", d3.forceLink(links).id(d => d.id).distance(80).strength(1)) .force("charge", d3.forceManyBody().strength(-200)) .force("x", d3.forceX()) .force("y", d3.forceY()); const svg = d3.select(svgRef.current); svg.selectAll('*').remove(); svg.attr("width", width) .attr("height", height) .attr("viewBox", [-width / 2, -height / 2, width, height]) .attr("style", "max-width: 100%; height: auto;") .attr("border", "1px solid red"); // 创建容器包裹连线和节点,避免缩放冲突 const container = svg.append("g"); // 连线部分 const link = container.append("g") .attr("stroke", "#999") .attr("stroke-opacity", 0.6) .selectAll("line") .data(links) .join("line") .attr("stroke-width", 1.5); // 自定义节点部分 const node = container.append("g") .selectAll("g") .data(nodes) .join("g") .call(drag(simulation)); // 添加圆形头像 node.append("image") .attr("xlink:href", d => d.data.url) .attr("width", 30) .attr("height", 30) .attr("rx", 15) .attr("ry", 15) .attr("x", -15) .attr("y", -15); // 文本背景(可选,提升可读性) node.append("rect") .attr("x", 18) .attr("y", -12) .attr("width", d => Math.max(d.data.name.length *7, d.data.other_details.length*7)+8) .attr("height", 25) .attr("fill", "white") .attr("opacity", 0.9) .attr("rx",4); // 添加名称文本 node.append("text") .attr("x",22) .attr("y",0) .style("font-size","12px") .style("font-weight","bold") .text(d => d.data.name); // 添加额外信息文本 node.append("text") .attr("x",22) .attr("y",15) .style("font-size","10px") .style("fill","#666") .text(d => d.data.other_details); // 力模拟更新逻辑 simulation.on("tick", () => { link .attr("x1", d => d.source.x) .attr("y1", d => d.source.y) .attr("x2", d => d.target.x) .attr("y2", d => d.target.y); node .attr("transform", d => `translate(${d.x}, ${d.y})`); }); // 缩放处理 const zoom = d3.zoom() .scaleExtent([0.1,5]) .on('zoom', (event) => { container.attr('transform', event.transform); }); svg.call(zoom); return () => simulation.stop(); }, []); return ( <div> <div style={{border: "1px solid red"}}> <svg ref={svgRef}></svg> </div> <div style={{margin: "10px", display: "flex", alignItems: "center"}}> <img style={{borderRadius: "50%", height:"30px", width: "30px", border: "1px solid red"}} src="https://t4.ftcdn.net/jpg/01/43/42/83/360_F_143428338_gcxw3Jcd0tJpkvvb53pfEztwtU9sxsgT.jpg"/> <div style={{display: "flex", flexDirection: "column", marginLeft:"5px"}}> <div style={{fontSize: "18px", fontWeight: "bold"}}>CEO</div> <div style={{fontSize: "13px"}}>Other info</div> </div> </div> </div> ); }; ReactDOM.render(<ForceGraph />, document.querySelector("#app"));
关键修改点
- 节点容器替换:将
<circle>改为<g>容器,在其中添加头像、文本背景、名称和额外信息文本。 - 定位方式调整:力模拟的
tick事件中,用transform: translate(x,y)替代cx/cy定位节点。 - 力参数优化:增大
distance和charge值,避免自定义节点因尺寸较大而重叠拥挤。 - 缩放逻辑修复:
相关产品推荐
相关产品推荐

