Q:
D3 force graph - force simulation to not let node connect to itself or adjacent node?
I am using D3.js force graph to make a force directed graph and have created a function that lets you move a node by clicking and dragging its node over the graph:
var click = function() {
nodes.append("rect")
.attr("x", d => x(d.x))
.attr("y", d => y(d.y))
.attr("height", d => height - y(d.y))
.attr("width", d => width - x(d.x))
.style("fill", "white")
.on("click", d => {d.fixed = !d.fixed;});
}
Which I then call with this line:
d3.select(window).on("mousedown", click);
However the nodes can connect to themselves and other nodes around them and I am not sure how to prevent this. What I would like to happen is to:
only allow a node to be connected to another node
only allow a node to move if there is not already a node connected to the end of the node that the user wants to move
if there is already a node connected to that node, then when the user tries to move the node, an error message would show up telling the user the current node is fixed and they must disconnect any nodes connected to that node.
A:
It looks like you may be confusing the issue of fixed nodes and unconnected nodes. The first two requirements above do not have to do with whether a node is fixed or not, and whether it is connected to another node or not. Instead, it sounds like your users should be able to only move a node if it is not already connected to any other nodes. This would probably be accomplished by checking the connectedNodes array for the target node.
To accomplish the third requirement, you could use a selection.each() function and check the nodes array for the connected nodes. For example:
d3.select(window).on("mousedown", function(d) {
if(d3.event.defaultPrevented) return;
var t = d3.event.target;
if (t.tagName === "circle") {
// do whatever else you do here
// ...
// get the node to move, d
var d = d3.event.selection;
// get the current list of nodes
var n = d3.select(this).selectAll("circle").data();
// go through each node in the list, starting at the first
d3.select(n).each(function(d, i) {
// check for connected nodes
if (d3.select(this).selectAll("line").size() > 0) {
// if a node is already connected, don't allow
// the connection
return;
}
// allow the connection
d3.select(this).append("line")
.attr("id", function(n) {
// return a unique ID for this line
return "line" + n.id;
})
.attr("x1", function(n) {
// find the index of the node to connect to
return d.x;
})
.attr("y1", function(n) {
// find the index of the node to connect to
return d.y;
})
.attr("x2", function(n) {
// find the index of the node to connect to
return d.x;
})
.attr("y2", function(n) {
// find the index of the node to connect to
return d.y;
})
.style("stroke", "steelblue")
.style("stroke-width", "2px")
.style("stroke-linejoin", "round")
.style("stroke-linecap", "round");
});
}
});
});
Here's an example:
var data = [{
"id": "node1",
"x": "250",
"y": "300"
}, {
"id": "node2",
"x": "100",
"y": "100"
}, {
"id": "node3",
"x": "350",
"y": "350"
}];
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 500);
var node = svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("class", "node")
.attr("r", 20)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.on("mouseover", function(d) {
d3.select(this).style("stroke", "red");
})
.on("mouseout", function(d) {
d3.select(this).style("stroke", "gray");
})
.on("click", function(d) {
d3.select(this).append("line")
.attr("x1", d => d.x)
.attr("y1", d => d.y)
.attr("x2", d => d.x)
.attr("y2", d => d.y)
.style("stroke", "red");
});
node.on("dblclick", function(d) {
d3.select(this).append("circle")
.attr("r", "40")
.attr("class", "nodelink");
});
d3.select(window).on("mousedown", function(d) {
if(d3.event.defaultPrevented) return;
var t = d3.event.target;
if (t.tagName === "circle") {
// do whatever else you do here
// var x = d3.event.selection[0];
var x = d.x;
if (x == d.x && !d.fixed) {
alert("This node is already connected to a node and cannot be moved.");
d3.event.stopPropagation();
return;
}
// get the node to move, d
var d = d3.event.selection;
// get the current list of nodes
var n = d3.select(this).selectAll("circle").data();
// go through each node in the list, starting at the first
d3.select(n).each(function(d, i) {
// check for connected nodes
if (d3.select(this).selectAll("line").size() > 0) {
// if a node is already connected, don't allow
// the connection
return;
}
// allow the connection
d3.select(this).append("line")
.attr("id", function(n) {
// return a unique ID for this line
return "line" + n.id;
})
.attr("x1", function(n) {
// find the index of the node to connect to
return d.x;
})
.attr("y1", function(n) {
// find the index of the node to connect to
return d.y;
})
.attr("x2", function(n) {
// find the index of the node to connect to
return d.x;
})
.attr("y2", function(n) {
// find the index of the node to connect to
return d.y;
})
.style("stroke", "steelblue")
.style("stroke-width", "2px")
.style("stroke-linejoin", "round")
.style("stroke-linecap", "round");
});
}
});