I want to exchange the current node label with a curved label, placed above the node. I miss knowledge regarding, how to use paths proper and how to append a text to a path. Maybe you guys could enlight me.
The text should have the same curve as the node itself.
UPDATE
I implemented the solution but it seems either the path or textPath is to short. Its possible to adjust the innerRadius, outerRadius as well as the startAngle and endAngle. I guess the startAngle and endAngle define the length of the arc. No matter which value I test, the full label isn´t shown.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Curved Text</title>
<!-- Script Import -->
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<style>
body {
background: white;
overflow: hidden;
margin: 0px;
}
circle {
fill: whitesmoke;
stroke-width: 2;
stroke: black;
transform: scale(1);
transition: all 200ms ease-in-out;
}
circle:hover {
transform: scale(1.5)
}
</style>
<body>
<svg id="svg"></svg>
<script>
var width = window.innerWidth,
height = window.innerHeight,
radius = 40, // circle radius
offset = 35; // arrow offset
const svg = d3.select('svg')
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function (event) {
svg.attr("transform", event.transform)
}))
.append("g")
var graph = {
"nodes": [
{ "id": "Stackoverflow" },
{ "id": "Reddit" },
{ "id": "Google" }
],
"links": [
{ "source": "Stackoverflow", "target": "Reddit"},
{ "source": "Reddit", "target": "Google"},
{ "source": "Google", "target": "Stackoverflow"},
]
}
const arc = d3.arc()
.innerRadius(radius 5)
.outerRadius(radius 5)
.startAngle(-Math.PI / 15)
.endAngle(Math.PI / 2 )
svg.append("defs")
.append("path")
.attr("id", "curvedLabelPath")
.attr("d", arc())
svg.append('defs').append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '-0 -5 10 10')
.attr('refX', 0)
.attr('refY', 0)
.attr('orient', 'auto')
.attr('markerWidth', 10)
.attr('markerHeight', 10)
.attr('xoverflow', 'visible')
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#999')
.style('stroke', 'none');
var linksContainer = svg.append("g").attr("class", "linksContainer")
var nodesContainer = svg.append("g").attr("class", "nodesContainer")
const simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function (d) { return d.id }).distance(250))
.force("charge", d3.forceManyBody().strength(-1000))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collision", d3.forceCollide().radius(radius))
link = linksContainer.selectAll("g")
.data(graph.links)
.join("g")
.attr("curcor", "pointer")
link = linksContainer.selectAll("path")
.data(graph.links)
.join("path")
.attr("id", function (_, i) {
return "path" i
})
.attr("stroke", "#000000")
.attr("opacity", 0.75)
.attr("stroke-width", 3)
.attr("fill", "transparent")
.attr("marker-end", "url(#arrowhead)")
node = nodesContainer.selectAll(".node")
.data(graph.nodes, d => d.id)
.join("g")
.attr("cursor", "pointer")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on("mouseenter", function (d) {
d3.select(this).select("text").attr("font-size", 15)
})
node.selectAll("circle")
.data(d => [d])
.join("circle")
.attr("r", radius)
node.append("text")
.append("textPath")
.attr("href", "#curvedLabelPath")
.attr("text-anchor", "middle")
.attr("startoffset", "5%")
.text(function (d) {
return d.id
})
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation
.force("link")
.links(graph.links);
function ticked() {
link.attr("d", function (d) {
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;
});
// Neuberechnung der Distanz
link.attr("d", function (d) {
// Länge des aktuellen Paths
var pl = this.getTotalLength(),
// Kreis Radius und Distanzwert
r = radius offset,
// Umlaufposition wo der Path den Kreis berührt
m = this.getPointAtLength(pl - r);
var dx = m.x - d.source.x,
dy = m.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 " m.x "," m.y;
});
node
.attr("transform", d => `translate(${d.x}, ${d.y})`);
}
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
dataFlow()
function dataFlow() {
var lines = linksContainer.selectAll("path")
dataflow = window.setInterval(function () {
lines.style("stroke-dashoffset", offset)
.style("stroke", "black")
.style("stroke-dasharray", 5)
.style("opacity", 0.5)
offset -= 1
}, 40)
var offset = 1;
}
</script>
</body>
</html>
CodePudding user response:
Here's an example for one circle and label that uses a <textPath>.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
// set up
const width = 200;
const height = 200;
const svg = d3.select('#chart')
.append('svg')
.attr('width', width)
.attr('height', height);
// radius of the circle that the label is above
const radius = 50;
// arc generator for the curved path
const arc = d3.arc()
// add a bit of space so that the label
// won't be right on the circle
.innerRadius(radius 5)
.outerRadius(radius 5)
.startAngle(-Math.PI / 2)
.endAngle(Math.PI / 2);
// add the path that the label will follow to <defs>
svg.append('defs')
.append('path')
.attr('id', 'curvedLabelPath')
.attr('d', arc());
// create a group for the circle and label
const g = svg.append('g')
.attr('transform', `translate(${width / 2},${height / 2})`);
// draw the circle
g.append('circle')
.attr('stroke', 'black')
.attr('fill', '#d3d3d3')
.attr('r', radius);
// draw the label
g.append('text')
.append('textPath')
.attr('href', '#curvedLabelPath')
// these two lines center along the arc.
// the offset is 25% instead of 50% because d3.arc() creates
// an arc that has an outer and inner part. in this case,
// each parth is ~50% of the path, so the middle of the
// outer arc is 25%
.attr('text-anchor', 'middle')
.attr('startOffset', '25%')
.text('stackoverflow');
</script>
</body>
</html>