Reputation: 722
Here is my sample code that shows a simple d3 graph which supports node dragging without force layout:
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.link {
stroke: #aaa;
}
.node text {
stroke:#333;
cursos:pointer;
}
.node circle{
stroke:#fff;
stroke-width:3px;
fill:#555;
}
</style>
<body>
<p id="first"><p>
<p id="second"><p>
<script>
var data = {
"nodes": [{
"id": "source1",
"x": 33,
"y": 133,
"width": 50,
"height": 50
},
{
"id": "target1",
"x": 166,
"y": 66,
"width": 50,
"height": 50
},
{
"id": "source2",
"x": 250,
"y": 40,
"width": 50,
"height": 50
},
{
"id": "target2",
"x": 350,
"y": 133,
"width": 50,
"height": 50
}
],
"links": [{
"source": "source1",
"target": "target1",
"weight": 1,
"id": "abc"
},
{
"source": "source2",
"target": "target2",
"weight": 3,
"id": "xyz"
}
]
};
var width = 1200,
height = 500
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var counterxOrtho = 0;
// Bootstrap the Drag Capability
var drag = d3.behavior.drag()
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);
var dragInitiated = false
function dragstarted(d) {
d3.selectAll(".node").each(function(d) {
d3.select(this).classed("selectedNode", function(d) {
return d.selected = false;
})
})
d3.select(this).classed("selectedNode", function(d) {
d.previouslySelected = d.selected;
return d.selected = true;
});
dragInitiated = true
}
function dragged(d, i) {
if (dragInitiated) {
d3.event.sourceEvent.stopPropagation();
d3.selectAll(".linkInGraph").attr("d", function(l) {
var sourceNode = data.nodes.filter(function(d, i) {
return d.id == l.source
})[0];
var targetNode = data.nodes.filter(function(d, i) {
return d.id == l.target
})[0];
if (!(sourceNode.selected || targetNode.selected)) {
lineData.length = 0;
controlPointsArr = [];
l.controlPoints.forEach(function(d) {
controlPointsArr.push(d);
})
for (i = 0; i < controlPointsArr.length; i += 2) {
lineData.push({
"a": controlPointsArr[i],
"b": controlPointsArr[i + 1]
});
}
return lineFunction(lineData)
}
lineData.length = 0;
controlPointsArr = [];
var randomVal = 0;
randomVal = 25;
lineData.push({
"a": sourceNode.x + randomVal,
"b": sourceNode.y + 50
});
controlPointsArr.push(sourceNode.x + randomVal);
controlPointsArr.push(sourceNode.y + 50);
lineData.push({
"a": targetNode.x + randomVal,
"b": targetNode.y - 8
});
controlPointsArr.push(targetNode.x + randomVal);
controlPointsArr.push(targetNode.y - 8);
l.controlPoints = [];
for (i = 0; i < controlPointsArr.length; i++) {
l.controlPoints.push(controlPointsArr[i]);
}
return lineFunction(lineData)
})
nodes.filter(function(d) {
return d.selected;
})
.each(function(d) {
d.x += d3.event.dx;
d.y += d3.event.dy;
var a = d.id;
var b = "\"";
var position = 0;
var output = [a.slice(0, position), b, a.slice(position)].join('');
output += "\"";
d3.select("[id=" + output + "]").attr("transform", "translate(" + (d.x) + "," + (d.y) + ")");
});
}
}
function dragended(d) {
if (d3.event.sourceEvent.which == 1) {
dragInitiated = false;
}
}
var nodes = svg.selectAll(".node")
.data(data.nodes)
.enter().append("g").attr("id", function(d) {
return d.id
})
.attr("class", "node").call(drag).attr("transform", function(d, i) {
return "translate(" + d.x + "," + d.y + ")";
});
nodes.append("rect")
.attr("width", "50").attr("height", "50").attr("fill", "lime").attr("rx", "5")
.attr("ry", "5").style("stroke", "grey").style("stroke-width", "1.5").style("stroke-dasharray", "").style("opacity", ".9");
nodes.append("text")
.attr("dx", 12)
.attr("dy", ".35em").attr("x", -12).attr("y", 25)
.text(function(d) {
return d.id
});
var LinkCurve = "linear";
var lineFunction = d3.svg.line()
.x(function(d) {
return d.a;
})
.y(function(d) {
return d.b;
})
.interpolate(LinkCurve);
// Marker elements for edges
var pathMarker = svg.append("marker").attr("id", "pathMarkerHead").attr("orient", "auto").attr("markerWidth", "8").attr("markerHeight", "12").attr("refX", "0.1").attr("refY", "2");
pathMarker.append("path").attr("d", "M0,0 V4 L7,2 Z").attr("fill", "navy")
//
//The data for our line
var lineData = [];
function setupPolyLinks() {
d3.selectAll(".linkInGraph").remove();
edges = svg.selectAll("linkInGraph")
.data(data.links)
.enter()
.insert("path", ".node")
.attr("class", "linkInGraph").attr("id", function(l) {
return l.id;
}).attr("source", function(l) {
return l.source;
}).attr("target", function(l) {
return l.target;
}).attr("marker-end", "url(#pathMarkerHead)").attr("d", function(l) {
lineData.length = 0;
controlPointsArr = [];
var sourceNode = data.nodes.filter(function(d, i) {
return d.id == l.source
})[0];
var targetNode = data.nodes.filter(function(d, i) {
return d.id == l.target
})[0];
lineData.push({
"a": sourceNode.x + 25,
"b": sourceNode.y + 50
});
controlPointsArr.push(sourceNode.x + 25);
controlPointsArr.push(sourceNode.y + 50);
lineData.push({
"a": targetNode.x + 25,
"b": targetNode.y
});
controlPointsArr.push(targetNode.x + 25);
controlPointsArr.push(targetNode.y);
l.controlPoints = [];
for (i = 0; i < controlPointsArr.length; i++) {
l.controlPoints.push(controlPointsArr[i]);
}
return lineFunction(lineData)
}).style("stroke-width", "2").attr("stroke", "blue").attr("fill", "none");
}
setupPolyLinks();
</script>
In this graph, while dragging nodes, the associated link always starts and ends at static points i.e. in this case, the link starts from lower middle point of source and end at top middle point of target.
What I want to achieve is when dragging node, the links start and end point should auto adjust like:
-In a case where target is at top and source just below it, then link should start at from top middle of source and end at bottom middle of target.
But in my case it appears like this, which I don't want:
-In a case where source and target are in a horizontal line where first is source and then target, then link should start at from right middle of source and end at left middle of target.
In my case it is like this:
And more cases like this...
The idea is for a link to never overlap with its own node while dragging.
Upvotes: 1
Views: 1291
Reputation: 722
Here is another solution I found myself, it uses the angle calculation of dragged nodes to find the best position of links.
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.link {
stroke: #aaa;
}
.node text {
stroke:#333;
cursos:pointer;
}
.node circle{
stroke:#fff;
stroke-width:3px;
fill:#555;
}
</style>
<body>
<p id="first"><p>
<p id="second"><p>
<script>
var data = {
"nodes": [{
"id": "source1",
"x": 200,
"y": 300,
"width": 50,
"height": 50
},
{
"id": "target1",
"x": 500,
"y": 200,
"width": 50,
"height": 50
},
{
"id": "source2",
"x": 600,
"y": 120,
"width": 50,
"height": 50
},
{
"id": "target2",
"x": 900,
"y": 300,
"width": 50,
"height": 50
}
],
"links": [{
"source": "source1",
"target": "target1",
"weight": 1,
"id": "abc"
},
{
"source": "source2",
"target": "target2",
"weight": 3,
"id": "xyz"
}
]
}
var width = 1200,
height = 500
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var counterxOrtho = 0;
// Bootstrap the Drag Capability
var drag = d3.behavior.drag()
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);
var dragInitiated = false
function dragstarted(d) {
d3.selectAll(".node").each(function(d) {
d3.select(this).classed("selectedNode", function(d) {
return d.selected = false;
})
})
d3.select(this).classed("selectedNode", function(d) {
d.previouslySelected = d.selected;
return d.selected = true;
});
dragInitiated = true
}
function dragged(d, i) {
if (dragInitiated) {
d3.event.sourceEvent.stopPropagation();
d3.selectAll(".linksOnUi").attr("d", function(l) {
var sourceNode = data.nodes.filter(function(d, i) {
return d.id == l.source
})[0];
var targetNode = data.nodes.filter(function(d, i) {
return d.id == l.target
})[0];
// Angle calculation to check the position of target/source node with respective of it's source/target node
// to find where the link should start/end to make it look better while dragging
var dy = targetNode.y - sourceNode.y;
var dx = targetNode.x - sourceNode.x;
var theta = Math.atan2(dy, dx);
theta *= 180 / Math.PI;
var SourceMX = 0;
var SourceMY = 0;
var TargetMX = 0;
var TargetMY = 0;
if (theta <= 170 && theta >= 10) {
SourceMX = 0;
SourceMY = 0;
TargetMX = 0;
TargetMY = 0;
} else if ((theta <= 180 && theta >= 170) || (theta <= -150 && theta >= -180)) {
SourceMX = -sourceNode.width / 2;
SourceMY = -sourceNode.height / 2;
TargetMX = targetNode.width / 2 + 8;
TargetMY = targetNode.height / 2;
} else if (theta <= -45 && theta >= -150) {
SourceMX = 0;
SourceMY = -sourceNode.height;
TargetMX = 0;
TargetMY = targetNode.height + 14;
} else {
SourceMX = sourceNode.width / 2;
SourceMY = -sourceNode.height / 2;
TargetMX = -targetNode.width / 2 - 8;
TargetMY = targetNode.height / 2;
}
if (!(sourceNode.selected || targetNode.selected)) {
lineData.length = 0;
controlPointsArr = [];
l.controlPoints.forEach(function(d) {
controlPointsArr.push(d);
})
for (i = 0; i < controlPointsArr.length; i += 2) {
lineData.push({
"a": controlPointsArr[i],
"b": controlPointsArr[i + 1]
});
}
return lineFunction(lineData)
}
lineData.length = 0;
controlPointsArr = [];
var randomVal = 0;
randomVal = 25;
lineData.push({
"a": sourceNode.x + randomVal + SourceMX,
"b": sourceNode.y + 50 + SourceMY
});
controlPointsArr.push(sourceNode.x + randomVal + SourceMX);
controlPointsArr.push(sourceNode.y + 50 + SourceMY);
lineData.push({
"a": targetNode.x + randomVal + TargetMX,
"b": targetNode.y - 8 + TargetMY
});
controlPointsArr.push(targetNode.x + randomVal + TargetMX);
controlPointsArr.push(targetNode.y - 8 + TargetMY);
counterxOrtho = counterxOrtho + .9;
if (counterxOrtho > 20) {
counterxOrtho = 20
}
l.controlPoints = [];
for (i = 0; i < controlPointsArr.length; i++) {
l.controlPoints.push(controlPointsArr[i]);
}
return lineFunction(lineData)
})
nodes.filter(function(d) {
return d.selected;
})
.each(function(d) {
d.x += d3.event.dx;
d.y += d3.event.dy;
var a = d.id;
var b = "\"";
var position = 0;
var output = [a.slice(0, position), b, a.slice(position)].join('');
output += "\"";
d3.select("[id=" + output + "]").attr("transform", "translate(" + (d.x) + "," + (d.y) + ")");
});
}
}
function dragended(d) {
if (d3.event.sourceEvent.which == 1) {
dragInitiated = false;
}
}
var nodes = svg.selectAll(".node")
.data(data.nodes)
.enter().append("g").attr("id", function(d) {
return d.id
})
.attr("class", "node").call(drag).attr("transform", function(d, i) {
return "translate(" + d.x + "," + d.y + ")";
});
nodes.append("rect")
.attr("width", "50").attr("height", "50").attr("fill", "lime").attr("rx", "5")
.attr("ry", "5").style("stroke", "grey").style("stroke-width", "1.5").style("stroke-dasharray", "").style("opacity", ".9");
nodes.append("text")
.attr("dx", 12)
.attr("dy", ".35em").attr("x", -12).attr("y", 25)
.text(function(d) {
return d.id
});
var LinkCurve = "linear";
var lineFunction = d3.svg.line()
.x(function(d) {
return d.a;
})
.y(function(d) {
return d.b;
})
.interpolate(LinkCurve);
// Marker elements for edges
var pathMarker = svg.append("marker").attr("id", "arrowHeadMarker").attr("orient", "auto").attr("markerWidth", "8").attr("markerHeight", "12").attr("refX", "0.1").attr("refY", "2");
pathMarker.append("path").attr("d", "M0,0 V4 L7,2 Z").attr("fill", "navy")
//
//The data for our line
var lineData = [];
function linkSetupFuncn() {
d3.selectAll(".linksOnUi").remove();
edges = svg.selectAll("linksOnUi")
.data(data.links)
.enter()
.insert("path", ".node")
.attr("class", "linksOnUi").attr("id", function(l) {
return l.id;
}).attr("source", function(l) {
return l.source;
}).attr("target", function(l) {
return l.target;
}).attr("marker-end", "url(#arrowHeadMarker)").attr("d", function(l) {
lineData.length = 0;
controlPointsArr = [];
var sourceNode = data.nodes.filter(function(d, i) {
return d.id == l.source
})[0];
var targetNode = data.nodes.filter(function(d, i) {
return d.id == l.target
})[0];
lineData.push({
"a": sourceNode.x + 25,
"b": sourceNode.y + 50
});
controlPointsArr.push(sourceNode.x + 25);
controlPointsArr.push(sourceNode.y + 50);
lineData.push({
"a": targetNode.x + 25,
"b": targetNode.y
});
controlPointsArr.push(targetNode.x + 25);
controlPointsArr.push(targetNode.y);
l.controlPoints = [];
for (i = 0; i < controlPointsArr.length; i++) {
l.controlPoints.push(controlPointsArr[i]);
}
return lineFunction(lineData)
}).style("stroke-width", "2").attr("stroke", "blue").attr("fill", "none");
}
linkSetupFuncn();
</script>
Upvotes: 0
Reputation: 61774
Here is a solution which attaches links to the right side of the node being dragged:
var data = {
"nodes": [{
"id": "source1",
"x": 33,
"y": 133,
"width": 50,
"height": 50
},
{
"id": "target1",
"x": 166,
"y": 66,
"width": 50,
"height": 50
},
{
"id": "source2",
"x": 250,
"y": 40,
"width": 50,
"height": 50
},
{
"id": "target2",
"x": 350,
"y": 133,
"width": 50,
"height": 50
}
],
"links": [{
"source": "source1",
"target": "target1",
"weight": 1,
"id": "abc"
},
{
"source": "source2",
"target": "target2",
"weight": 3,
"id": "xyz"
}
]
};
let svg = d3.select("svg").attr("width", 1200).attr("height", 500);
// nodes:
let nodes = svg.selectAll(".node")
.data(data.nodes)
.enter().append("g")
.attr("id", d => d.id)
.attr("class", "node")
.attr("transform", d => "translate(" + d.x + "," + d.y + ")")
.call(d3.drag().on("drag", dragged));
nodes.append("rect")
.attr("width", 50).attr("height", 50)
.attr("fill", "lime")
.attr("rx", 5).attr("ry", 5)
.style("stroke", "grey").style("stroke-width", "1.5").style("stroke-dasharray", "")
.style("opacity", ".9")
.style("cursor", "pointer");
nodes.append("text")
.attr("x", -12).attr("y", 25)
.attr("dx", 12).attr("dy", ".35em")
.text(d => d.id)
.style("cursor", "pointer");
// links:
var pathMarker = svg.append("marker").attr("id", "pathMarkerHead").attr("orient", "auto").attr("markerWidth", "8").attr("markerHeight", "12").attr("refX", "7").attr("refY", "2");
pathMarker.append("path").attr("d", "M0,0 V4 L7,2 Z").attr("fill", "navy");
svg.selectAll("linkInGraph")
.data(data.links)
.enter().append("path")
.attr("class", "linkInGraph")
.attr("id", d => d.id)
.attr("d", moveLink)
.style("stroke-width", "2").attr("stroke", "blue").attr("fill", "none")
.attr("marker-end", "url(#pathMarkerHead)");
// drag behavior:
function dragged(n) {
// Move the node:
d3.select(this)
.attr(
"transform",
d => "translate(" + (d.x = d3.event.x) + "," + (d.y = d3.event.y) + ")"
);
// Move the link:
d3.selectAll(".linkInGraph")
.filter(l => l.source == n.id || l.target == n.id)
.attr("d", moveLink)
}
// link position:
function moveLink(l) {
let nsid = data.nodes.filter(n => n.id == l.source)[0].id;
let ndid = data.nodes.filter(n => n.id == l.target)[0].id;
let ns = d3.select("#" + nsid).datum();
let nd = d3.select("#" + ndid).datum();
let min = Number.MAX_SAFE_INTEGER;
let best = {};
[[25, 0], [50, 25], [25, 50], [0, 25]].forEach(s =>
[[25, 0], [50, 25], [25, 50], [0, 25]].forEach(d => {
let dist = Math.hypot(
(nd.x + d[0]) - (ns.x + s[0]),
(nd.y + d[1]) - (ns.y + s[1])
);
if (dist < min) {
min = dist;
best = {
s: { x: ns.x + s[0], y: ns.y + s[1] },
d: { x: nd.x + d[0], y: nd.y + d[1] }
};
}
})
);
var lineFunction = d3.line().x(d => d.x).y(d => d.y).curve(d3.curveLinear);
return lineFunction([best.s, best.d]);
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
Since the goal consists in avoiding overlap between the dragged node and its link, we have to attach links to the appropriate side of its nodes.
For a given link, the optimal node's sides are simply the ones which minimize the length of the link.
The idea is thus to compute the 16 sizes the link can get if it was attached to all combinations of its couple of nodes' sides; which is in our case the cartesian product of [[25, 0], [50, 25], [25, 50], [0, 25]]
with itself (where a node's width/height is 50 and each elements of this list is the coordinates of the middle of a node's side).
Note the change in the svg marker at the end of links. I had to translate it a bit within the link in order to have the head of the arrow coincide with the end of the link and thus avoid having the arrow within the node.
Also note that I switched to using d3v5 to avoid making one more d3v3 legacy example (the switch back to d3v3 shouldn't be that hard if necessary).
Upvotes: 3