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.
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.
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>