I am facing two visual problems with the given D3 forced graph.
- The last inch of each link is still visible. The arrowhead should end exactly at the node edge, which is the case, but the red link underneath is visible. I could adjust the link stroke-width or the arrow size but actually do not want to change those attributes.
- The radius of a node increases on a mouseenter event. If node size increases, the arrowhead is out of position and does not end on the node endge anymore. I assume I need to adjust the tick function, but do not find a proper starting point.
I appreciate any link, hint, help which gets me closer to the result.
var width = window.innerWidth,
height = window.innerHeight;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function(event) {
svg.attr("transform", event.transform)
}))
.append("g")
////////////////////////
// outer force layout
var data = {
"nodes":[
{ "id": "A" },
{ "id": "B" },
{ "id": "C" },
],
"links": [
{ "source": "A", "target": "B"},
{ "source": "B", "target": "C"},
{ "source": "C", "target": "A"}
]
};
var simulation = d3.forceSimulation()
.force("size", d3.forceCenter(width / 2, height / 2))
.force("charge", d3.forceManyBody().strength(-1000))
.force("link", d3.forceLink().id(function (d) { return d.id }).distance(250))
svg.append("defs").append("marker")
.attr('id', 'arrowhead')
.attr('viewBox', '-0 -5 10 10')
.attr("refX", 23.5)
.attr("refY", 0)
.attr('orient', 'auto')
.attr('markerWidth', 10)
.attr('markerHeight', 10)
.attr("orient", "auto")
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#999')
.style('stroke', 'none');
var links = svg.selectAll(".links")
.data(data.links)
.join("line")
.attr("stroke", "red")
.attr("stroke-width", 3)
.attr("marker-end", "url(#arrowhead)")
var nodes = svg.selectAll("g.outer")
.data(data.nodes, function (d) { return d.id; })
.enter()
.append("g")
.attr("class", "outer")
.attr("id", function (d) { return d.id; })
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
);
nodes
.append("circle")
.style("fill", "lightgrey")
.style("stroke", "blue")
.attr("r", 40)
.on("mouseenter", function() {
d3.select(this)
.transition()
.duration(250)
.attr("r", 40 * 1.3)
.attr("fill", "blue")
})
.on("mouseleave", function() {
d3.select(this)
.transition()
.duration(250)
.attr("r", 40)
.attr("fill", "lightgrey")
})
simulation
.nodes(data.nodes)
.on("tick", tick)
simulation
.force("link")
.links(data.links)
function tick() {
links
.attr("x1", function (d) { return d.source.x; })
.attr("y1", function (d) { return d.source.y; })
.attr("x2", function (d) { return d.target.x; })
.attr("y2", function (d) { return d.target.y; });
nodes
.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;
}
body {
background: whitesmoke,´;
overflow: hidden;
margin: 0px;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>D3v7</title>
<!-- d3.js framework -->
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
</body>
</html>
CodePudding user response:
solution
at the moment I can think of two solutions.
- calculate each
<path(.links)/>
vector and subtract with extended circle radius - give offset to each
<marker />
personally I've chosen the 2nd solution; it requires less computation and easier to understand.
1. give each links custom arrowhead
add separate arrowheads for each link because it seems individual offset is not supported by svg path.
// add custom arrowheads for each link
// to avoid exhaustive computations
svg.append("defs").selectAll('marker')
.data(data.links)
.join("marker")
.attr('id', d => `arrowhead-target-${d.target}`)
// ...
var links = svg.selectAll(".links")
.data(data.links)
.join("line")
.attr("stroke", "red")
.attr("stroke-width", 3)
.attr("marker-end", d => `url(#arrowhead-target-${d.target})`)
// add source and target so that it can be traced back
2. extend, subtract each individual marker's offset with mouseevent
the offset for <marker />
is applied with .refX, .refY
attributes, as noted in MDN docs
nodes
.append("circle")
.style("fill", "lightgrey")
.style("stroke", "blue")
.attr("r", 40)
.on("mouseenter", function(ev, d) {
// trace back to custom arrowhead with given id
const arrowHead = d3.select(`#arrowhead-target-${d.id}`)
arrowHead.transition().duration(250).attr('refX', 40)
d3.select(this)
.transition()
.duration(250)
.attr("r", 40 * 1.3)
.attr("fill", "blue")
})
.on("mouseleave", function(ev, d) {
const arrowHead = d3.select(`#arrowhead-target-${d.id}`)
arrowHead.transition().duration(250).attr('refX', 23.5)
d3.select(this)
.transition()
.duration(250)
.attr("r", 40)
.attr("fill", "lightgrey")
})
Try run the code below. The size of extension and subtraction is purposefully exaggerated so that you can see the difference.
var width = window.innerWidth,
height = window.innerHeight;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function(event) {
svg.attr("transform", event.transform)
}))
.append("g")
////////////////////////
// outer force layout
var data = {
"nodes":[
{ "id": "A" },
{ "id": "B" },
{ "id": "C" },
],
"links": [
{ "source": "A", "target": "B"},
{ "source": "B", "target": "C"},
{ "source": "C", "target": "A"}
]
};
var simulation = d3.forceSimulation()
.force("size", d3.forceCenter(width / 2, height / 2))
.force("charge", d3.forceManyBody().strength(-1000))
.force("link", d3.forceLink().id(function (d) { return d.id }).distance(250))
// add custom arrowheads for each link
// to avoid exhaustive computations
svg.append("defs").selectAll('marker')
.data(data.links)
.join("marker")
.attr('id', d => `arrowhead-target-${d.target}`)
.attr('viewBox', '-0 -5 10 10')
.attr("refX", 23.5)
.attr("refY", 0)
.attr('orient', 'auto')
.attr('markerWidth', 10)
.attr('markerHeight', 10)
.attr("orient", "auto")
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#999')
.style('stroke', 'none');
var links = svg.selectAll(".links")
.data(data.links)
.join("line")
.attr("stroke", "red")
.attr("stroke-width", 3)
.attr("marker-end", d => `url(#arrowhead-target-${d.target})`)
// add source and target so that it can be traced back
.attr('data-source', d => d.source)
.attr('data-target', d => d.target)
var nodes = svg.selectAll("g.outer")
.data(data.nodes, function (d) { return d.id; })
.enter()
.append("g")
.attr("class", "outer")
.attr("id", function (d) { return d.id; })
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
);
nodes
.append("circle")
.style("fill", "lightgrey")
.style("stroke", "blue")
.attr("r", 40)
.on("mouseenter", function(ev, d) {
// trace back to custom arrowhead with given id
const arrowHead = d3.select(`#arrowhead-target-${d.id}`)
arrowHead.transition().duration(250).attr('refX', 40)
d3.select(this)
.transition()
.duration(250)
.attr("r", 40 * 1.3)
.attr("fill", "blue")
})
.on("mouseleave", function(ev, d) {
const arrowHead = d3.select(`#arrowhead-target-${d.id}`)
arrowHead.transition().duration(250).attr('refX', 23.5)
d3.select(this)
.transition()
.duration(250)
.attr("r", 40)
.attr("fill", "lightgrey")
})
simulation
.nodes(data.nodes)
.on("tick", tick)
simulation
.force("link")
.links(data.links)
function tick() {
links
.attr("x1", function (d) { return d.source.x; })
.attr("y1", function (d) { return d.source.y; })
.attr("x2", function (d) { return d.target.x; })
.attr("y2", function (d) { return d.target.y; });
nodes
.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;
}
body {
background: whitesmoke,´;
overflow: hidden;
margin: 0px;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>D3v7</title>
<!-- d3.js framework -->
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
</body>
</html>