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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.23 19:24:03