React.js组件中D3图表重复渲染:每次更新新增SVG而非修改原有SVG
问题原因
核心问题是useEffect依赖data属性,每次data更新触发副作用执行时,都会直接通过append("svg")向容器追加新的SVG元素,没有清理上一次渲染的内容,导致多个SVG堆叠显示。此外原代码使用固定ID选择容器,同一页面渲染多个组件实例时会出现选择冲突,且getPosition函数声明在调用之后,会触发未定义错误。
修复方案
import { useRef, useEffect } from 'react'; import * as d3 from 'd3'; export const FourDirectionsTimeChart = ({data}) => { // 用ref绑定容器,避免多实例ID冲突问题 const containerRef = useRef(null); useEffect(() => { if(!containerRef.current) return; const container = d3.select(containerRef.current); // 清空容器已有内容,解决SVG重复追加的核心问题 container.html(''); const width = containerRef.current.clientWidth; const height = containerRef.current.clientHeight; const R = (width + height) / 8; const CX = width / 2; const CY = height / 2; const smallR = R * 0.1; const circleColor = "bisque"; const svg = container .append("svg") .attr("preserveAspectRatio", "xMinYMin meet") .attr("viewBox", `0 0 ${width} ${height}`) // 绘制基础底图 svg.append("circle") .attr("fill", circleColor) .attr("r", R) .attr("cx", CX) .attr("cy", CY); svg.append("circle") .attr("fill", "white") .attr("r", smallR) .attr("cx", CX) .attr("cy", CY); const timePercentage = (time) => (time * 100 / 23) / 100; const timeToRadius = (time) => smallR + timePercentage(time) * (R - smallR); // 绘制时间同心圆刻度 for (let i = 0; i <= 23; i += 4) { svg.append("circle") .attr("fill", "none") .attr("cx", CX) .attr("cy", CY) .attr("stroke-dasharray", "4 20") .attr("stroke", "gray") .attr("stroke-width", 1) .attr("r", timeToRadius(i)) } // 提前声明位置计算函数,避免调用时未定义错误 function getPosition(degrees, time) { const getRadians = (degrees) => degrees * Math.PI / 180 + Math.PI / 2; const x = timeToRadius(time) * Math.cos(getRadians(degrees)) + CX; const y = timeToRadius(time) * Math.sin(getRadians(degrees)) * -1 + CY; return { x, y }; } // 绘制方位文字 const textTime = 25; const fontSize = R * 0.25; svg.append("text") .attr("dx", getPosition(0, textTime).x) .attr("dy", getPosition(0, textTime).y) .text("N") .attr("fill", "black") .attr("font-size", fontSize) .attr("text-anchor", "middle") .style("font-family", "serif") svg.append("text") .attr("dx", getPosition(180, textTime).x) .attr("dy", getPosition(180, textTime).y) .text("S") .attr("fill", "black") .attr("font-size", fontSize) .attr("text-anchor", "middle") .style("font-family", "serif") .attr("alignment-baseline", "hanging") svg.append("text") .attr("dx", getPosition(-90, textTime).x) .attr("dy", getPosition(-90, textTime).y) .text("E") .attr("fill", "black") .attr("font-size", fontSize) .attr("text-anchor", "start") .attr("alignment-baseline", "middle") .style("font-family", "serif"); svg.append("text") .attr("dx", getPosition(90, textTime).x) .attr("dy", getPosition(90, textTime).y) .text("O") .attr("fill", "black") .attr("font-size", fontSize) .attr("text-anchor", "end") .attr("alignment-baseline", "middle") .style("font-family", "serif") // 处理并渲染数据点位 const parsedData = data?.map(x => ({ id: x.Patente, course: x.Rumbo, speed: x.Velocidad, time: new Date(x.Fecha_gps).getHours() })) || []; parsedData.forEach(d => { const position = getPosition(d.course, d.time); svg.append("circle") .attr("fill", "brown") .attr("r", R * 0.015) .attr("cx", position.x) .attr("cy", position.y) .attr("opacity", 0.4); }); }, [data]); return ( <div ref={containerRef} style={{ width: '100%', height: '100%' }}></div> ) }
可选性能优化
如果数据量较大,全量清空重绘存在性能损耗,可以改用D3的enter/update/exit增量更新模式:首次渲染时生成不变的底图、刻度、方位文字,后续更新仅对船舶点位做增量修改,不需要重绘整个SVG。
内容的提问来源于stack exchange,提问作者Daniel Tkach
相关产品推荐
相关产品推荐

