I have a force graph where my nodes are actually a "g" element with an appended circle and an appended text element.
I am having trouble with my nodes as they do not move when dragged and are not positionned on the tip of the links.
If I change my nodes to simple circles only, then it works fine. But I am unable to get it to work with the "g" elements.
I have been suggested to use ".attr("transform", function (d) { return "translate(" d.x "," d.y ")";});" in my ticked() function since "g" element does not have cx and cy values (as those are specific to circles).
I think the problem comes from the way I create my drag() function.
Any help is more than welcome
ForceGraph(){
var links = [
{src:"John",target:"Aurora"},
{src:"John",target:"Mary"},
{src:"John",target:"Erik"},
{src:"John",target:"Susan"},
{src:"John",target:"Mel"},
]
var nodes = [
{id:"John"},
{id:"Aurora"},
{id:"Mary"},
{id:"Erik"},
{id:"Susan"},
{id:"Mel"},
]
var width = this.$refs.mapFrame.clientWidth // scale to parent container
var height = this.$refs.mapFrame.clientHeight // scale to parent container
// Compute values.
var nodeId = d => d.id // given d in nodes, returns a unique identifier (string)
const N = d3.map(nodes, nodeId);
const nodeTitle = (_, i) => N[i];
const T = d3.map(nodes, nodeTitle);
// Replace the input nodes and links with mutable objects for the simulation.
nodes = nodes.map(n => Object.assign({}, n));
links = links.map(l => ({
orig: l,
source: l.src,
target: l.target
}));
// Construct the forces.
const forceNode = d3.forceManyBody();
const forceLink = d3.forceLink(links).id(({index: i}) => N[i]);
forceNode.strength(-450);
forceLink.strength(1);
forceLink.distance(100)
const simulation = d3.forceSimulation(nodes)
.force(link, forceLink)
.force("charge", forceNode)
.force("x", d3.forceX())
.force("y", d3.forceY())
.on("tick", ticked);
const svg = d3.create("svg")
.attr("id", "svgId")
.attr("preserveAspectRatio", "xMidYMid meet")
.attr("viewBox", [-width/2,-height/2, width,height])
.classed("svg-content-responsive", true)
const link = svg.append("g")
.selectAll("line")
.data(links)
.join("line").attr("stroke", "white")
.attr("stroke-width", "5")
;
var node = svg
.selectAll(".circle-group")
.data(nodes)
.join(enter => {
node = enter.append("g")
.attr("class", "circle-group")
.call(drag(simulation));
node.append("circle")
.attr("class", "background")
.style("fill", "blue")
.attr("r", 30)
// .call(drag(simulation));
node.append("text")
.attr("class", "foreground")
.attr("dx", function(){return -20})
.style('font-size', 30 * 0.4 'px')
.text(({index: i}) => T[i])
// .call(drag(simulation))
node.attr("stroke", "grey");
})
node.call(drag(simulation));
function ticked() {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node
.attr("transform", function (d) { return "translate(" d.x "," d.y ")";});
// .attr("cx", d => d.x)
// .attr("cy", d => d.y);
}
function drag(simulation) {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
return Object.assign(svg.node() );
}
CodePudding user response:
Your node
variable should be empty, which means you aren't updating anything in the tick function. Here's an abbreviated and isolated example of your join where I call selection.size()
afterwards to see how many items are selected with node
:
var nodes = [1,2,3];
var svg = d3.select("body").append("svg");
var node = svg
.selectAll(".circle-group")
.data(nodes)
.join(enter => {
node = enter.append("g")
.attr("class", "circle-group")
node.append("circle")
.attr("class", "background")
.style("fill", "blue")
.attr("r", 30)
node.append("text")
node.attr("stroke", "grey");
})
console.log(node.size());
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
This is expected, the join returns the merged enter and update selections; however, since we are supplying a custom enter function we need to explicitly return the entered nodes (as we would also need to do if supplying a custom update function). From the docs:
The selections returned by the enter and update functions are merged and then returned by selection.join
If we add return node
to the end of your enter function we can see that our selection size is now 3.
var nodes = [1,2,3];
var svg = d3.select("body").append("svg");
var node = svg
.selectAll(".circle-group")
.data(nodes)
.join(enter => {
node = enter.append("g")
.attr("class", "circle-group")
node.append("circle")
.attr("class", "background")
.style("fill", "blue")
.attr("r", 30)
node.append("text")
node.attr("stroke", "grey");
return node;
})
console.log(node.size());
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
Now as node
includes the entered nodes, the tick function should work as intended.
An alternative would be to select the g
elements every tick with d3.select(".circle-group")
instead of using node
, however, this isn't very performant.