# How is entering new nodes in a force directed graph working?

This is related to my earlier post:

I am still missing a basic understanding of how D3 is operating.

When new nodes are add, I would like to have them start in the middle rather then in the top left corner. To accomplish this, I tried doing the following:

``````          .join(
(enter) => {
let e = enter
.append('circle')
.attr('cx', (d) => {
console.log(`π ~ cx .attr ~ d:`, d)
return width / 2
})
.attr('cy', (d) => {
console.log(`π ~ cy .attr ~ d:`, d)
return height / 2
})
.attr('r', 16)
.attr('fill', '#318631')
.attr('stroke', '#7CC07C')
.attr('stroke-width', '3')
e.append('title').text((d) => `hello \${d.id}`)

return e
},
``````

The idea was to provide initial cx & cy values for the new circles. It was my understanding that D3 figures out which nodes are new and that only the new nodes would be passing through enter.

However, I was seeing two strange things:

1. The nodes are still starting in the upper left corner before being pulled to the center. I tried changing cx and cy to x and y, but the behavior remained the same.

2. I thought that enter would apply only the new nodes. However, logging the information, I am seeing:

For the first add, It is operating as expected. Nodes 1 and 2 are new and they are the ones passing through the two .attr functions.

For the second add, only node 3 is being added. However, both node 2 and node 3 are being passed through the two .attr functions.

For the third add, only node 4 is being added. However, nodes 2, 3, & 4 are passing through the two .attr functions.

What I am also finding strange is that node 1 does not show up again in the second and third adds. Why not? Why does 2 show up in the second and third adds? Why does 3 show up in the third add?

Can anyone explain what is happening?

Here is a complete example.

``````<!DOCTYPE html>

<html>
<style>
.graph {
width: 750px;
height: 400px;
}
</style>

<body>
<script src="https://d3js.org/d3.v7.min.js" charset="utf-8"></script>
<svg ref="chart" id="chart" class="graph" style="background-color: #141621"></svg>
<script>
const nodeData = [
{
id: 'node 1',
},
{
id: 'node 2',
},
{
id: 'node 3',
},
{
id: 'node 4',
},
]

{
source: 'node 1',
target: 'node 2',
},
{
source: 'node 1',
target: 'node 3',
},
{
source: 'node 3',
target: 'node 4',
},
]

//
//
//

let svg = null

const width = 750
const height = 400

function setupGraph() {
svg = d3.select('#chart').call(d3.zoom().on('zoom', zoomed)).append('g')
nodeGroup = svg.append('g').attr('stroke', '#fff').attr('stroke-width', 1.5)
}

// nodes and links are the d3 selections
let nodes = null
// currentNodes and currentLinks is the current data
const currentNodes = []

.id((d) => d.id)
.distance((d) => 50)

// overall simulation
const simulation = d3
.forceSimulation()
.force('charge', d3.forceManyBody().strength(-400))
.force('x', d3.forceX())
.force('y', d3.forceY())
.force('center', d3.forceCenter(width / 2, height / 2))
.on('tick', ticked)

nodes = nodeGroup
.selectAll('circle')
.data(currentNodes, (d) => d)
.join(
(enter) => {
let e = enter
.append('circle')
.attr('cx', (d) => {
console.log(`π ~ cx .attr ~ d:`, d)
return width / 2
})
.attr('cy', (d) => {
console.log(`π ~ cy .attr ~ d:`, d)
return height / 2
})
.attr('r', 16)
.attr('fill', '#318631')
.attr('stroke', '#7CC07C')
.attr('stroke-width', '3')
e.append('title').text((d) => `hello \${d.id}`)

return e
},
(update) => update,
(exit) => exit.remove()
)

nodes.call(d3.drag().on('start', dragstarted).on('drag', dragged).on('end', dragended))

.selectAll('line')
.join(
(enter) => enter.append('line').attr('stroke-width', 1),
(update) => update,
(exit) => exit.remove()
)

// stop and start the simulation
simulation.stop()
simulation.nodes(currentNodes)
simulation.alpha(0.3).restart()
}

setupGraph()

const timeout = 7000

let nodeChunk = allNodes.slice(0, 2)

await new Promise((r) => setTimeout(r, timeout))
//
nodeChunk = allNodes.slice(2, 3)

await new Promise((r) => setTimeout(r, timeout))
//
nodeChunk = allNodes.slice(3, 4)

await new Promise((r) => setTimeout(r, timeout))

}

//
// 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() {
.attr('x1', (d) => d.source.x)
.attr('y1', (d) => d.source.y)
.attr('x2', (d) => d.target.x)
.attr('y2', (d) => d.target.y)

nodes.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>
</body>
</html>
``````

Thatβs a bit too much code for me to look at in detail, but my general advice would be to shrink your dataset to the smallest number of items that are still representative of your problem, then keep an eye on the Elements panel in your browserβs devtools while you run your code.

I have shrunk everything as much as I can. It is both simple and straightforward. There are only 4 nodes and three links.

I am beginning to wonder if this represents a bug in D3β¦

I am wondering, without looking at the code, if you could confirm my general understanding that the nodes passing through `enter` should only be the new nodes being addedβ¦?

Your node objects are the only source of truth for force calculations. If you want new nodes to appear at a specific location you need to initialize their position:

``````currentNodes.push(...addNodes.map((d) => ({
...d,
x: width / 2,
y: height / 2
})))
``````

Got it. Thank you.

If one more question could be answered so I can better understand how D3 operates, I would appreciate it.

It was general understanding that the nodes passing through `enter` should only be the new nodes being added. But, this is clearly not the case. I am having difficulty understanding why Node 1 only shows up on the first add and Node 2 shows up in all of them, for example. Can you provide some insight into this behavior?

Thanks again.

Key values have to be strings. What you do in your .data() call is equivalent to

``````.data(currentNodes, (d) => String(d)) // "[object Object]"
``````

Either return the node ID

``````.data(currentNodes, (d) => d.id)
``````

or let D3 use the element order:

``````.data(currentNodes)
``````

Got it. Works perfectly now. I think I am finally starting to understand the design and philosophy of D3.

For anyone who is interested, here is the final version:

``````<!DOCTYPE html>

<html>
<style>
.graph {
width: 750px;
height: 400px;
}
</style>

<body>
<script src="https://d3js.org/d3.v7.min.js" charset="utf-8"></script>
<svg ref="chart" id="chart" class="graph" style="background-color: #141621"></svg>
<script>
const width = 750
const height = 400

const nodeData = [
{
id: 'node 1',
x: width / 2,
y: height / 2,
},
{
id: 'node 2',
x: width / 2,
y: height / 2,
},
{
id: 'node 3',
x: width / 2,
y: height / 2,
},
{
id: 'node 4',
x: width / 2,
y: height / 2,
},
]

{
source: 'node 1',
target: 'node 2',
},
{
source: 'node 1',
target: 'node 3',
},
{
source: 'node 3',
target: 'node 4',
},
]

//
//
//

let svg = null

function setupGraph() {
svg = d3.select('#chart').call(d3.zoom().on('zoom', zoomed)).append('g')
nodeGroup = svg.append('g').attr('stroke', '#fff').attr('stroke-width', 1.5)
}

// nodes and links are the d3 selections
let nodes = null
// currentNodes and currentLinks is the current data
const currentNodes = []

.id((d) => d.id)
.distance((d) => 50)

// overall simulation
const simulation = d3
.forceSimulation()
.force('charge', d3.forceManyBody().strength(-400))
.force('x', d3.forceX())
.force('y', d3.forceY())
.force('center', d3.forceCenter(width / 2, height / 2))
.on('tick', ticked)

nodes = nodeGroup
.selectAll('circle')
.data(currentNodes, (d) => d.id)
.join(
(enter) => {
let e = enter
.append('circle')
.attr('r', 16)
.attr('fill', '#318631')
.attr('stroke', '#7CC07C')
.attr('stroke-width', '3')
e.append('title').text((d) => `hello \${d.id}`)

return e
},
(update) => update,
(exit) => exit.remove()
)

nodes.call(d3.drag().on('start', dragstarted).on('drag', dragged).on('end', dragended))

.selectAll('line')
.join(
(enter) => enter.append('line').attr('stroke-width', 1),
(update) => update,
(exit) => exit.remove()
)

// stop and start the simulation
simulation.stop()
simulation.nodes(currentNodes)
simulation.alpha(0.3).restart()
}

setupGraph()

const timeout = 7000

let nodeChunk = allNodes.slice(0, 2)

await new Promise((r) => setTimeout(r, timeout))
//
nodeChunk = allNodes.slice(2, 3)

await new Promise((r) => setTimeout(r, timeout))
//
nodeChunk = allNodes.slice(3, 4)

await new Promise((r) => setTimeout(r, timeout))

}

//
// 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() {
.attr('x1', (d) => d.source.x)
.attr('y1', (d) => d.source.y)
.attr('x2', (d) => d.target.x)
.attr('y2', (d) => d.target.y)

nodes.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>
</body>
</html>
``````