I have created a force directed graph using d3, which is rendering in a react component using the useEffect hook. The graph is initially rendered correctly, but if it is re-rendered/updated by new nodes being submitted through an input form, the existing nodes are duplicated.
I thought that the existing nodes would be left alone and only new nodes would be created after .enter(), which is clearly not happening. Any help with where I'm going wrong?
Edit 1: this is a sample of the data coming in
var nodesData = [
{"id": "Do Something", "type": "activity"},
{"id": "My Document", "type": "object"}
]
This is the code for the graph:
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;
Edit 2: Reproducible code example