D3 SVG在Next.js中mouseenter仅触发一次的问题修复求助
问题描述
开发集成D3的React组件时,发现SVG圆圈的鼠标事件仅能触发一次,对应的Tooltip仅渲染一次(已确认Tooltip组件功能正常)。相关代码如下:
import React, { useEffect, useRef, useState } from 'react'; import * as d3 from 'd3'; import D3ColorLegend from './D3ColorLegend'; import {Tooltip} from './ToolTip' const Scatterplot = ({ data, colorProperty, hoverProp }) => { const svgRef = useRef(); const [details, setDetails] = useState(null); useEffect(() => { const handleMouseOver = (event, d) => { const [x, y] = d3.pointer(event); setDetails({ xPos : x, yPos : y, name: hoverProp[data.indexOf(d)], }); }; // set the dimensions and margins of the graph const margin = { top: 10, right: 30, bottom: 30, left: 60 }; const width = 800 - margin.left - margin.right; const height = 600 - margin.top - margin.bottom; // Find the minimum and maximum values in your data for both x and y const xExtent = d3.extent(data, d => d.x); const yExtent = d3.extent(data, d => d.y); // Append the SVG object to the body of the page const svg = d3.select(svgRef.current) .append('svg') .attr('width', width + margin.left + margin.right) .attr('height', height + margin.top + margin.bottom) .append('g') .attr('transform', `translate(${margin.left},${margin.top})`); // Add X axis const x = d3.scaleLinear() .domain([Math.min(0, xExtent[0]), xExtent[1]]) .range([0, width]); const xAxis = svg.append('g') .attr('transform', `translate(0,${height})`) .call(d3.axisBottom(x)); // Add Y axis const y = d3.scaleLinear() .domain([Math.min(0, yExtent[0]), yExtent[1]]) .range([height, 0]); const yAxis = svg.append('g') .call(d3.axisLeft(y)); // Add a clipPath: everything out of this area won't be drawn. const clip = svg.append('defs').append('clipPath') .attr('id', 'clip') .append('rect') .attr('width', width) .attr('height', height) .attr('x', 0) .attr('y', 0); // Create the scatter variable: where both the circles and the brush take place const scatter = svg.append('g') .attr('clip-path', 'url(#clip)'); const colorScale = d3.scaleSequential().domain([1, 10]).interpolator(d3.interpolateSinebow); // Add circles scatter .selectAll('circle') .data(data) .enter() .append('circle') .attr('cx', (d) => x(d.x)) .attr('cy', (d) => y(d.y)) .attr('r', 8) .style('fill', (_, i) => colorScale(colorProperty[i])) .style('opacity', 0.5) .on("mouseover", () => {} ) .on('mousemove', (event, d) => handleMouseOver(event, d)) .on('mouseleave', () => setDetails(null)); // Set the zoom and Pan features: how much you can zoom, on which part, and what to do when there is a zoom const zoom = d3.zoom() .scaleExtent([0.5, 20]) // This controls how much you can unzoom (x0.5) and zoom (x20) .extent([[0, 0], [width, height]]) .on('zoom', updateChart); // This adds an invisible rect on top of the chart area. This rect can recover pointer events: necessary to understand when the user zoom svg.append('rect') .attr('width', width) .attr('height', height) .style('fill', 'none') .style('pointer-events', 'all') .attr('transform', `translate(${margin.left},${margin.top})`) .call(zoom); // now the user can zoom, and it will trigger the function called updateChart // A function that updates the chart when the user zooms and thus new boundaries are available function updateChart(event) { // recover the new scale const newX = event.transform.rescaleX(x); const newY = event.transform.rescaleY(y); // update axes with these new boundaries xAxis.call(d3.axisBottom(newX)); yAxis.call(d3.axisLeft(newY)); // update circle position scatter .selectAll('circle') .attr('cx', (d) => newX(d.x)) .attr('cy', (d) => newY(d.y)); } }, [data]); return ( <div className='container'> <D3ColorLegend /> <div id="dataviz_axisZoom" ref={svgRef}></div> {details && <Tooltip interactionData={details} />} </div> ); }; export default Scatterplot;
修复方案
1. 清空重复SVG元素,避免事件拦截
每次useEffect执行时,会向容器追加新的SVG元素,新SVG中的zoom矩形(设置了pointer-events: all)会覆盖在旧SVG的圆圈之上,导致后续鼠标事件被矩形拦截。需在创建新SVG前清空容器:
// 在append('svg')前添加清空逻辑 const svgContainer = d3.select(svgRef.current); svgContainer.selectAll("*").remove(); const svg = svgContainer .append('svg') .attr('width', width + margin.left + margin.right) .attr('height', height + margin.top + margin.bottom) .append('g') .attr('transform', `translate(${margin.left},${margin.top})`);
2. 修正事件绑定与状态更新逻辑
- 移除空的
mouseover事件绑定,避免干扰其他鼠标事件的触发 - 优化状态更新逻辑:添加唯一标识(如时间戳)确保每次状态对象引用不同,绕过React浅比较的限制,强制状态更新
- 替换
data.indexOf(d)为findIndex,避免数据有重复项时取值错误
修改后的事件绑定代码:
scatter .selectAll('circle') .data(data) .enter() .append('circle') .attr('cx', (d) => x(d.x)) .attr('cy', (d) => y(d.y)) .attr('r', 8) .style('fill', (_, i) => colorScale(colorProperty[i])) .style('opacity', 0.5) .on('mousemove', (event, d) => { const [xPos, yPos] = d3.pointer(event); // 添加timestamp确保每次状态对象引用不同 setDetails({ xPos, yPos, name: hoverProp[data.findIndex(item => item === d)], timestamp: Date.now() }); }) .on('mouseleave', () => setDetails(null));
3. 修正Zoom矩形的位置偏移
原代码中给Zoom矩形添加了重复的transform,导致矩形位置偏移,无法正确覆盖图表区域,修正后:
svg.append('rect') .attr('width', width) .attr('height', height) .style('fill', 'none') .style('pointer-events', 'all') // 移除多余的transform,父g已做偏移处理 .call(zoom);
4. 完善useEffect依赖数组
当前useEffect仅依赖data,但colorProperty、hoverProp变化时也需要重新渲染图表,否则会出现数据与视图不一致的问题:
useEffect(() => { // ... 原代码逻辑 }, [data, colorProperty, hoverProp]);
内容的提问来源于stack exchange,提问作者Zayyan Masud
相关产品推荐
相关产品推荐

