I am trying to convert a D3 force-directed graph codebase. Source code:
And the snippet is here:
/* eslint-disable no-undef */
var width = 600;
var height = 600;
var nodes = [
{ color: "red", size: 5 },
{ color: "orange", size: 10 },
{ color: "yellow", size: 15 },
{ color: "green", size: 20 },
{ color: "blue", size: 25 },
{ color: "purple", size: 30 }
];
var links = [
{ source: "red", target: "orange" },
{ source: "orange", target: "yellow" },
{ source: "yellow", target: "green" },
{ source: "green", target: "blue" },
{ source: "blue", target: "purple" },
{ source: "purple", target: "red" },
{ source: "green", target: "red" }
];
var svg = d3
.select("svg")
.attr("width", width)
.attr("height", height);
var linkSelection = svg
.selectAll("line")
.data(links)
.enter()
.append("line")
.attr("stroke", "black")
.attr("stroke-width", 1);
var nodeSelection = svg
.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("r", d => d.size)
.attr("fill", d => d.color)
.call(
d3
.drag()
.on("start", dragStart)
.on("drag", drag)
.on("end", dragEnd)
);
var simulation = d3.forceSimulation(nodes);
simulation
.force("center", d3.forceCenter(width / 2, height / 2))
.force("nodes", d3.forceManyBody())
.force(
"links",
d3
.forceLink(links)
.id(d => d.color)
.distance(d => 5 * (d.source.size d.target.size))
)
.on("tick", ticked);
function ticked() {
// console.log(simulation.alpha());
nodeSelection.attr("cx", d => d.x).attr("cy", d => d.y);
linkSelection
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
}
function dragStart(d) {
// console.log('drag start');
simulation.alphaTarget(0.5).restart();
d.fx = d.x;
d.fy = d.y;
}
function drag(d) {
// console.log('dragging');
// simulation.alpha(0.5).restart()
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragEnd(d) {
// console.log('drag end');
simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg
version="1.1"
baseProfile="full"
xmlns="http://www.w3.org/2000/svg"
></svg>
But my goal is draw lines with arrows on the end, where the arrows are pointing to a target from a source. Does anyone know how to properly change the line type, and have it point the right way?
CodePudding user response:
You can create a marker representing the arrow tip in your svg, then use it in a marker-end attribute on your lines.
Here is an example: http://thenewcode.com/1068/Making-Arrows-in-SVG
CodePudding user response:
There is a reasonable solution here but it is D3 v3 and uses a path
not line
.
Basically, using a marker
you calculate the amount to shorten your line
so that it starts at the boundary of the source
node (i.e. shorten by the source
radius) and ends at the boundary of the target
node (i.e. shorten at the end by the radius of the target
node plus the dimension of the marker).
The hard-coded 12
in the snippet is the dimension of the marker - if you change e.g. markerWidth
then you will probably need to play with this constant in the tick
function.
So, there are 3 edits to your original code: add the marker
, append the marker
to the line
and in tick
, do the math to shorten each end and replot the line
. See below:
var width = 600;
var height = 200;
var nodes = [
{ color: "red", size: 5 },
{ color: "orange", size: 10 },
{ color: "yellow", size: 15 },
{ color: "green", size: 20 },
{ color: "blue", size: 25 },
{ color: "purple", size: 30 }
];
var links = [
{ source: "red", target: "orange" },
{ source: "orange", target: "yellow" },
{ source: "yellow", target: "green" },
{ source: "green", target: "blue" },
{ source: "blue", target: "purple" },
{ source: "purple", target: "red" },
{ source: "green", target: "red" }
];
var svg = d3
.select("svg")
.attr("width", width)
.attr("height", height);
// append a path marker to svg defs
svg.append("defs").selectAll("marker")
.data(["dominating"])
.enter().append("marker")
.attr("id", function(d) { return d; })
.attr("viewBox", "0 -5 10 10")
.attr("refX", 0)
.attr("refY", 0)
.attr("markerWidth", 12)
.attr("markerHeight", 12)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5");
//
var linkSelection = svg
.selectAll("line")
.data(links)
.enter()
.append("line")
.attr("stroke", "black")
.attr("stroke-width", 1)
// add marker to line
.attr("marker-end", d => "url(#dominating)");
//
var nodeSelection = svg
.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("r", d => d.size)
.attr("fill", d => d.color)
.call(
d3
.drag()
.on("start", dragStart)
.on("drag", drag)
.on("end", dragEnd)
);
var simulation = d3.forceSimulation(nodes);
simulation
.force("center", d3.forceCenter(width / 2, height / 2))
.force("nodes", d3.forceManyBody())
.force(
"links",
d3
.forceLink(links)
.id(d => d.color)
.distance(d => 5 * (d.source.size d.target.size))
)
.on("tick", ticked);
function ticked() {
// console.log(simulation.alpha());
nodeSelection.attr("cx", d => d.x).attr("cy", d => d.y);
linkSelection
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
// recalculate and back off the distance
linkSelection.each(function(d, i, n) {
// current path length
const pl = this.getTotalLength();
// radius of marker head plus def constant
const mrs = (d.source.size);
const mrt = (d.target.size) 12;
// get new start and end points
const m1 = this.getPointAtLength(mrs);
const m2 = this.getPointAtLength(pl - mrt);
// new line start and end
d3.select(n[i])
.attr("x1", m1.x)
.attr("y1", m1.y)
.attr("x2", m2.x)
.attr("y2", m2.y);
});
}
function dragStart(d) {
// console.log('drag start');
simulation.alphaTarget(0.5).restart();
d.fx = d.x;
d.fy = d.y;
}
function drag(d) {
// console.log('dragging');
// simulation.alpha(0.5).restart()
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragEnd(d) {
// console.log('drag end');
simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg
version="1.1"
baseProfile="full"
xmlns="http://www.w3.org/2000/svg"
></svg>
PS works on v4.13, v5.7 and v7.6.1