Place text on an arc with d3 force layout

68 views Asked by At

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:
"Life, physical, & social science" curved around circle

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.

    enter image description here

    enter image description here

  • 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?

0

There are 0 answers