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:
-
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.
-
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>
<head>
<style>
.graph {
width: 750px;
height: 400px;
}
</style>
</head>
<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',
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
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)
linkGroup = svg.append('g').attr('stroke', '#999').attr('stroke-opacity', 0.6)
}
// nodes and links are the d3 selections
let nodes = null
let links = null
// currentNodes and currentLinks is the current data
const currentNodes = []
const currentLinks = []
// the link simulation
const linkSim = d3
.forceLink()
.id((d) => d.id)
.distance((d) => 50)
// overall simulation
const simulation = d3
.forceSimulation()
.force('link', linkSim)
.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)
function addData(addNodes, addLinks) {
console.log(`π ~ addData ~ addNodes:`, addNodes)
console.log(`π ~ addData ~ addLinks:`, addLinks)
addNodes = addNodes.map((d, index) => ({ ...d }))
addLinks = addLinks.map((d) => ({ ...d }))
currentNodes.push(...addNodes)
currentLinks.push(...addLinks)
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))
links = linkGroup
.selectAll('line')
.data(currentLinks)
.join(
(enter) => enter.append('line').attr('stroke-width', 1),
(update) => update,
(exit) => exit.remove()
)
// stop and start the simulation
simulation.stop()
simulation.nodes(currentNodes)
linkSim.links(currentLinks)
simulation.alpha(0.3).restart()
}
setupGraph()
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() {
links
.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>