D3 图表在 React.js 组件上重复

问题描述 投票:0回答:2

这是一个 React 应用程序,可能出了什么问题?每次都会附加一个新的 svg,而不是现有的正在更新的 svg... 我不确定我应该添加哪些其他详细信息,我已经包含了您可能需要帮助我的所有信息,如果还有什么我应该添加的,请告诉我。谢谢。

这是我的组件,我通过 props 传递数据。

export const FourDirectionsTimeChart = ({data}) => {
    useEffect(() => {
        const width = document.getElementById("container").clientWidth;
        const height = document.getElementById("container").clientHeight;
        const R = (width + height) / 8;
        const CX = width / 2;
        const CY = height / 2;
        const smallR = R * 0.1;
        const circleColor = "bisque";
        const itemColor = "#3F4200";

        let svg = d3.select("#container")
            .append("svg")
            .attr("preserveAspectRatio", "xMinYMin meet")
            .attr("viewBox", `0 0 ${width} ${height}`)

        let mainCircle = svg.append("circle")
            .attr("fill", circleColor)
            .attr("r", R)
            .attr("cx", CX)
            .attr("cy", CY);

        let centerCircle = svg.append("circle")
            .attr("fill", "white")
            .attr("r", smallR)
            .attr("cx", CX)
            .attr("cy", CY);

        const timePercentage = (time) => {
            const percentage = (time * 100 / 23) / 100;
            return percentage;
        };
        function timeToRadius(time) {
            return (smallR + timePercentage(time) * (R - smallR))
        }

        // Times concentric circles ---
        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))
        }

        // Cardinal points ---
        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")

        // Ships positions --- 
        function getPosition(degrees, time) {
            const getRadians = (degrees) => degrees * Math.PI / 180 + Math.PI / 2;
            let x = (smallR + timePercentage(time) * (R - smallR)) * Math.cos(getRadians(degrees)) + CX;
            let y = (smallR + timePercentage(time) * (R - smallR)) * Math.sin(getRadians(degrees)) * -1 + CY;
            return { x, y };
        }

        // Data mapping ---
        let parsedData = [];
        (() => {
            data?.forEach((x) => {
                parsedData.push({
                    id: x.Patente,
                    course: x.Rumbo,
                    speed: x.Velocidad,
                    time: new Date(x.Fecha_gps).getHours()
                })
            });

            let position;
            parsedData.map(d => {
                position = getPosition(d.course, d.time);
                svg.append("circle")
                    .attr("fill", () => {
                        if (d.speed == 0) return "brown";
                        else return "brown";
                    })
                    .attr("r", R * 0.015)
                    .attr("cx", position.x)
                    .attr("cy", position.y)
                    .attr("opacity", 0.4);
            });
        })();

    }, [data]);

    return (
        <div id="container" style={{ width: '100%', height: '100%' }}></div>
    )
}
reactjs d3.js
2个回答
4
投票

你认为

let svg = d3.select("#container").append("svg")
这条线有什么作用?它选择 ID 为
"container"
的元素并向其附加一个新的 SVG 元素。

您基本上可以做以下两件事之一:

  1. 选择并删除容器中所有现有的SVG元素,然后在更新时从头开始绘制图表:
    d3.select("#container svg").remove()
    ,然后
    d3.select("#container").append("svg")
    ;
  2. 将初始化(附加 svg、设置大小、绘制轴)逻辑与可能需要多次运行(更新值)的逻辑分开,并确保在更新时仅调用第二部分。这更复杂,但也更高效,尤其是在像 React 这样已经很重的 DOM 生态系统中。

我已经获取了您的代码并添加了一个示例来说明如何实现下面的第二种方法:

请注意,要访问

R
smallR
等,我必须将初始化逻辑划分为两个
useEffect
钩子。

const {
  useEffect,
  useState
} = React;

const FourDirectionsTimeChart = () => {
  const [data, setData] = useState([]);
  const [constants, setConstants] = useState({});

  // Initialization logic, run only once
  useEffect(() => {
    const width = document.getElementById("container").clientWidth;
    const height = document.getElementById("container").clientHeight;
    const R = (width + height) / 8;
    const CX = width / 2;
    const CY = height / 2;
    const smallR = R * 0.1;

    setConstants({
      R,
      CX,
      CY,
      smallR
    });

    const circleColor = "bisque";
    const itemColor = "#3F4200";

    const svg = d3.select("#container")
      .append("svg")
      .attr("preserveAspectRatio", "xMinYMin meet")
      .attr("viewBox", `0 0 ${width} ${height}`);

    const mainCircle = svg.append("circle")
      .attr('id', 'mainCircle')
      .attr("fill", circleColor);

    const centerCircle = svg.append("circle")
      .attr('id', 'innerCircle')
      .attr("fill", "white");

    // Times concentric circles ---
    for (let i = 0; i <= 23; i += 4) {
      svg.append("circle")
        .attr("class", "concentric")
        .datum(i)
        .attr("fill", "none")
        .attr("stroke-dasharray", "4 20")
        .attr("stroke", "gray")
        .attr("stroke-width", 1);
    }

    // Cardinal points ---
    svg.append("text")
      .attr("id", "N")
      .text("N")
      .attr("fill", "black")
      .attr("text-anchor", "middle")
      .style("font-family", "serif")
    svg.append("text")
      .attr("id", "S")
      .text("S")
      .attr("fill", "black")
      .attr("text-anchor", "middle")
      .style("font-family", "serif")
      .attr("alignment-baseline", "hanging")
    svg.append("text")
      .attr("id", "E")
      .text("E")
      .attr("fill", "black")
      .attr("text-anchor", "start")
      .attr("alignment-baseline", "middle")
      .style("font-family", "serif");
    svg.append("text")
      .attr("id", "O")
      .text("O")
      .attr("fill", "black")
      .attr("text-anchor", "end")
      .attr("alignment-baseline", "middle")
      .style("font-family", "serif")
  }, []);

  // Drawing logic, run whenever `constants` changes (which is only once)
  useEffect(() => {
    const { R, smallR, CX, CY } = constants;
    const textTime = 25;
    const fontSize = R * 0.25;

    const svg = d3.select("#container svg");

    const mainCircle = svg.select("#mainCircle")
      .attr("r", R)
      .attr("cx", CX)
      .attr("cy", CY);

    const centerCircle = svg.select("#innerCircle")
      .attr("r", smallR)
      .attr("cx", CX)
      .attr("cy", CY);

    // Times concentric circles ---
    function timeToRadius(time) {
      return (smallR + timePercentage(time) * (R - smallR));
    }

    svg.selectAll(".concentric")
      .attr("cx", CX)
      .attr("cy", CY)
      .attr("r", d => timeToRadius(d))

    // Cardinal points ---
    svg.select("text#N")
      .attr("font-size", fontSize)
      .attr("dx", getPosition(0, textTime).x)
      .attr("dy", getPosition(0, textTime).y);
    svg.select("text#S")
      .attr("font-size", fontSize)
      .attr("dx", getPosition(180, textTime).x)
      .attr("dy", getPosition(180, textTime).y);
    svg.select("text#E")
      .attr("font-size", fontSize)
      .attr("dx", getPosition(-90, textTime).x)
      .attr("dy", getPosition(-90, textTime).y);
    svg.select("text#O")
      .attr("font-size", fontSize)
      .attr("dx", getPosition(90, textTime).x)
      .attr("dy", getPosition(90, textTime).y);
  }, [constants]);

  const timePercentage = (time) => {
    const percentage = (time * 100 / 23) / 100;
    return percentage;
  };

  function timeToRadius(time) {
    const {
      smallR,
      R
    } = constants;
    return (smallR + timePercentage(time) * (R - smallR));
  }

  const getRadians = (degrees) => degrees * Math.PI / 180 + Math.PI / 2;

  function getPosition(degrees, time) {
    const {
      smallR,
      R,
      CX,
      CY
    } = constants;
    const x = (smallR + timePercentage(time) * (R - smallR)) * Math.cos(getRadians(degrees)) + CX;
    const y = (smallR + timePercentage(time) * (R - smallR)) * Math.sin(getRadians(degrees)) * -1 + CY;
    return {
      x,
      y
    };
  }

  useEffect(() => {
    const svg = d3.select('#container svg');

    // Data mapping ---
    let position;
    
    const points = svg.selectAll('.point').data(data);
    
    // Remove old points
    points.exit().remove();
    
    // Append and set constants only for new points
    points
      .enter()
      .append('circle')
      .attr('class', 'point')
      .attr("r", 15)
      .attr("opacity", 0.4)
    
      // Then, take both the new and the updated points
      .merge(points)
      .attr("cx", d => getPosition(d.course, d.time).x)
      .attr("cy", d => getPosition(d.course, d.time).y)
      .attr("fill", d => d.speed === 0 ? "brown" : "red");
  }, [data]);

  useEffect(() => {
    let i = 0;
    setInterval(() => {
      setData(i % 2 === 0 ? data1 : data2);
      i++;
    }, 2000);
  }, []);

  return ( <
    div id = "container"
    style = {
      {
        width: '100%',
        height: '100%'
      }
    } > < /div>
  )
}

const data1 = [{
  id: 100,
  course: 35,
  speed: 0,
  time: 5
}];

const data2 = [{
  id: 101,
  course: 80,
  speed: 25,
  time: 12
}, {
  id: 102,
  course: 256,
  speed: 0,
  time: 15
}];

ReactDOM.render( <
  FourDirectionsTimeChart / > ,
  document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root" style="width: 500px; height: 500px"></div>


0
投票

有点晚了,他

另外,检查是否启用了 StrictMode,因为它会渲染包装树两次,

© www.soinside.com 2019 - 2024. All rights reserved.