Как добавить только новые узлы в ориентированный граф d3?

Я создал силовой направленный граф, используя d3, который рендерится в компоненте react с помощью хука useEffect. Первоначально граф отображается правильно, но если он повторно отображается/обновляется новыми узлами, отправленными через форму ввода, существующие узлы дублируются.

Я думал, что существующие узлы останутся в покое и будут создаваться только новые узлы после .enter(), чего явно не происходит. Любая помощь с тем, где я ошибаюсь?

Редактировать 1: это образец входящих данных.

var nodesData = [
{"id": "Do Something", "type": "activity"},
{"id": "My Document", "type": "object"}
]

Это код для графика:

import React, { useRef, useEffect } from 'react';
import * as d3 from 'd3';
import '../../custom_styles/bpForceDirected.css';

interface IProps {
    data?: string;
    linkData?: string;
}

/* Component */
export const BpForceDirectedGraph = (props: IProps) => {

    const d3Container = useRef(null);

    /* The useEffect Hook is for running side effects outside of React,
       for instance inserting elements into the DOM using D3 */
    useEffect(
        () => {
            if (props.data && d3Container.current) {
                var w=500;
                var h=500;
                const svg = d3.select(d3Container.current)
                            .attr("viewBox", "0 0 " + w + " " + h )
                            .attr("preserveAspectRatio", "xMidYMid meet");

                var simulation = d3.forceSimulation()
                    .nodes(props.data);
                simulation
                    .force("charge_force", d3.forceManyBody())
                    .force("center_force", d3.forceCenter(w / 2, h / 2));

                function circleColor(d){
                    if(d.type ==="activity"){
                        return "blue";
                    } else {
                        return "pink";
                    }
                }

                function linkColor(d){
                    console.log(d); 
                    if(d.type === "Activity Output"){
                        return "green";
                    } else {
                        return "red";
                    }
                }

                //Create a node that will contain an object and text label      
                var node = svg.append("g")
                  .attr("class", "nodes")
                  .selectAll("g")
                  .data(props.data)
                  .enter()
                  .append("g");

                node.append("circle")
                        .attr("r", 10)
                        .attr("fill", circleColor);

                node.append("text")
                    .attr("class", "nodelabel")
                    .attr("dx", 12)
                    .attr("dy", ".35em")
                    .text(function(d) { return d.activityname });

                // The complete tickActions() function    
                function tickActions() {
                //update circle positions each tick of the simulation 
                    node.attr('transform', d => `translate(${d.x},${d.y})`);

                //update link positions 
                //simply tells one end of the line to follow one node around
                //and the other end of the line to follow the other node around
                    link
                        .attr("x1", function(d) { return d.source.x; })
                        .attr("y1", function(d) { return d.source.y; })
                        .attr("x2", function(d) { return d.target.x; })
                        .attr("y2", function(d) { return d.target.y; });
                }  

                  simulation.on("tick", tickActions );

                //Create the link force 
                //We need the id accessor to use named sources and targets 
                var link_force =  d3.forceLink(props.linkData)
                    .id(function(d) { return d.id; })

                simulation.force("links",link_force)

                //draw lines for the links 
                var link = svg.append("g")
                    .attr("class", "links")
                    .selectAll("line")
                    .data(props.linkData)
                    .enter().append("line")
                        .attr("stroke-width", 2)
                        .style("stroke", linkColor);

                // Remove old D3 elements
                node.exit()
                    .remove();
            }
        },

        [props.data, props.linkData, /*d3Container.current*/])

    return (
        <svg
            className="d3-component"
            ref={d3Container}
        />
    );
}

export default BpForceDirectedGraph;

Изменить 2: Пример воспроизводимого кода


person userNick    schedule 28.03.2020    source источник
comment
Не могли бы вы отредактировать вопрос, чтобы добавить образец структуры данных?   -  person Mehdi    schedule 29.03.2020
comment
@Mehdi Я добавил образец данных, поступающих на график в посте.   -  person userNick    schedule 29.03.2020
comment
Хорошо, я указал на возможное решение. Было бы намного проще ответить на этот вопрос, если бы был предоставлен минимальный воспроизводимый пример (например, во фрагменте стека ).   -  person Mehdi    schedule 29.03.2020
comment
@Mehdi, чтобы уточнить дублирование - по сути, это воссоздание графика после каждого повторного рендеринга. Поэтому каждый раз я получаю новый родительский элемент nodes.   -  person userNick    schedule 29.03.2020


Ответы (1)


Проблема возникает из-за того, что функция, создающая диаграмму, вызывается при каждом обновлении и не учитывает существующий контент.

Вот один из способов решения проблемы:

Очищайте SVG в начале каждого выполнения useEffect при (пере)определении переменной svg. Как показано ниже, .html('') очищает существующие узлы SVG.

  const svg = d3
    .select(d3Container.current)
    .html("")
    .attr("viewBox", "0 0 " + w + " " + h)
    .attr("preserveAspectRatio", "xMidYMid meet");

Более элегантным подходом было бы обновить код так, чтобы функция инициализировала диаграмму, а вторая (повторно) генерировала график. Насколько я понимаю, реакция заключается в том, что это делается с использованием componentDidMount и componentDidUpdate.

person Mehdi    schedule 29.03.2020
comment
Спасибо! Ваше решение устранило проблему. Я все еще новичок в реагировании, но у меня сложилось впечатление, что useEffect заменяет некоторые функции componentDidMout/componentDidUpdate, поэтому я использовал его здесь. Хотя, возможно, это не лучшее решение. - person userNick; 29.03.2020
comment
Пожалуйста. Я не реагирую, поэтому не могу посоветовать подход... - person Mehdi; 29.03.2020