I'm trying to construct a directed force graph in d3.js that has a click listener on it that changes the the underlying data and redraws the graph. I belive I'm following Mr. Bostock's update pattern, but the issue that I'm having is that when I run the update triggered by the click listener the nodes disappear off the bottom of the screen leaving the links and labels behind.
This update seems to run, updates the existing nodes (turns them green in this case) then ignores the "enter" and "exit" sections (which is the desired behaviour) then hits the tick() function which send the nodes south.
I can get this working by removing the "g" tag on the node and thus decoupling the labels and the node, which is obviously not desirable.
I can't help feeling I'm missing something obvious! Or perhaps I should be tacking this a different way?
Here's the code:
var width = 960,
height = 500,
links,
nodes,
root;
var force = d3.layout.force()
.size([width, height])
.charge(-200)
.linkDistance(50)
.on("tick", tick);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
d3.json("test.json", function(json) {
root = json;
update();
});
function update() {
nodes = root.nodes
links = root.links
// Restart the force layout.
force
.nodes(nodes)
.links(links)
.start();
svg.append("svg:defs").append("marker")
.attr("id", "end")
.attr("refX", 15)
.attr("refY", 2)
.attr("markerWidth", 6)
.attr("markerHeight", 4)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M 0,0 V 4 L8,2 Z");
// Update the links…
//link = link.data(links, function(d) { return d.target.name; });
link = link.data(links)
// Exit any old links.
link.exit().remove();
// Enter any new links.
link.enter().insert("svg:line", ".node")
.attr("class", "link")
.attr("marker-end", "url(#end)")
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
// Update the nodes…
node = svg.selectAll("g").select(".node").data(nodes, function(d) { return d.name; });
node.style("fill", "green")
// Exit any old nodes.
node.exit().remove();
// Enter any new nodes.
node.enter().append("g")
.append("svg:circle")
.attr("class", "node")
.attr("id", function(d) {return "node" + d.index; })
.attr("r", 12)
.style("fill", "#BBB")
.on("click", click)
.call(force.drag);
node.append("svg:text")
.attr("dx", 16)
.attr("dy", ".15em")
.attr("class", "nodelabel")
.text(function(d) { return d.name });
}
function tick() {
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
}
function click(d) {
if (!d3.event.defaultPrevented) {
// DO ANYTHING
update()
}
}
and the contents of test.json is:
{
"nodes": [
{"name" : "John"},
{"name" : "Alison"},
{"name" : "Phil"},
{"name" : "Jim"},
{"name" : "Jane"},
{"name" : "Mary"},
{"name" : "Joe"}
],
"links": [
{"source": 1, "target": 0},
{"source": 2, "target": 0},
{"source": 3, "target": 0},
{"source": 4, "target": 0},
{"source": 5, "target": 1},
{"source": 6, "target": 1}
]
}
OK, so I figured out the problem. When I was selecting the nodes to update I was selecting the nodes (that is, the elements that had the class "node") and they were being updated:
node = svg.selectAll("g").select(".node").data(nodes, function(d) { return d.name; });
Then in the tick function I was updating those nodes:
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
so that was quite right, however, the nodes were encapsulated in the "g" tag along with the text label, but the tick() function was only acting on the node. The fix was to force the transform attribute in tick() to update the whole group rather than just the node:
svg.selectAll("g").attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
All works now!
Related
I'm working on a map (found here), that is using the svg viewbox attribute to scale with the size of the client.
Unfortunately the project I'm using, d3.geoAlbersUsa() does not seem to scale the tooltip correctly with the rest of the SVG. As in, it suddenly places the tooltip in the same spot it would be if the client width had been the original 960x500 specs.
Here's the tooltip code:
d3.tsv("CitiesTraveledTo.tsv",cityVisited, function(data) {
var cities = svg.selectAll(".city")
.data(data)
.enter()
.append("g")
.classed("city",true);
cities.append("line")
.attr("x1", function(d) {
return projection([d.Longitude, d.Latitude])[0];
})
.attr("x2", function(d) {
return projection([d.Longitude, d.Latitude])[0];
})
.attr("y1", function(d) {
return projection([d.Longitude, d.Latitude])[1]-pinLength;
})
.attr("y2", function(d) {
return (projection([d.Longitude, d.Latitude])[1]);
})
.attr("stroke-width",function(d) {
return 2;
})
.attr("stroke",function(d) {
return "grey";
});
cities.append("circle")
.attr("cx", function(d) {
return projection([d.Longitude, d.Latitude])[0];
})
.attr("cy", function(d) {
return projection([d.Longitude, d.Latitude])[1]-pinLength;
})
.attr("r", function(d) {
return 3;
})
.style("fill", function(d) {
if (d.Reason === "Work") {
return "rgb(214, 69, 65)";
}
else if (d.Reason === "Fun") {
return "rgb(245, 215, 110)";
}
else {
return "rgb(214, 69, 65)";
}
})
.style("opacity", 1.0)
// Modification of custom tooltip code provided by Malcolm Maclean, "D3 Tips and Tricks"
// http://www.d3noob.org/2013/01/adding-tooltips-to-d3js-graph.html
.on("mouseover", function(d) {
div.transition()
.duration(200)
.style("opacity", .9);
div.text(d.City + ", " + d.State)
.style("left", function() {
var centerCircle = (projection([d.Longitude, d.Latitude])[0]);
return (centerCircle-26) + "px";
})
.style("top", function() {
var centerCircle = projection([d.Longitude, d.Latitude])[1];
var circleRadius = 3;
return ( centerCircle - circleRadius - 33-pinLength) + "px";
});
div.append("div").attr("class","arrow-down");
})
// fade out tooltip on mouse out
.on("mouseout", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
});
I thought that the scaling should just happen automatically for the tooltip as well. Wrong. I then tried to reset the height and width passed to the projection and that didn't work. What's the best way to get the element bound to a data "d" node?
I ask because it will likely be easier to say "for this node, get me this element, give me the bound html element", so that I can set the position of the tooltip relative to the new position of the bound element.
I have created a cluster tree layout and I want to add custom node styles to selected nodes. To be more precise, I'm adding treemap as node.
I managed to add those, but they are not positioned in the center of node.
I have tried all sort of x,y attributes and translations but I quess I don't get svg that much yet.
Part of code where I add the node is here (for JSfiddle see below):
nodeEnter.each(function(d) {
if (d.status == "D") {
var treemap = d3.layout.treemap()
.size([20, 20])
.sticky(true)
.value(function(d) {
return 1;
});
var cell = d3.select(this)
.selectAll("g")
.data(function(d) {
return treemap.nodes(d.annotations);
})
.enter().append("g")
.attr("class", "cell")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
cell.append("rect")
.attr("width", function(d) {
return d.dx;
})
.attr("height", function(d) {
return d.dy;
})
.style("fill", function(d) {
return d.children ? null : hex2rgb(color(d.parent.name));
});
}
})
Any help would be appreciated
Here is my JSfiddle.
L.
Assuming you wanted the lines to connect to the middle of the appended rect. I just added a third .attr to your JSfiddle
cell.append("rect")
.attr("width", function(d) {
return d.dx;
})
.attr("height", function(d) {
return d.dy;
})
.attr("transform","translate(0,-10)")
.style("fill", function(d) {
return d.children ? null : hex2rgb(color(d.parent.name));
});
I can use d3 to draw a pie chart or a graph, I can even draw a pie chart within each node of a graph as shown here.
Is it possible to create a reusable function that generate the pie chart and attach its result to the each node? That way the pie chart code could be reused, for instance in a gallery of charts.
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node");
// draw pie chart
node.selectAll("path")
.data(function(d, i) {return pie(d.proportions); })
.enter()
.append("svg:path")
.attr("d", arc)
.attr("fill", function(d, i) { return color(d.data.group); });;
From the above code, I tried the following code which doesn't work
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.call(drawPie(function(d) { return d.proportions; }));
function drawPie(d) {
this.selectAll("path")
.data(function(d, i) {return pie(d.proportions); })
.enter()
.append("svg:path")
.attr("d", arc)
.attr("fill", function(d, i) { return color(d.data.group); });;
}
Your original idea is much closer than the one recommended in the other answer, you just need to understand how selection.call works.
This is not tested but the general principle is like...
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.call(drawPie);
function drawPie(selection) {
this.selectAll("path")
.data(function(d, i) {return pie(d.proportions); })
.enter()
.append("svg:path")
.attr("d", arc)
.attr("fill", function(d, i) { return color(d.data.group); });;
}
In reference to your first attempt, if you stop and think about this line...
.call(drawPie(function(d) { return d.proportions; }));
...it's actually trying to call null because that's what is returned by drawPie. It's equivalent to...
.call(null);
Based on the recommendations, here is the modified code which still require some improvements. An error message report that "row 93 undefined is not an object evaluating d.proportions"
graph = { "nodes":[
{"proportions": [{"group": 1, "value": 1},
{"group": 2, "value": 2},
{"group": 3, "value": 3}]},
{"proportions": [{"group": 1, "value": 2},
{"group": 2, "value": 2},
{"group": 3, "value": 2}]}],
"links":[{"source": 0, "target": 1, "length": 500, "width": 1}]
}
var width = 960,
height = 500,
radius = 25,
color = d3.scale.category10();
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d.value; });
var arc = d3.svg.arc()
.outerRadius(radius)
.innerRadius(10);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.charge(-120)
.linkDistance(4 * radius)
.size([width, height]);
force.nodes(graph.nodes)
.links(graph.links)
.start();
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter()
.append(function(d) {return createPie(d);}) // .append(createPie) --- shorter version
.attr("class", "node");
// node.selectAll("path")
// .data(function(d, i) {return pie(d.proportions); })
// .enter()
// .append("svg:path")
// .attr("d", arc)
// .attr("fill", function(d, i) { return color(d.data.group); });;
force.on("tick", function() {
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"});
});
function createPie(d) {
console.log(d);
var pie = d3.select(document.createElement('svg:g'));
pie.selectAll('path')
.data(function(d, i) {return pie(d.proportions); })
.enter()
.append("svg:path")
.attr("d", arc)
.attr("fill", function(d, i) { return color(d.data.group); });
return pie.node();
}
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter()
.append(function(d){return createPie(d);}) // .append(createPie) --- shorter version
.attr("class", "node");
function createPie(data) {
var pie = d3.select(document.createElement('svg:g'));
pie.selectAll('path')
...;
return pie.node();
}
UPDATE:
function createPie(d) {
console.log(d);
var p = d3.select(document.createElement('svg:g'));
p.selectAll('path')
.data(pie(d.proportions))
.enter()
.append("svg:path")
.attr("d", arc)
.attr("fill", function(d, i) { return color(d.data.group); });
return p.node();
}
the previous variable pie needs to be refactored because it overwrites the one in parent scope.
and the data call needs to be fixed as well
I've made a force directed graph with d3.js plugin, and I wanna color the nodes and the labels with the different color according to group which they belong.
I've added scale for color:
var color = d3.scale.category20();
and to node variable I've added:
.style("fill", function(d) { return color(d.group); })
but all nodes are in the same color..
Here is my current situation: http://jsfiddle.net/WBkw9/
full script:
var links = [
{source: "John", target: "Mike", group: "5"},
{source: "John", target: "Janice", group: "5"},
{source: "John", target: "Caleb", group: "5"},
{source: "John", target: "Anna", group: "4"},
{source: "John", target: "Tommy", group: "3"},
{source: "John", target: "Jack", group: "2"},
{source: "John", target: "Vilma", group: "1"},
];
var nodes = {};
// Compute the distinct nodes from the links.
links.forEach(function(link) {
link.source = nodes[link.source] || (nodes[link.source] = {name: link.source});
link.target = nodes[link.target] || (nodes[link.target] = {name: link.target});
});
var color = d3.scale.category20();
var width = 960,
height = 500;
var force = d3.layout.force()
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(60)
.charge(-300)
.on("tick", tick)
.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var link = svg.selectAll(".link")
.data(force.links())
.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
.style("fill", function(d) { return color(d.group); })
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.call(force.drag);
node.append("circle")
.attr("r", 8);
node.append("text")
.attr("x", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
function tick() {
link
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
}
function mouseover() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 16);
}
function mouseout() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 8);
}
what am I missing for different color on each group?
Your problem is that group is not defined for your data. As a result, all of your nodes are colored for group 'undefined'. Your circles are defined for the data in force.nodes(), which have the attributes index name px py weight x and y. group is only defined for the links, which never have color applied to them.
As it currently stands, there also isn't a clear way to determine what color a node should be. What happens if more than one link connects to a node, and these links are in different groups?
Here is my code (based on http://bl.ocks.org/mbostock/4062045). It's working perfectly.
You can see how it looks like here : http://jsfiddle.net/Rom2BE/H2PkT/
Each group has a different color.
**index.html**
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.link {
stroke: #999;
stroke-opacity: .6;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var width = 650,
height = 700;
var color = d3.scale.category10();
var force = d3.layout.force()
.charge(-120)
.linkDistance(30)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
d3.json("data.json", function(error, graph) {
force
.nodes(graph.nodes)
.links(graph.links)
.start();
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
// You define here your nodes and the color will be d.group
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 5)
.style("fill", function(d) { return color(d.group); })
.call(force.drag);
//Display node name when mouse on a node
node.append("title")
.text(function(d) { return d.name; });
//Where and how nodes are displayed
force.on("tick", function() {
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
});
//Legend
var legend = svg.selectAll(".legend")
.data(color.domain())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
});
</script>
**data.json**
{"nodes":[
{"name":"Vertex 5","group":"Virtuals-MacBook-Pro-36095"},{"name":"Vertex 9","group":"Virtuals-MacBook-Pro-36095"},{"name":"Vertex 15","group":"Virtuals-MacBook-Pro-3-53688"},{"name":"Vertex 20","group":"Virtuals-MacBook-Pro-36095"},{"name":"Vertex 26","group":"Virtuals-MacBook-Pro-4-40842"},{"name":"Vertex 29","group":"Virtuals-MacBook-Pro-36095"},{"name":"Vertex 33","group":"Virtuals-MacBook-Pro-36095"},{"name":"Vertex 37","group":"Virtuals-MacBook-Pro-36095"},{"name":"Vertex 49","group":"Virtuals-MacBook-Pro-3-53688"},{"name":"Vertex 52","group":"Virtuals-MacBook-Pro-4-40842"},{"name":"Vertex 53","group":"Virtuals-MacBook-Pro-4-40842"},{"name":"Vertex 58","group":"Virtuals-MacBook-Pro-36095"},{"name":"Vertex 59","group":"Virtuals-MacBook-Pro-4-40842"},{"name":"Vertex 65","group":"Virtuals-MacBook-Pro-4-40842"},{"name":"Vertex 73","group":"Virtuals-MacBook-Pro-4-40842"},{"name":"Vertex 74","group":"Virtuals-MacBook-Pro-36095"},{"name":"Vertex 80","group":"Virtuals-MacBook-Pro-36095"},{"name":"Vertex 84","group":"Virtuals-MacBook-Pro-4-40842"},{"name":"Vertex 87","group":"Virtuals-MacBook-Pro-4-40842"},{"name":"Vertex 99","group":"Virtuals-MacBook-Pro-4-40842"}
],
"links":[
{"source":5,"value":1,"target":11},{"source":5,"value":1,"target":12},{"source":10,"value":1,"target":12},{"source":11,"value":1,"target":5},{"source":11,"value":1,"target":12},{"source":11,"value":1,"target":14},{"source":12,"value":1,"target":5},{"source":12,"value":1,"target":10},{"source":12,"value":1,"target":11},{"source":14,"value":1,"target":11},{"source":16,"value":1,"target":19},{"source":18,"value":1,"target":19},{"source":19,"value":1,"target":16},{"source":19,"value":1,"target":18}
]}
Your group info is only available in the links object, like #ckersch already pointed out. You would need to add the group info to you nodes object too. For this example that can be done by changing line 16 into:
link.target = nodes[link.target] || (nodes[link.target] = {name: link.target, group: link.group});
But for more complex data, with more than one source, all sources would have the same colour (or would that be OK?).
I made that change in this Fiddle: http://jsfiddle.net/WBkw9/19/.
I think you need to change the style attribute of the circle, not the g element.
node.append("circle").style("fill", function(d) { return color(d.group); })
Edit: The group property in the data must also be changed integers, or cast later.
Did you ever solve this? if not a possible solution is here: http://jsfiddle.net/adeaver/F2fbu/1/
Each group/node is differently colored along with the corresponding text by adding:
.style("fill", function(d) { return color(d.group); })
to the text append and group: link.group to the function that computes the nodes from the links
I am new to D3 and having trouble setting the bounds for my force directed layout. I have managed to piece together (from examples) what I would like, but I need the graph to be contained. In the tick function, a transform/translate will display my graph correctly, but when i use cx and cy with Math.max/min (See commented code), the nodes are pinned to the
top left corner while the lines are contained properly.
Here is what I have below... what am I doing wrong??
var w=960, h=500, r=8, z = d3.scale.category20();
var color = d3.scale.category20();
var force = d3.layout.force()
.linkDistance( function(d) { return (d.value*180) } )
.linkStrength( function(d) { return (1/(1+d.value)) } )
.charge(-1000)
//.gravity(.08)
.size([w, h]);
var vis = d3.select("#chart").append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(" + w / 4 + "," + h / 3 + ")");
vis.append("svg:rect")
.attr("width", w)
.attr("height", h)
.style("stroke", "#000");
d3.json("miserables.json", function(json) {
var link = vis.selectAll("line.link")
.data(json.links);
link.enter().append("svg:line")
.attr("class", "link")
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.source.x; })
.attr("y2", function(d) { return d.source.y; })
.style("stroke-width", function(d) { return (1/(1+d.value))*5 });
var node = vis.selectAll("g.node")
.data(json.nodes);
var nodeEnter = node.enter().append("svg:g")
.attr("class", "node")
.on("mouseover", fade(.1))
.on("mouseout", fade(1))
.call(force.drag);
nodeEnter.append("svg:circle")
.attr("r", r)
.style("fill", function(d) { return z(d.group); })
.style("stroke", function(d) { return
d3.rgb(z(d.group)).darker(); });
nodeEnter.append("svg:text")
.attr("text-anchor", "middle")
.attr("dy", ".35em")
.text(function(d) { return d.name; });
force
.nodes(json.nodes)
.links(json.links)
.on("tick", tick)
.start();
function tick() {
// This works
node.attr("transform", function(d) { return "translate(" + d.x + ","
+ d.y + ")"; });
// This contains the lines within the boundary, but the nodes are
stuck in the top left corner
//node.attr("cx", function(d) { return d.x = Math.max(r, Math.min(w
- r, d.x)); })
// .attr("cy", function(d) { return d.y = Math.max(r, Math.min(h -
r, d.y)); });
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
}
var linkedByIndex = {};
json.links.forEach(function(d) {
linkedByIndex[d.source.index + "," + d.target.index] = 1;
});
function isConnected(a, b) {
return linkedByIndex[a.index + "," + b.index] ||
linkedByIndex[b.index + "," + a.index] || a.index == b.index;
}
function fade(opacity) {
return function(d) {
node.style("stroke-opacity", function(o) {
thisOpacity = isConnected(d, o) ? 1 : opacity;
this.setAttribute('fill-opacity', thisOpacity);
return thisOpacity;
});
link.style("stroke-opacity", opacity).style("stroke-opacity",
function(o) {
return o.source === d || o.target === d ? 1 : opacity;
});
};
}
});
There's a bounding box example in my talk on force layouts. The position Verlet integration allows you to define geometric constraints (such as bounding boxes and collision detection) inside the "tick" event listener; simply move the nodes to comply with the constraint and the simulation will adapt accordingly.
That said, gravity is definitely a more flexible way to deal with this problem, since it allows users to drag the graph outside the bounding box temporarily and then the graph will recover. Depend on the size of the graph and the size of the displayed area, you should experiment with different relative strengths of gravity and charge (repulsion) to get your graph to fit.
A custom force is a possible solution too. I like this approch more since not only the displayed nodes are repositioned but the whole simulation works with the bounding force.
let simulation = d3.forceSimulation(nodes)
...
.force("bounds", boxingForce);
// Custom force to put all nodes in a box
function boxingForce() {
const radius = 500;
for (let node of nodes) {
// Of the positions exceed the box, set them to the boundary position.
// You may want to include your nodes width to not overlap with the box.
node.x = Math.max(-radius, Math.min(radius, node.x));
node.y = Math.max(-radius, Math.min(radius, node.y));
}
}
The commented code works on node which is, from your definition, a svg g(rouping) element and does not operate the cx/cy attributes. Select the circle element inside node to make these attributes come alive:
node.select("circle") // select the circle element in that node
.attr("cx", function(d) { return d.x = Math.max(r, Math.min(w - r, d.x)); })
.attr("cy", function(d) { return d.y = Math.max(r, Math.min(h - r, d.y)); });