D3 adjust linkText position

Viewed 12

I got two issues with my forced graph. The first problem refers to the position of the link text. I added an offSet of 50%, to make sure that each linkText will be centered. This works pretty well if the linkText isn´t long. But it completely looks

akward as soon as the description is longer.

I am not sure if it would be possible to calculate the length of the needed linkText space and somehow subtract it from the given offSet. In general the amount of needed space is there, no clue if it could be used for further calculations.

enter image description here

My second problem relates to the link curves. I added those to be able to visualize bi-directional links. Otherwise those would be on top of each other. The thing is, as soon as you play with a target node and drag them around in a way, that the target node X-posiiton is smaller than the x-position of the source node, the linkText curved wrongly.

enter image description here

Maybe you guys got an idea or hint.

       console.log("D3 Forced Layout ready.")

////////////////////////////////////////////////////////////
//////////////////// D3 Forced Graph ///////////////////////
////////////////////////////////////////////////////////////

    var data = {
        "nodes": [
            { "id": 1 }, 
            { "id": 2 }, 
            { "id": 3 }, 
            { "id": 4 }, 
            { "id": 5 }
        ],
        "links": [
            { "source": 1, "target": 2, "text": "this description is not centered"},
            { "source": 2, "target": 1, "text": "Shorter description" },
            { "source": 2, "target": 3, "text": "Shorter description" },
            { "source": 3, "target": 4, "text": "even shorter" },
            { "source": 4, "target": 5, "text": "shorter" },
            { "source": 5, "target": 1, "text": "short" }
        ]
    }

    initForceLayout()

function initForceLayout() {
    let vw = 800
    let vh = 800

    const svg = d3.select("#chart").append("svg")
                .attr("width", vw)
                .attr("height", vh)
            
    const forceLayout = svg.append("g")
                .attr("id", "forceLayout")
                .call(d3.zoom().on("zoom", function (event) {
                    svg.attr("transform", event.transform)
                }))
                .on("dblclick.zoom", null)

    linksContainer = forceLayout.append("g").attr("class", "linkscontainer")
    nodesContainer = forceLayout.append("g").attr("class", "nodesContainer")
                                
    var simulation = d3.forceSimulation()
        .force("link", d3.forceLink().id(function (d) { return d.id; }).distance(300))
        .force('charge', d3.forceManyBody().strength(-400))
        .force('center', d3.forceCenter(vw / 2, vh / 2))
    
    link = linksContainer.selectAll("g")
        .data(data.links)
        .join("g")
        .attr("cursor", "pointer")

    linkLine = linksContainer.selectAll(".linkPath")
        .data(data.links)
        .join("path")
        .attr("id", function(_,i) {
            return "path" + i
        })
        .attr("stroke", "black")
        .attr("opacity", 0.75)
        .attr("stroke-width", 3)
        .attr("fill", "transparent")

    linkText = linksContainer.selectAll(".linkLabel")
        .data(data.links)
        .join("text")
        .attr("dy", -10)
        .attr("class", "linkLabel")
        .attr("id", function (d, i) {return "linkLabel" + i })
        .text("")

    linkText.append("textPath")
        .attr("xlink:href", function (_, i) {
            return "#path" + i
        })
        .attr("startOffset", "50%")
        .attr("opacity", 0.75)
        .attr("cursor", "pointer")
        .attr("class", "linkText")
        .text(function (d) {
            return d.text
        })
    
    node = nodesContainer.selectAll(".node")
        .data(data.nodes, d => d.id)
        .join("g")
        .attr("class", "node")
        .call(d3.drag()
            .on("start", function(event, d) {
                if (!event.active) simulation.alphaTarget(0.3).restart();
                d.fx = d.x;
                d.fy = d.y;
            })
            .on("drag", function(event, d) {
                d.fx = event.x;
                d.fy = event.y;
            })
            .on("end", function(event, d) {
                if (!event.active) simulation.alphaTarget(0);
                d.fx = undefined;
                d.fy = undefined;       
            })
        )
    
    node.selectAll("circle")
        .data(d => [d])
        .join("circle")
        .attr("r", 30)
        .attr("fill", "whitesmoke")
        .attr("stroke", "white")
        .attr("stroke-width", 2)

    simulation
        .nodes(data.nodes)
        .on("tick", function () {
            // update link positions
            linkLine.attr("d", function (d) {
                if (d.target.x > d.source.x) {    
                    var dx = (d.target.x - d.source.x),
                        dy = (d.target.y - d.source.y),
                        dr = Math.sqrt(dx * dx + dy * dy)
                    return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
                } else if (d.target.x < d.source.x) {
                    var dx = (d.target.x - d.source.x),
                        dy = (d.target.y - d.source.y),
                        dr = Math.sqrt(dx * dx + dy * dy)
                    return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;    
                }
            });
            
            // update node positions
            node
                .attr("transform", function (d) {
                    return "translate(" + d.x + ", " + d.y + ")";
                });

            linkText.attr("transform", function (d) {
                if (d.target.x < d.source.x) {
                    var bbox = this.getBBox();
    
                    rx = bbox.x + bbox.width / 2;
                    ry = bbox.y + bbox.height / 2;
                    return 'rotate(180 ' + rx + ' ' + ry + ')';
                } else if (d.target.x > d.source.x) {
                    var bbox = this.getBBox();
    
                    rx = bbox.x + bbox.width / 2;
                    ry = bbox.y + bbox.height / 2;
                    return 'rotate(0 ' + rx + ' ' + ry + ')';
                }
            })
        })
    
    simulation
        .force("link")
        .links(data.links)

}
    :root {
        --bs-gradient-dark-right: #141727;
        --bs-gradient-dark-left: #3a416f;
    }

    html, body {
        height: 100%;
        width: 100%;
        margin: 0;
        padding: 0;
    }

    body {
        background-color: lightgray;
        overflow: hidden;
    }

    .border-radius-lg {
       border-radius: 0.75rem;
    }

    .svg-container {
        display: inline-block;
        position: relative;
        width: 100%;
        padding-bottom: 100%; /* aspect ratio */
        vertical-align: top;
        overflow: hidden;
    }

    #svg-content-responsive {
        display: inline-block;
        position: absolute;
        top: 10px;
        left: 0;
    }

    svg .rect {
        fill: gold;
        stroke: steelblue;
        stroke-width: 5px;
    }
<!DOCTYPE html>
<html>

<head>
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
    <title>linkText</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- D3.js -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.6.1/d3.min.js" charset="utf-8"></script>    
</head>

<body>
    <div id="chart"></div>
</body>
</html>

0 Answers
Related