Search code examples
javascriptd3.jsthree.js

Directed graph update nodes with new data


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.

enter image description here

enter image description here

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!


Solution

  • 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()
      }