D3.js v6+React力导向图无法拖拽整个画布问题排查
问题
使用D3 v6结合React开发了具备缩放功能与可拖拽节点的力导向图。由于图表会随动态数据变得复杂庞大,希望用户能够拖拽包裹所有节点的<g>元素(尤其是图表放大后),但该<g>元素调用与单个节点相同的拖拽函数却无任何响应,尝试拆分拖拽函数也未解决问题。忽略TypeScript相关问题,以下是相关代码:
const data = jsonFyStory(selectedVariable, stories) const links = data.links.map((d) => d) const nodes = data.nodes.map((d: any) => d) const containerRect = container.getBoundingClientRect() const height = containerRect.height const width = containerRect.width function dragstarted() { // @ts-ignore d3.select(this).classed('fixing', true) setDisplayCta(false) setDisplayNodeDescription(false) setNodeData({}) } function dragged(event: DragEvent, d: any) { d.fx = event.x d.fy = event.y simulation.alpha(1).restart() setDisplayNodeDescription(true) d.class === 'story-node' && setDisplayCta(true) setNodeData({ name: d.name as string, class: d.class as string, definition: d.definition as string, summary: d.summary as string, }) } // dragended function in case we move away from sticky dragging! function dragended(event: DragEvent, d: DNode) { // @ts-ignore d3.select(this).classed('fixed', true) console.log(d) } function click(event: TouchEvent, d: DNode) { delete d.fx delete d.fy console.log(d) // @ts-ignore d3.select(this).classed('fixed', false) // @ts-ignore d3.select(this).classed('fixing', false) simulation.alpha(1).restart() } const simulation = d3 .forceSimulation(nodes as any[]) .force( 'link', d3.forceLink(links).id((d: any) => d.id) ) .force('charge', d3.forceManyBody().strength(isMobile ? -600 : -1300)) .force('collision', d3.forceCollide().radius(isMobile ? 5 : 20)) .force('x', d3.forceX()) .force('y', d3.forceY()) if (container.children) { d3.select(container).selectAll('*').remove() } const zoom = d3 .zoom() .on('zoom', (event) => { group.attr('transform', event.transform) }) .scaleExtent([0.2, 100]) const svg = d3 .select(container) .append('svg') .attr('viewBox', [-width / 2, -height / 2, width, height]) const group = svg .append('g') .attr('width', '100%') .attr('height', '100%') .call( d3 .drag() .on('start', dragstarted) .on('drag', dragged as any) .on('end', dragended as any) as any ) // .call(zoom as any) const link = group .append('g') .attr('stroke', '#1e1e1e') .attr('stroke-opacity', 0.2) .selectAll('line') .data(links) .join('line') const node = group .append('g') .selectAll<SVGCircleElement, { x: number; y: number }>('g') .data(nodes) .join('g') .classed('node', true) .classed('fixed', (d: any) => d.fx !== undefined) .attr('class', (d: any) => d.class as string) .call( d3 .drag() .on('start', dragstarted) .on('drag', dragged as any) .on('end', dragended as any) as any ) .on('click', click as any) d3.selectAll('.category-node') .append('circle') .attr('fill', '#0083C5') .attr('r', isMobile ? 4 : 7) d3.selectAll('.tag-node') .append('circle') .attr('fill', '#FFC434') .attr('r', isMobile ? 4 : 7) d3.selectAll('.story-node') .append('foreignObject') .attr('height', isMobile ? 18 : 35) .attr('width', isMobile ? 18 : 35) .attr('x', isMobile ? -9 : -17) .attr('y', isMobile ? -18 : -30) .attr('r', isMobile ? 16 : 30) .append('xhtml:div') .attr('class', 'node-image') .append('xhtml:img') .attr('src', (d: any) => d.image) .attr('transform-origin', 'center') .attr('height', isMobile ? 18 : 35) .attr('width', isMobile ? 18 : 35) d3.selectAll('.main-story-node') .append('foreignObject') .attr('height', isMobile ? 50 : 100) .attr('width', isMobile ? 50 : 100) .attr('x', isMobile ? -25 : -50) .attr('y', isMobile ? -25 : -50) .attr('r', isMobile ? 50 : 100) .append('xhtml:div') .attr('class', 'node-image') .append('xhtml:img') .attr('src', (d: any) => d.image) .attr('transform-origin', 'center') .attr('height', isMobile ? 50 : 100) .attr('width', isMobile ? 50 : 100) node .append('foreignObject') .attr('height', (d: any) => (d.class === 'main-story-node' ? 65 : 55)) .attr('width', (d: any) => isMobile ? d.class === 'main-story-node' ? 80 : 50 : d.class === 'main-story-node' ? 120 : 70 ) .attr('x', (d: any) => isMobile ? d.class === 'main-story-node' ? -40 : -25 : d.class === 'main-story-node' ? -60 : -35 ) .attr('y', (d: any) => isMobile ? d.class === 'main-story-node' ? 32 : 7 : d.class === 'main-story-node' ? 60 : 12 ) .append('xhtml:p') .attr('class', (d: any) => d.class) .text((d: any) => d.name) simulation.on('tick', () => { link .attr('x1', (d: any) => d.source.x) .attr('y1', (d: any) => d.source.y) .attr('x2', (d: any) => d.target.x) .attr('y2', (d: any) => d.target.y) node .attr('cx', (d: any) => d.x as number) .attr('cy', (d: any) => d.y as number) .attr('transform', (d: any) => { return `translate(${d.x},${d.y})` }) }) function transition(zoomLevel: number) { group .transition() .delay(100) .duration(500) .call(zoom.scaleBy as any, zoomLevel) } transition(0.7) d3.selectAll('.zoom-button').on('click', function () { // @ts-ignore if (this && this.id === 'zoom-in') { transition(1.2) // increase on 0.2 each time } // @ts-ignore if (this.id === 'zoom-out') { transition(0.8) // deacrease on 0.2 each time } // @ts-ignore if (this.id === 'zoom-init') { group .transition() .delay(100) .duration(500) .call(zoom.scaleTo as any, 0.7) // return to initial state } })
解决方案
问题核心是<g>无事件捕获区域、拖拽逻辑与节点混淆,以下是修复步骤:
1. 给<g>添加可捕获事件的背景
<g>本身是容器元素,没有可视区域无法触发鼠标事件,需添加透明全屏矩形确保能捕获拖拽:
const group = svg .append('g') .attr('width', '100%') .attr('height', '100%') // 添加透明背景矩形用于捕获拖拽事件 group.append('rect') .attr('width', width) .attr('height', height) .attr('x', -width/2) .attr('y', -height/2) .attr('fill', 'transparent') .attr('pointer-events', 'all')
2. 为<g>编写独立的拖拽逻辑
节点拖拽是修改单个节点的fx/fy,而<g>拖拽是平移整个图表,需用独立逻辑并与zoom状态同步:
// 记录拖拽起始时的transform状态 let initialTransform function groupDragStarted(event) { initialTransform = d3.zoomTransform(svg.node()) // 阻止事件冒泡,避免触发节点拖拽逻辑 event.sourceEvent.stopPropagation() } function groupDragged(event) { // 计算拖拽偏移量 const dx = event.x - event.sourceEvent.offsetX const dy = event.y - event.sourceEvent.offsetY // 生成新的变换矩阵 const newTransform = initialTransform.translate(dx, dy) // 更新group的transform group.attr('transform', newTransform) // 同步zoom状态,保证缩放平移一致 svg.call(zoom.transform, newTransform) } function groupDragEnded() { initialTransform = null }
3. 绑定拖拽到<g>并启用zoom
将新拖拽函数绑定到<g>,同时启用svg的zoom功能:
// 绑定group的拖拽 group.call( d3.drag() .on('start', groupDragStarted) .on('drag', groupDragged) .on('end', groupDragEnded) ) // 启用svg的zoom功能 svg.call(zoom as any)
4. 保留节点原有拖拽逻辑
节点拖拽逻辑无需修改,groupDragStarted中的stopPropagation()已确保拖拽节点时不会触发group的拖拽事件。
内容的提问来源于stack exchange,提问作者nopaleale
相关产品推荐
相关产品推荐

