I've been struggling with this issue for days now and cannot find a solution to update the nodes properly.
Since I'm using threejs to render the graph I couldn't find enough info on the web on how to properly achieve this.
On initial render everything works as expected. The problem is when I add new nodes with the update
function.
Newly added nodes are disconnected from their parents and the created links seem to lack coordinates and forces.
In the example below I am adding a new node with id: test
.
This is what I've tried so far:
let root = d3
.stratify()
.id((d) => d.id)
.parentId((d) => d.linkedTo)(data)
let nodes = root.descendants()
let links = root.links()
simulation = d3
.forceSimulation(nodes)
.force('charge', d3.forceManyBody().strength(-1000))
.force('center', d3.forceCenter(0, 0))
.force('collide', d3.forceCollide().radius(50).strength(0.9))
.on('tick', ticked)
simulation.force(
'link',
d3
.forceLink(links)
.id((d) => {
return d.data._id
})
.distance(10)
.strength(0.9)
)
// Render nodes and links three scene
links.forEach(renderLink)
nodes.forEach(renderNode)
function update(newData, oldData) {
simulation.stop()
const newRoot = d3
.stratify()
.id((d) => d.id)
.parentId((d) => d.linkedTo)(newData)
const newNodes = newRoot.descendants()
const newLinks = newRoot.links()
// Find nodes to remove and remove them
const nodesToRemove = nodes.filter((node) => !newNodes.some((newNode) => newNode.id === node.id))
// remove nodes from the three scene
removeNodes(nodesToRemove)
// Find links to remove and remove them
const linksToRemove = links.filter(
(link) =>
!newLinks.some(
(newLink) => newLink.source.id === link.source.id && newLink.target.id === link.target.id
)
)
// remove links from the three scene
removeLinks(linksToRemove)
// Find new nodes
const nodesToAdd = newNodes.filter((newNode) => !nodes.some((node) => node.id === newNode.id))
nodes = [...nodes, ...nodesToAdd]
// Find links to add and add them
const linksToAdd = newLinks.filter(
(newLink) =>
!links.some((link) => {
const sourceMathces = link.source.id === newLink.source.id
const targetMathces = link.target.id === newLink.target.id
return sourceMathces && targetMathces
})
)
links = [...links, ...linksToAdd]
simulation.nodes(nodes).force('link').links(links)
simulation.alpha(0.5).restart()
}
Thank you!
The fix is mainly around how the new links were added and the fact that each new link was not instantiated correctly with their new sources and targets.
async function update(newData, oldData) => {
// Create a map of existing nodes by their id
const existingNodesMap = new Map(nodes.map((node) => [node.id, node]))
// Create new root, nodes, and links
const newRoot = d3
.stratify()
.id((d) => d.id)
.parentId((d) => d.linkedTo)(newData)
const newNodes = newRoot.descendants()
const newLinks = newRoot.links()
await Promise.all(
newNodes.slice().map((newNode) => {
if (!existingNodesMap.has(newNode.id)) {
nodes.push(newNode)
existingNodesMap.set(newNode.id, newNode)
return renderNode(newNode) // Render new node
}
})
)
// Create a map of existing links by their source and target ids
const existingLinksSet = new Set(links.map((link) => `${link.source.id}-${link.target.id}`))
// Update the links array, avoiding duplicates and adding valid links
await Promise.all(
newLinks.slice().map((newLink) => {
const source = existingNodesMap.get(newLink.source.id)
const target = existingNodesMap.get(newLink.target.id)
const linkId = `${newLink.source.id}-${newLink.target.id}`
if (source && target && !existingLinksSet.has(linkId)) {
newLink.source = source
newLink.target = target
links.push(newLink)
existingLinksSet.add(linkId)
return renderLink(newLink) // Render new link
}
})
)
// Restart the simulation with the updated nodes and links
simulation.nodes(nodes)
simulation.force('link').links(links)
simulation.alpha(0.2).restart()
}