I am creating a hub and spoke visualisation using d3 force (with sveltekit). I have used Link force, ManyBody force, and Center force. I wanted to add labels to each of the nodes so that the text curves around the edge of the node.
My desired output would look something like this:
Here is the relevant part of the code without any labels
const svg = d3.select('#graph').attr('width', width).attr('height', height);
// Create the simulation
const simulation = d3
.forceSimulation(nodes)
.force(
'link',
d3
.forceLink(links)
.id((d) => d.name)
.distance(linkLength)
)
.force('charge', d3.forceManyBody().strength(-100))
.force('center', d3.forceCenter(width / 2, height / 2));
// Create links
const link = svg
.append('g')
.attr('stroke', 'black')
.selectAll('line')
.data(links)
.join('line')
.attr('stroke-width', 2);
// Create nodes
const node = svg
.append('g')
.attr('stroke', 'black')
.attr('stroke-width', 2)
.selectAll('circle')
.data(nodes)
.join('circle')
.attr('r', nodeRadius)
.attr('fill', 'lightblue');
simulation.on('tick', () => {
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.attr('cx', (d) => d.x).attr('cy', (d) => d.y);
}
I have tried creating labels the curved labels like this:
defs
.selectAll('.node-text-path')
.data(nodes)
.enter()
.append('path')
.attr('id', (d) => `text-path-${d.name.replace(/\s/g, '-')}`)
.attr(
'd',
(d) => `
M ${d.x - nodeRadius}, ${d.y}
a ${nodeRadius},${nodeRadius} 0 1,1 ${nodeRadius * 2},0
a ${nodeRadius},${nodeRadius} 0 1,1 -${nodeRadius * 2},0
`
);
// Create text elements
svg
.selectAll('.node-text')
.data(nodes)
.enter()
.append('text')
.append('textPath')
.attr('xlink:href', (d) => `#text-path-${d.name.replace(/\s/g, '-')}`)
.style('text-anchor', 'middle')
.attr('startOffset', '50%')
.text((d) => d.name);
simulation.on('tick', () => {
// other code
defs.selectAll('.node-text-path').attr(
'd',
(d) => `
M ${d.x - nodeRadius}, ${d.y}
a ${nodeRadius},${nodeRadius} 0 1,1 ${nodeRadius * 2},0
a ${nodeRadius},${nodeRadius} 0 1,1 -${nodeRadius * 2},0
`
);
});
This solution is close but there are a few issues
When the svg is initally loaded, the labels appear in the top left corner of the svg. When it is updated, the labels move into a correct-ish position.
The labels don't follow / update with the simulation, meaning that their position is slighty wonky
ideally I'd like the labels placed beneath the nodes rather than to the right, but I'd let this one slide
Does anyone have any tips / experience with using text on a curved path with a force directed layout?