Force directed graph's gravity that forms a rectangle - d3.js

I would like to make so that my nodes fit a rectangle shaped space instead of a circle (default gravity).
I have read a similar topic here but was unable to make it work with my code.
// Construct the forces.
const forceNode = d3.forceManyBody();
const forceLink = d3.forceLink(links).id(({index: i}) => N[i]);
forceNode.strength(-150);
forceLink.strength(1);
forceLink.distance(50)
const simulation = d3.forceSimulation(nodes)
.force(link, forceLink)
.force("charge", d3.forceManyBody().strength(-150))
.force("collide", d3.forceCollide(10).strength(10).iterations(1))
.force('x', d3.forceX(width/4).strength(1))
.force('y', d3.forceY(height/4).strength(10))
.on("tick", ticked);
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 + ")";});
function drag(simulation) {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart(); //comment to remove sim
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); //comment to remove sim
event.subject.fx = null;
event.subject.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
Then I apply ".call(drag(simulation));" to my node element
When the map loads, the nodes and links are all cramed into a vertical line, then after a few seconds and a few lag spikes they end up forming a sort of oval. The map remains messy and unreadable.
Any clue what I am doing wrong?

Related

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?
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.

How to center nodes without using centering force?

I have a force layout graph where user adds nodes dynamically. How can i center all my nodes with a bit distance between, and make them move independently and not around the center.
I have tried removing the d3.forceCenter(width / 2, height / 2) which makes nodes move independently but then it possitions all nodes at (0, 0).
simulation = d3.forceSimulation()
.force('charge', d3.forceManyBody().strength(0))
.force('center', d3.forceCenter(width / 2, height / 2));
I want all the nodes to be centered and move independently.
EDIT:
I tried setting cx and cy values but that did not work either.
const nodeEnter = nodeElements
.enter()
.append('circle')
.attr('r', 20)
.attr('fill', 'orange')
.attr('cx', (d, i) => {
return (width / 2) + i * 10;
})
.attr('cy', (d, i) => {
return (height / 2) + i * 10;
})
.call(dragDrop(simulation))
.on('click', ({ id }) => handleClick(id));
Given what you said in your comment...
If i move 1 node then all other nodes move relatively in order to keep the center of mass at the same spot.
... you already know that forceCenter is the wrong tool for the task, since it will keep the centre of mass.
Therefore, just replace it for forceX and forceY:
const simulation = d3.forceSimulation()
.force('centerX', d3.forceX(width / 2))
.force('centerY', d3.forceY(height / 2));
Since you didn't provide enough code here is a general demo:
svg {
background-color: wheat;
}
<svg width="400" height="300"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
const svg = d3.select('svg');
const width = svg.attr('width');
const height = svg.attr('height');
const data = d3.range(50).map(() => ({}));
const node = svg.selectAll()
.data(data)
.enter()
.append('circle')
.attr('r', 10)
.attr('fill', 'teal')
.attr('stroke', 'black')
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended));
const simulation = d3.forceSimulation()
.force('charge', d3.forceManyBody().strength(-15))
.force('centerX', d3.forceX(width / 2))
.force('centerY', d3.forceY(height / 2));
simulation
.nodes(data)
.on('tick', ticked);
function ticked() {
node.attr('cx', d => d.x)
.attr('cy', d => d.y);
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
</script>

On drag, force is applied to node before mouse click is released

I've been implementing a force layout in d3js and I can zoom and drag the layout / nodes.
When I'm dragging a node, the behavior is fine, except that when I stopped moving, the node instantly starts moving back to it's original position, even though I haven't released the mouse click and so the event should still be considered in progress.
I tried to fix the behavior by setting d.fx and d.fy to current values, which doesn't solve anything.
Here's the code:
const WIDTH = 1600;
const HEIGHT = 900;
const V_MARGIN = 20;
const H_MARGIN = 50;
const ALPHA_DECAY = 0.03;
const VELOCITY_DECAY = 0.6;
const LINK_DISTANCE = Math.min(WIDTH, HEIGHT) / 10;
const CHARGE_FORCE = -Math.min(WIDTH, HEIGHT) / 3;
const ITERATIONS = 16;
const CIRCLE_WIDTH = 3;
const ON_HOVER_OPACITY = 0.1;
const c10 = d3.scaleOrdinal(d3.schemeCategory10);
const SVG = d3.select('body')
.append('svg')
.attr('width', WIDTH)
.attr('height', HEIGHT)
;
const g = SVG.append('g')
.attr('class', 'everything')
;
d3.json('got_social_graph.json')
.then(data => {
const nodes = data.nodes;
const links = data.links;
//Create force layout
const force = d3.forceSimulation()
.nodes(nodes)
.alphaDecay(ALPHA_DECAY)
.velocityDecay(VELOCITY_DECAY)
.force('links', d3.forceLink(links).distance(LINK_DISTANCE))
.force("collide",d3.forceCollide(d => d.influence > 15 ? d.influence : 15).iterations(ITERATIONS))
.force('charge_force', d3.forceManyBody().strength(CHARGE_FORCE))
.force('center_force', d3.forceCenter((WIDTH - H_MARGIN) / 2, (HEIGHT - V_MARGIN) / 2))
.force("y", d3.forceY(0))
.force("x", d3.forceX(0))
;
//Create links
const link = g.append('g')
.attr('class', 'link')
.selectAll('line')
.data(links)
.enter()
.append('line')
.attr('stroke-width', d => d.weight / 10)
;
//Create nodes elements
const node = g.append('g')
.attr('class', 'node')
.selectAll('circle')
.data(nodes)
.enter()
.append('g')
;
//Append circles to nodes
const circle = node.append('circle')
.attr('r', d => d.influence > 10 ? d.influence : 10)
.attr('stroke-width', CIRCLE_WIDTH)
.attr('stroke', d => c10(d.zone * 10))
.attr('fill', 'black')
;
/*const label = node.append('text')
.attr('x', 12)
.attr('y', '0.25em')
.attr('font-size', d => d.influence * 1.5 > 9 ? d.influence * 1.5 : 9)
.text(d => d.character)
;*/
//Refresh force layout data
force.on('tick', () => {
node.attr('cx', d => d.x = Math.max(H_MARGIN, Math.min(WIDTH - H_MARGIN, d.x - H_MARGIN)))
.attr('cy', d => d.y = Math.max(V_MARGIN, Math.min(HEIGHT - V_MARGIN, d.y - V_MARGIN)))
.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
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)
;
});
//Handle mouse events
circle
.on('click', (d, k, n) => {
d3.select(n[k])
})
.on('mouseover', (d, k, n) => {
})
.on('mouseout', (d, k, n) => {
circle
.attr('opacity', 1)
})
;
//Handle translation events
node.call(d3.drag()
.on('start', (d, n, k) => {
if (!d3.event.active) force.alphaTarget(0.3).restart();
})
.on('drag', (d, n, k) => {
d3.select(n[k])
.attr('cx', d.x = d3.event.x)
.attr('cy', d.y = d3.event.y)
})
.on('end', (d, n, k) => {
if (!d3.event.active) force.alphaTarget(0);
}))
;
//Handle zoom events
g.call(d3.zoom()
.scaleExtent([0.8, 2.5])
.on('start', () => {
if (!d3.event.active) force.alphaTarget(0.3).restart();
d3.event.sourceEvent.stopPropagation();
})
.on('zoom', () => {
if (!d3.event.active) force.alphaTarget(0.3).restart();
g.attr('transform', d3.event.transform)
})
.on('end', () => {
if (!d3.event.active) force.alphaTarget(0);
})
);
});
I want the node to stay over my mouse cursor until the click is released. The node doesn't need to be sticky.

How to create a custom element in d3

The code is based on Denise's one on bl.ocks. How can I add to the existing code text and an icon to a node? I have almost got it by appending the needed elements but the problem is that circles appear at (0, 0) coordinate.
A picture is worth a thousand words,
and my target is having inside the nodes and where the nodes need to be, a text in the middle and an icon,
This is my current code, that works perfectly as long as the commented part is not uncommented (that's what I have tried to do)
let translateVar = [0,0];
let scaleVar = 1;
let radius = 50;
function create_pan_zoomable_svg(html_element, width, height) {
let svg = d3.select("body")
.append("svg")
.attr("width", "100%")
.attr("height", "100%")
.style("background-color", "#eeeeee")
.call(_zoom).on("dblclick.zoom", null)
.append("g");
d3.select("#zoom_in").on('click', function() { _zoom.scaleBy(svg, 2)});
d3.select("#zoom_out").on('click', function() { _zoom.scaleBy(svg, 0.5)});
create_marker(svg);
initialize_link_node(svg);
return svg;
}
var _zoom = d3.zoom()
.on("zoom", function() {
translateVar[0] = d3.event.transform.x;
translateVar[1] = d3.event.transform.y;
scaleVar = d3.event.transform.k;
svg.attr('transform', 'translate(' + translateVar[0] + ',' + translateVar[1] + ') scale(' + scaleVar + ')');
});
function create_marker(svg) {
let defs = svg.append("defs");
defs.append("marker")
.attr("id", "arrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 40)
.attr("refY", 0)
.attr("markerWidth", 8)
.attr("markerHeight", 8)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
}
function getScreenInfo() {
return {
width : ($(window).width()-translateVar[0])/scaleVar,
height : ($(window).height()-translateVar[1])/scaleVar,
centerx : (($(window).width()-translateVar[0])/scaleVar)/2,
centery : (($(window).height()-translateVar[1])/scaleVar)/2
};
}
let link, node, simulation;
function initialize_link_node(svg) {
let currentScreen = getScreenInfo();
simulation = d3.forceSimulation()
.force("link", d3.forceLink()
.id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody()
.strength(function(d) { return -20000;}))
.force("center", d3.forceCenter(currentScreen.centerx, currentScreen.centery));
link = svg.append("g").selectAll(".link");
node = svg.append("g").selectAll(".node");
}
function spawn_nodes(svg, graph, around_node, filtering_options) {
node = node.data(theData.nodes, function(d) {
return d.id;
});
let newNode = node.enter().append("g");
newNode = newNode.append("circle")
.attr("class", "node")
.attr("r", radius)
.attr("fill", function(d) {
return colors[d.type]
})
.attr("id", function(d) {
return d.id
});
/****************************************************
newNode.append("text")
.text( function(d) {
return d.id;
})
.style("fill", "red")
.style("text-anchor", "middle");
newNode.append("svg:image")
.attr("href", function(d) {
return dict_type_icon[d.type];
}).attr("width", "30")
.attr("height", "30");**************************/
newNode.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
);
node = node.merge(newNode);
link = link.data(theData.edges, function(d) {
return d.id;
});
let newLink = link.enter().append("line").attr("class", "link").attr("marker-end", "url(#arrow)");
newLink.append("title").text(function(d) {
return d.labeled;
});
link = link.merge(newLink);
node = node.merge(newNode);
simulation.nodes(theData.nodes).on("tick", ticked);
simulation.force("link").links(theData.edges);
simulation.alpha(1).alphaTarget(0).restart();
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
function ticked() {
let currentScreen = getScreenInfo();
node
.attr("cx", function(d) {
return d.x = Math.max(radius, Math.min(currentScreen.width - radius, d.x));
})
.attr("cy", function(d) { return d.y = Math.max(radius, Math.min(currentScreen.height - radius, 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; });
}
When I uncomment the circles appear at (0,0) point as you may have seen in the image.
I would appreciate if you could help me! Thank you.
Right now, you're turning newNode into a circles' selection:
newNode = newNode.append("circle")
//etc...
So, in order to keep it as a groups' selection, change that line to:
newNode.append("circle")
//etc...
Then, uncomment the commented part.
Finally, since that now node.merge(newNode) will be a groups' selection, change the cx and cy in ticked function to translate:
node.attr("transform", function(d) {
return "translate(" + (d.x = Math.max(radius, Math.min(currentScreen.width - radius, d.x))) +
"," + (d.y = Math.max(radius, Math.min(currentScreen.height - radius, d.y))) + ")";
});

d3 v4 force layout with boundary

I'm trying to create a forceSimulation in d3 v4 which does not let the nodes float outside the boundries of the svg in the same way that this example has it for d3 v3 https://bl.ocks.org/mbostock/1129492.
Have tried a few different things in simulation.on("tick", ticked) to no avail. My codePen is below. Any ideas on how to achieve this?
https://codepen.io/mtsvelik/pen/rzxVrE
//Read the data from the mis element
var graph = document.getElementById('json').innerHTML;
graph = JSON.parse(graph);
render(graph);
function render(graph){
// Dimensions of sunburst.
var radius = 6;
var maxValue = d3.max(graph.links, function(d, i, data) {
return d.value;
});
//sub-in max-value from
d3.select("div").html('<form class="force-control" ng-if="formControl">Link threshold 0 <input type="range" id="thersholdSlider" name="points" value="0" min="0" max="'+ maxValue +'">'+ maxValue +'</form>');
document.getElementById("thersholdSlider").onchange = function() {threshold(this.value);};
var svg = d3.select("svg");
var width = svg.attr("width");
var height = svg.attr("height");
console.log(graph);
var graphRec = JSON.parse(JSON.stringify(graph)); //Add this line
//graphRec = graph; //Add this line
console.log(graphRec);
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody().strength(Number(-1000 + (1.25*graph.links.length)))) //default force is -30, making weaker to increase size of chart
.force("center", d3.forceCenter(width / 2, height / 2));
var link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); });
var node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", radius)
.attr("fill", function(d) { return d.color; })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
node.append("title")
.text(function(d) { return d.id; });
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
console.log(link.data(graph.links));
function ticked() {
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("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
function threshold(thresh) {
thresh = Number(thresh);
graph.links.splice(0, graph.links.length);
for (var i = 0; i < graphRec.links.length; i++) {
if (graphRec.links[i].value > thresh) {graph.links.push(graphRec.links[i]);}
}
console.log(graph.links);
/*var threshold_links = graph.links.filter(function(d){ return (d.value > thresh);});
console.log(graph.links);
restart(threshold_links);*/
restart();
}
//Restart the visualisation after any node and link changes
// function restart(threshold_links) {
function restart() {
//DATA JOIN
//link = link.data(threshold_links);
link = link.data(graph.links);
console.log(link);
//EXIT
link.exit().remove();
console.log(link);
// ENTER - https://bl.ocks.org/colbenkharrl/21b3808492b93a21de841bc5ceac4e47
// Create new links as needed.
link = link.enter().append("line")
.attr("class", "link")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); }).merge(link);
console.log(link);
// DATA JOIN
node = node.data(graph.nodes);
/*
// EXIT
node.exit().remove();
// ENTER
node = node.enter().append("circle")
.attr("class", "node")
.attr("r", radius)
.attr("fill", function(d) {return d.color;})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
)
.merge(node);
node.append("title")
.text(function(d) { return d.id; });
*/
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
simulation.alphaTarget(0.3).restart();
}
}
In the tick function restrict the nodes to move out from the boundary:
node
.attr("cx", function(d) {
return (d.x = Math.max(radius, Math.min(width - radius, d.x)));
})
.attr("cy", function(d) {
return (d.y = Math.max(radius, Math.min(height - radius, d.y)));
})
//now update the links.
working code here
You can also use d3.forceBoundary that allows you to set a boundary with a strength. In your code
import it
<script src="https://unpkg.com/d3-force-boundary#0.0.1/dist/d3-force-boundary.min.js"></script>
then
var simulation = d3.forceSimulation()
.force("boundary", forceBoundary(0,0,width, height))
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody().strength(Number(-1000 + (1.25*graph.links.length)))) //default force is -30, making weaker to increase size of chart
.force("center", d3.forceCenter(width / 2, height / 2));
your pen fixed https://codepen.io/duto_guerra/pen/XWXagqm

Resources