点击节点时如何实现含关联节点/边的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;
关键功能说明
- 缩放与平移:通过
d3.zoom()绑定到SVG容器,自动处理视图的缩放和拖拽平移,所有图形元素放在<g>容器中统一变换。 - 节点居中:计算目标节点的当前坐标,结合当前缩放级别,通过
zoom.transform()调整视图位置,实现平滑过渡居中。 - 适配视图:获取所有图形元素的边界框,计算合适的缩放比例和偏移量,让整个网络图完整适配容器。
- 关联节点高亮:点击节点时,遍历连线找出关联节点,通过修改样式实现高亮效果。
- React与D3协同:用React ref获取DOM元素,在useEffect中初始化D3逻辑,避免直接操作React管理的DOM,保证状态一致性。
内容的提问来源于stack exchange,提问作者user3237169
相关产品推荐
相关产品推荐

