How to shorten length of links and space them out in d3?

162 Views Asked by At

I have created a force-directed graph in D3. I will attach the relevant code. I'm trying to space out the links in a better way and avoid collision of arrows when there are multiple links to one node (see image) my graph

In terms of arrow placement, this is what I'm going for: desired outcome

I have tried adjusting the x2 and y2 coords in the tick event. This moves the arrows around as expected, but I need a way to standardize these coords (I think) so that arrows won't collide and they are always near the edges of the nodes.


          const link = svg.selectAll(".link")
          .data(this.actionData.links).enter().append("line") 
          .attr("class", "link") 
          .style("stroke", "black")
          .attr('marker-end', 'url(#arrow)')

        const node = svg.selectAll(".node")
            .data(this.actionData.nodes).enter().append("rect")
            .attr("class", "node")
            .attr("width", 20)
            .attr("height", 30)
            .attr("rx", 10) 
            .attr("ry", 5) 
            .style("stroke", "black")
            .style("fill", "pink");

// arrow
 let markerBoxWidth = 20
        let markerBoxHeight = 20
        let refX = markerBoxWidth / 2
        let refY = markerBoxHeight / 2
        let arrowPoints = [[0, 0], [0, 20], [20, 10]];

// arrow
        svg
            .append('defs')
            .append('marker')
            .attr('id', 'arrow')
            .attr('viewBox', [0, 0, markerBoxWidth, markerBoxHeight])
            .attr('refX', refX)
            .attr('refY', refY)
            .attr('markerWidth', markerBoxWidth)
            .attr('markerHeight', markerBoxHeight)
            .attr('orient', 'auto-start-reverse')
            .append('path')
            .attr('d', d3.line()(arrowPoints))
            .attr('stroke', 'black');

        let simulation = d3.forceSimulation(this.actionData.nodes)
            .force("center", d3.forceCenter(width/2, height/2))
            .force("link", d3.forceLink(this.actionData.links)
                .id(link => link.id)
                .distance(200))
            .force("charge", d3.forceManyBody().strength(-500))
         
          

        simulation.on("tick", () => {
            node
                .attr("x", node => node.x)
                .attr("y", node => node.y)
            // connecting the links to the nodes 
            link
                // .attr("x1", link => link.source.x + 100) what i tried, didn't work
                // .attr("y1", link => link.source.y + 20)
                // .attr("x2", link => link.target.x + 20)
                // .attr("y2", link => link.target.y - 20)
                .attr("x1", link => link.source.x) 
                .attr("y1", link => link.source.y)
                .attr("x2", link => link.target.x)
                .attr("y2", link => link.target.y)

            
        })


           
1

There are 1 best solutions below

0
Ganesh Nemade On
simulation.on("tick", () => {
  node.attr("x", node => node.x)
      .attr("y", node => node.y);

  link.attr("x1", link => link.source.x)
      .attr("y1", link => link.source.y)
      .attr("x2", link => link.target.x)
      .attr("y2", link => link.target.y)
      .attr("transform", (link) => {
        // Calculate the angle of the line between source and target
        let dx = link.target.x - link.source.x;
        let dy = link.target.y - link.source.y;
        let angle = Math.atan2(dy, dx);

        // Calculate the offset based on the index of the link
        let index = link.source.links.indexOf(link);
        let offset = (index - (link.source.links.length - 1) / 2) * 10;

        // Return a transform string to apply the rotation and offset
        return `rotate(${angle * 180 / Math.PI}, ${link.target.x}, ${link.target.y}) translate(0, ${offset})`;
      });
});

Math.atan2 function calculates the angle of the line between the source & thetarget nodes.

I think if we rotate and translate, it will not collide.