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

在已启用拖拽、缩放和平移的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"));

关键修改点

  1. 节点容器替换:将<circle>改为<g>容器,在其中添加头像、文本背景、名称和额外信息文本。
  2. 定位方式调整:力模拟的tick事件中,用transform: translate(x,y)替代cx/cy定位节点。
  3. 力参数优化:增大distance和charge值,避免自定义节点因尺寸较大而重叠拥挤。
  4. 缩放逻辑修复:
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 10:15:57