In this force directed graph, I am creating a circle or square node depending on what group they have been assigned to.
I am creating these nodes with:
const node = svg
.append('g')
.attr('stroke', '#fff')
.attr('stroke-width', 1.5)
.selectAll()
.data(nodes)
.join((enter) => {
return enter.append((d) => {
console.log('join - d', d)
if (d.group === 1) {
const rectElement = document.createElementNS(svgNS, 'rect')
rectElement.setAttribute('width', 16)
rectElement.setAttribute('height', 16)
rectElement.setAttribute('fill', color(d.group))
return rectElement
} else {
const circleElement = document.createElementNS(svgNS, 'circle')
circleElement.setAttribute('r', 16)
circleElement.setAttribute('fill', color(d.group))
return circleElement
}
})
})
Since the square and circle nodes need different attributes, it seemed right to set them explicitly on the node with .setAttribute
.
This logic worked when creating the nodes, but I am having trouble with the tick
function and implementing something similar.
The function is:
function ticked() {
link
.attr('x1', (d) => d.source.x)
.attr('y1', (d) => d.source.y)
.attr('x2', (d) => d.target.x)
.attr('y2', (d) => d.target.y)
// node.each((d, i) => {
// console.log(`🚀 ~ node.each ~ d:`, i, d)
//
// if (d.group === 1) {
// //
// } else {
// console.log(`🚀 ~ node.each ~ node:`, d3.select(this).node())
//
// // d3.select(this).setAttribute('cx', d.x)
// // this.node().setAttribute('cx', d.x)
// // this.node().setAttribute('cy', d.y)
// }
// })
node.attr('cx', (d) => d.x).attr('cy', (d) => d.y)
}
I am chaining .attr
to set the cx and cy attributes. The problem is these attributes do not apply to rect nodes. I have tried to use the .each
function to be able to process each node and update the attributes. While I can get the data and index for each node, I cannot obtain the node itself to update the attributes.
The documentation (https://d3js.org/d3-selection/control-flow) says:
with this as the current DOM element (nodes[I])
I tried calling this.setAttribute('cy', d.y)
, but that does not work. I tried a few other variants, but haven't figured it out yet.
What am I missing? How have I misinterpreted the documentation? How can I set the attributes based on the node group?
const width = 1000
const height = 400
const svgNS = d3.namespace('svg:text').space
const node_data = Array.from({ length: 5 }, () => ({
group: Math.floor(Math.random() * 3),
}))
const edge_data = Array.from({ length: 10 }, () => ({
source: Math.floor(Math.random() * 5),
target: Math.floor(Math.random() * 5),
value: Math.floor(Math.random() * 10) + 1,
}))
const links = edge_data.map((d) => ({ ...d }))
const nodes = node_data.map((d, index) => ({ id: index, ...d }))
const color = d3.scaleOrdinal(d3.schemeCategory10)
const svg = d3.select('#chart')
const simulation = d3
.forceSimulation(nodes)
.force(
'link',
d3
.forceLink(links)
.id((d) => d.id)
.distance((d) => 100)
)
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2))
.on('tick', ticked)
const link = svg
.append('g')
.attr('stroke', '#999')
.attr('stroke-opacity', 0.6)
.selectAll()
.data(links)
.join('line')
.attr('stroke-width', (d) => Math.sqrt(d.value))
const node = svg
.append('g')
.attr('stroke', '#fff')
.attr('stroke-width', 1.5)
.selectAll()
.data(nodes)
.join((enter) => {
return enter.append((d) => {
if (d.group === 1) {
const rectElement = document.createElementNS(svgNS, 'rect')
rectElement.setAttribute('width', 16)
rectElement.setAttribute('height', 16)
rectElement.setAttribute('fill', color(d.group))
return rectElement
} else {
const circleElement = document.createElementNS(svgNS, 'circle')
circleElement.setAttribute('r', 16)
circleElement.setAttribute('fill', color(d.group))
return circleElement
}
})
})
function ticked() {
link
.attr('x1', (d) => d.source.x)
.attr('y1', (d) => d.source.y)
.attr('x2', (d) => d.target.x)
.attr('y2', (d) => d.target.y)
// node.each((d, i) => {
// console.log(`🚀 ~ node.each ~ d:`, i, d)
//
// if (d.group === 1) {
// //
// } else {
// console.log(`🚀 ~ node.each ~ node:`, d3.select(this).node())
//
// // d3.select(this).setAttribute('cx', d.x)
// // this.node().setAttribute('cx', d.x)
// // this.node().setAttribute('cy', d.y)
// }
// })
node.attr('cx', (d) => d.x).attr('cy', (d) => d.y)
}
.graph {
width: 1000px;
height: 400px;
}
<script src="https://d3js.org/d3.v7.min.js" charset="utf-8"></script>
<svg ref="chart" id="chart" class="graph"></svg>
There are a couple of ways to modify the code so it will work.
The first is to do:
node.each((d, i, n) => {
if (d.group === 1) {
n[i].setAttribute('x', d.x - 8)
n[i].setAttribute('y', d.y - 8)
} else {
n[i].setAttribute('cx', d.x)
n[i].setAttribute('cy', d.y)
}
})
The documentation also states:
current datum (d), the current index (i), and the current group (nodes)
Using the third argument passes (the list of node elements), can grab the node element at index i and set the attributes.
For the second option, the documentation was correct when it said:
with this as the current DOM element (nodes[I])
The mistake in the documentation was not mentioning that arrow functions should not be used. Javascript does not allow this
to be rebound in the way D3 needs it to be.
Passing a classic function, which does permit this
to be bound, works.
node.each(function (d) {
if (d.group === 1) {
this.setAttribute('x', d.x - 8)
this.setAttribute('y', d.y - 8)
} else {
this.setAttribute('cx', d.x)
this.setAttribute('cy', d.y)
}
})
This second option is used in the included code snippet.
const width = 1000
const height = 400
const svgNS = d3.namespace('svg:text').space
const node_data = Array.from({ length: 5 }, () => ({
group: Math.floor(Math.random() * 3),
}))
const edge_data = Array.from({ length: 10 }, () => ({
source: Math.floor(Math.random() * 5),
target: Math.floor(Math.random() * 5),
value: Math.floor(Math.random() * 10) + 1,
}))
const links = edge_data.map((d) => ({ ...d }))
const nodes = node_data.map((d, index) => ({ id: index, ...d }))
const color = d3.scaleOrdinal(d3.schemeCategory10)
const svg = d3.select('#chart')
const simulation = d3
.forceSimulation(nodes)
.force(
'link',
d3
.forceLink(links)
.id((d) => d.id)
.distance((d) => 100)
)
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2))
.on('tick', ticked)
const link = svg
.append('g')
.attr('stroke', '#999')
.attr('stroke-opacity', 0.6)
.selectAll()
.data(links)
.join('line')
.attr('stroke-width', (d) => Math.sqrt(d.value))
const node = svg
.append('g')
.attr('stroke', '#fff')
.attr('stroke-width', 1.5)
.selectAll()
.data(nodes)
.join((enter) => {
return enter.append((d) => {
if (d.group === 1) {
const rectElement = document.createElementNS(svgNS, 'rect')
rectElement.setAttribute('width', 16)
rectElement.setAttribute('height', 16)
rectElement.setAttribute('fill', color(d.group))
return rectElement
} else {
const circleElement = document.createElementNS(svgNS, 'circle')
circleElement.setAttribute('r', 16)
circleElement.setAttribute('fill', color(d.group))
return circleElement
}
})
})
function ticked() {
link
.attr('x1', (d) => d.source.x)
.attr('y1', (d) => d.source.y)
.attr('x2', (d) => d.target.x)
.attr('y2', (d) => d.target.y)
node.each(function (d) {
if (d.group === 1) {
this.setAttribute('x', d.x - 8)
this.setAttribute('y', d.y - 8)
} else {
this.setAttribute('cx', d.x)
this.setAttribute('cy', d.y)
}
})
}
.graph {
width: 1000px;
height: 400px;
}
<script src="https://d3js.org/d3.v7.min.js" charset="utf-8"></script>
<svg ref="chart" id="chart" class="graph"></svg>