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

点击节点时如何实现含关联节点/边的zoom、pan、居中与scale to fit(D3/React)

React + D3 关联网络图(带缩放、平移、居中、适配视图)

以下是符合需求的完整实现示例,包含你需要的所有核心功能,同时支持关联节点高亮:

核心功能实现要点

  • 基于D3力导向图构建关联网络
  • 集成D3原生缩放(zoom)、平移(pan)功能
  • 实现节点居中、**视图适配(scale to fit)**按钮
  • 点击节点高亮关联节点与连线
  • React与D3状态协同,避免DOM冲突

完整代码示例

import React, { useEffect, useRef, useState } from 'react';
import * as d3 from 'd3';

const NetworkGraph = () => {
  const svgRef = useRef(null);
  const containerRef = useRef(null);
  const [graphData, setGraphData] = useState({
    nodes: [
      { id: 'wine1', group: 'wine', label: '赤霞珠' },
      { id: 'cheese1', group: 'cheese', label: '切达奶酪' },
      { id: 'wine2', group: 'wine', label: '黑皮诺' },
      { id: 'cheese2', group: 'cheese', label: '布里奶酪' },
      { id: 'wine3', group: 'wine', label: '雷司令' },
      { id: 'cheese3', group: 'cheese', label: '戈贡佐拉奶酪' },
    ],
    links: [
      { source: 'wine1', target: 'cheese1', value: 1 },
      { source: 'wine2', target: 'cheese2', value: 1 },
      { source: 'wine3', target: 'cheese3', value: 1 },
      { source: 'wine1', target: 'cheese2', value: 0.5 },
    ]
  });

  useEffect(() => {
    if (!svgRef.current || !containerRef.current) return;

    const container = containerRef.current;
    const width = container.clientWidth;
    const height = container.clientHeight;

    // 清除旧内容
    d3.select(svgRef.current).selectAll('*').remove();

    const svg = d3.select(svgRef.current)
      .attr('width', width)
      .attr('height', height);

    // 添加缩放容器
    const g = svg.append('g').attr('class', 'graph-group');

    // 初始化缩放行为
    const zoom = d3.zoom()
      .scaleExtent([0.2, 3])
      .on('zoom', (event) => {
        g.attr('transform', event.transform);
      });

    svg.call(zoom);

    // 初始化力导向图
    const simulation = d3.forceSimulation(graphData.nodes)
      .force('link', d3.forceLink(graphData.links).id(d => d.id).distance(120))
      .force('charge', d3.forceManyBody().strength(-300))
      .force('center', d3.forceCenter(width / 2, height / 2));

    // 绘制连线
    const links = g.append('g')
      .selectAll('line')
      .data(graphData.links)
      .join('line')
      .attr('stroke', '#999')
      .attr('stroke-opacity', 0.6)
      .attr('stroke-width', d => Math.sqrt(d.value) * 2);

    // 绘制节点
    const nodes = g.append('g')
      .selectAll('circle')
      .data(graphData.nodes)
      .join('circle')
      .attr('r', 15)
      .attr('fill', d => d.group === 'wine' ? '#8B0000' : '#FFD700')
      .attr('cursor', 'pointer')
      .on('click', handleNodeClick)
      .call(d3.drag()
        .on('start', dragstarted)
        .on('drag', dragged)
        .on('end', dragended));

    // 节点标签
    const labels = g.append('g')
      .selectAll('text')
      .data(graphData.nodes)
      .join('text')
      .attr('font-size', 12)
      .attr('text-anchor', 'middle')
      .attr('dy', 25)
      .text(d => d.label);

    // 力导向图tick事件更新位置
    simulation.on('tick', () => {
      links
        .attr('x1', d => d.source.x)
        .attr('y1', d => d.source.y)
        .attr('x2', d => d.target.x)
        .attr('y2', d => d.target.y);

      nodes
        .attr('cx', d => d.x)
        .attr('cy', d => d.y);

      labels
        .attr('x', d => d.x)
        .attr('y', d => d.y);
    });

    // 拖拽函数
    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;
    }

    // 节点点击高亮关联元素
    function handleNodeClick(event, clickedNode) {
      // 重置所有样式
      nodes.attr('fill', d => d.group === 'wine' ? '#8B0000' : '#FFD700');
      links.attr('stroke', '#999').attr('stroke-opacity', 0.6);

      // 高亮当前节点
      d3.select(event.target).attr('fill', '#FF4500');

      // 高亮关联节点和连线
      const connectedNodeIds = new Set();
      connectedNodeIds.add(clickedNode.id);

      links.each(function(link) {
        if (link.source.id === clickedNode.id || link.target.id === clickedNode.id) {
          d3.select(this).attr('stroke', '#FF4500').attr('stroke-opacity', 1);
          connectedNodeIds.add(link.source.id);
          connectedNodeIds.add(link.target.id);
        }
      });

      nodes.each(function(node) {
        if (connectedNodeIds.has(node.id) && node.id !== clickedNode.id) {
          d3.select(this).attr('fill', '#FF8C00');
        }
      });
    }

    // 暴露缩放控制方法到ref
    svgRef.current.__zoomControl = {
      centerNode: (nodeId) => {
        const node = graphData.nodes.find(n => n.id === nodeId);
        if (!node) return;

        const transform = d3.zoomTransform(svg.node());
        const translateX = width / 2 - transform.k * node.x;
        const translateY = height / 2 - transform.k * node.y;

        svg.transition().duration(500).call(zoom.transform, transform.translate(translateX, translateY));
      },
      scaleToFit: () => {
        const bounds = g.node().getBBox();
        const fullWidth = width;
        const fullHeight = height;
        const widthRatio = fullWidth / bounds.width;
        const heightRatio = fullHeight / bounds.height;
        const scale = Math.min(widthRatio, heightRatio) * 0.8; // 留边距
        const translateX = fullWidth / 2 - scale * bounds.x - scale * bounds.width / 2;
        const translateY = fullHeight / 2 - scale * bounds.y - scale * bounds.height / 2;

        svg.transition().duration(750).call(zoom.transform, d3.zoomIdentity.translate(translateX, translateY).scale(scale));
      }
    };

    return () => {
      simulation.stop();
    };
  }, [graphData]);

  // 控制按钮点击事件
  const handleCenterFirstNode = () => {
    svgRef.current?.__zoomControl?.centerNode('wine1');
  };

  const handleScaleToFit = () => {
    svgRef.current?.__zoomControl?.scaleToFit();
  };

  return (
    <div style={{ width: '100%', height: '600px', border: '1px solid #eee' }} ref={containerRef}>
      <div style={{ padding: '10px', background: '#f5f5f5' }}>
        <button onClick={handleCenterFirstNode} style={{ marginRight: '10px', padding: '6px 12px' }}>
          居中第一个节点
        </button>
        <button onClick={handleScaleToFit} style={{ padding: '6px 12px' }}>
          适配视图
        </button>
      </div>
      <svg ref={svgRef}></svg>
    </div>
  );
};

export default NetworkGraph;

关键功能说明

  1. 缩放与平移:通过d3.zoom()绑定到SVG容器,自动处理视图的缩放和拖拽平移,所有图形元素放在<g>容器中统一变换。
  2. 节点居中:计算目标节点的当前坐标,结合当前缩放级别,通过zoom.transform()调整视图位置,实现平滑过渡居中。
  3. 适配视图:获取所有图形元素的边界框,计算合适的缩放比例和偏移量,让整个网络图完整适配容器。
  4. 关联节点高亮:点击节点时,遍历连线找出关联节点,通过修改样式实现高亮效果。
  5. React与D3协同:用React ref获取DOM元素,在useEffect中初始化D3逻辑,避免直接操作React管理的DOM,保证状态一致性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 18:50:43