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:
The documentation also states:
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:
The mistake in the documentation was not mentioning that arrow functions should not be used. Javascript does not allow
thisto be rebound in the way D3 needs it to be.Passing a classic function, which does permit
thisto be bound, works.This second option is used in the included code snippet.