I am trying to add nodes to a D3 force directed graph dynamically. I believe I am close, but am not understanding some critical concept related to how selections in D3 work.
If I call my addData
function directly and add all of the nodes at once, everything works as expected.
However, if I use the addDataInChunks
, the first two nodes and first link are added, but after that no further nodes appear in the graph. Additionally, the ability to drag nodes around is lost as soon as the second chunk is added to the graph.
What have I done wrong? How does my code need to change so this will work?
complete, sample .html file
<html>
<head>
<script src="https://d3js.org/d3.v7.min.js" charset="utf-8"></script>
</head>
<style>
.graph {
width: 750px;
height: 400px;
}
</style>
<script type="module">
const nodeData = [
{
id: 'node 1',
added: false,
},
{
id: 'node 2',
added: false,
},
{
id: 'node 3',
added: false,
},
{
id: 'node 4',
added: false,
},
]
const linkData = [
{
linkID: 'link 1',
added: false,
source: 'node 1',
target: 'node 2',
},
{
linkID: 'link 2',
added: false,
source: 'node 1',
target: 'node 3',
},
{
linkID: 'link 3',
added: false,
source: 'node 3',
target: 'node 4',
},
]
//
//
//
let svg = null
let nodeContainer = null
let linkContainer = null
let simulation = null
const width = 750
const height = 400
const nodeCount = 10
const svgNS = d3.namespace('svg:text').space
function setupGraph() {
svg = d3.select('#chart').call(d3.zoom().on('zoom', zoomed)).append('g').attr('id', 'container')
nodeContainer = svg.append('g').attr('stroke', '#fff').attr('stroke-width', 1.5).attr('id', 'nodes')
linkContainer = svg.append('g').attr('stroke', '#999').attr('stroke-opacity', 0.6).attr('id', 'links')
}
const simulationNodes = []
const simulationLinks = []
function addData(addNodes, addLinks) {
const links = addLinks.map((d) => ({ ...d }))
const nodes = addNodes.map((d, index) => ({ ...d }))
console.log(`๐ ~ nodes:`, nodes)
console.log(`๐ ~ links:`, links)
simulationNodes.push(...nodes)
simulationLinks.push(...links)
simulation = d3
.forceSimulation(simulationNodes)
.force(
'link',
d3
.forceLink(simulationLinks)
.id((d) => d.id)
.distance((d) => 50)
)
.force('charge', d3.forceManyBody().strength(-400))
.force('x', d3.forceX())
.force('y', d3.forceY())
.on('tick', ticked)
nodeContainer = nodeContainer
.selectAll('circle')
.data(nodes)
.join((enter) => {
return enter.append((d) => {
const circleElement = document.createElementNS(svgNS, 'circle')
circleElement.setAttribute('r', 16)
circleElement.setAttribute('fill', '#00ff00')
circleElement.setAttribute('stroke', '#ffffff')
circleElement.setAttribute('stroke-width', '3')
return circleElement
})
})
nodeContainer.append('title').text((d) => `hello ${d.id}`)
nodeContainer.call(d3.drag().on('start', dragstarted).on('drag', dragged).on('end', dragended))
linkContainer = linkContainer.selectAll('line').data(links).join('line').attr('stroke-width', 1)
}
setupGraph()
// addData(nodeData, linkData)
async function addDataInChunks(allNodes, allLinks) {
const timeout = 7000
let nodeChunk = allNodes.slice(0, 2)
let linkChunk = allLinks.slice(0, 1)
addData(nodeChunk, linkChunk)
await new Promise((r) => setTimeout(r, timeout))
//
nodeChunk = allNodes.slice(2, 3)
linkChunk = allLinks.slice(1, 2)
addData(nodeChunk, linkChunk)
await new Promise((r) => setTimeout(r, timeout))
//
nodeChunk = allNodes.slice(3, 4)
linkChunk = allLinks.slice(2, 3)
addData(nodeChunk, linkChunk)
await new Promise((r) => setTimeout(r, timeout))
console.log('addDataInChunks finished')
}
addDataInChunks(nodeData, linkData)
//
// Misc Functions
//
function zoomed(transform) {
const t = transform.transform
const container = this.getElementsByTagNameNS(svgNS, 'g')[0]
const transformString = 'translate(' + t.x + ',' + t.y + ') scale(' + t.k + ')'
container.setAttribute('transform', transformString)
}
function ticked() {
linkContainer
.attr('x1', (d) => d.source.x)
.attr('y1', (d) => d.source.y)
.attr('x2', (d) => d.target.x)
.attr('y2', (d) => d.target.y)
nodeContainer.each(function (d) {
this.setAttribute('cx', d.x)
this.setAttribute('cy', d.y)
})
}
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart()
event.subject.fx = event.subject.x
event.subject.fy = event.subject.y
}
function dragged(event) {
event.subject.fx = event.x
event.subject.fy = event.y
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0)
event.subject.fx = null
event.subject.fy = null
}
</script>
<svg ref="chart" id="chart" class="graph" style="background-color: #141621"></svg>
</html>