'D3 force directed graph, apply force to a "g" element
I am having trouble with my force directed graph. I had to make some changed to my nodes for design purposes and ever since, my forces stoped working. Each node is now a "g" element with two "circle" elements inside. One being the background of the node and the other being the partially transparent foreground.
Unlike before where I would apply ".call(drag(simulation))" to my node that used to be a "circle", I now need to apply it the the "g" element.
As seen on the screenshot, the nodes are not where they are supposed to be. They are detached from their respective links, and are all in the center of the map , on the top of each other.
Any clue on what I am doing wrong?
ForceGraph(
nodes, // an iterable of node objects (typically [{id}, …])
links // an iterable of link objects (typically [{src, target}, …])
){
var nodeId = d => d.id // given d in nodes, returns a unique identifier (string)
const nodeStrength = -450 // -1750
const linkDistance = 100
const linkStrokeOpacity = 1 // link stroke opacity
const linkStrokeWidth = 3 // given d in links, returns a stroke width in pixels
const linkStrokeLinecap = "round" // link stroke linecap
const linkStrength =1
var width = this.$refs.mapFrame.clientWidth // scale to parent container
var height = this.$refs.mapFrame.clientHeight // scale to parent container
const N = d3.map(nodes, nodeId);
// 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,
//Object.assign({}, 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(nodeStrength);
forceLink.strength(linkStrength);
forceLink.distance(linkDistance)
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 defs = svg.append('svg:defs');
defs.selectAll("pattern")
.data(nodes)
.join(
enter => {
// For every new <pattern>, set the constants and append an <image> tag
const patterns = enter
.append("pattern")
.attr("preserveAspectRatio", "none")
.attr("viewBox", [0,0, 100,100])
.attr("width", 1)
.attr("height", 1);
patterns
.append("image")
.attr("width", 80)
.attr("height", 80)
.attr("x", 10)
.attr("y", 10);
return patterns;
}
)
// For every <pattern>, set it to point to the correct
// URL and have the correct (company) ID
.attr("id", d => d.id)
.select("image")
.datum(d => {
return d;
})
.attr("xlink:href", d => {
return d.image
})
const link = svg.append("g")
.attr("stroke-opacity", linkStrokeOpacity)
.attr("stroke-width", linkStrokeWidth)
.attr("stroke-linecap", linkStrokeLinecap)
.selectAll("line")
.data(links)
.join("line")
;
link.attr("stroke", "white")
var node = svg
.selectAll(".circle-group")
.data(nodes)
.join(enter => {
node = enter.append("g")
.attr("class", "circle-group");
node.append("circle")
.attr("class", "background") // classes aren't necessary here, but they can help with selections/styling
.style("fill", "red")
.attr("r", 30);
node.append("circle")
.attr("class", "foreground") // classes aren't necessary here, but they can help with selections/styling
.style("fill", d => `url(#${d.id})`)
.attr("r", 30)
})
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
//.transform("translate", d => "translate("+[d.x,d.y]+")"); // triggers error
.attr("transform", d => "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() );
}//forcegraph
EDIT 1
I updated the ticked() function with what was suggested but ".transform("translate", d => "translate("+[d.x,d.y]+")");" triggered the following error :
Mapping.vue?d90b:417 Uncaught TypeError: node.transform is not a function
at Object.ticked (Mapping.vue?d90b:417:1)
at Dispatch.call (dispatch.js?c68f:57:1)
at step (simulation.js?5481:32:1)
at timerFlush (timer.js?74f4:61:1)
at wake (timer.js?74f4:71:1)
So I changed it for ".attr("transform", d => "translate("+[d.x,d.y]+")");"
I don't get any error anymore but my nodes are still all in the center of the map as per the initial screenshot. I am not quite sure what I am doing wrong. Perhaps I need to call ".call(drag(simulation))" on each of the two circles instead of calling it on node?
Solution 1:[1]
g elements don't have cx or cy properties. Those are specific to circle elements (and ellipses). This is why your positioning does not work. However, both circle and g can use a transform for positioning. Instead of:
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
You can use:
node
.transform("translate", d => "translate("+[d.x,d.y]+")");
In regards to your question title, d3 does not apply a force to the elements but rather the data itself. The forces continue to work regardless of whether you render the changes - as seen in your case by the links which move as they should.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Andrew Reid |

