I am trying to select a set of nodes in a Force Directed Layout graph in d3, then to compress the component the nodes form. My idea was to make a force simulation, as shown below:
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().distance(function(d) {
return d.distance;
}).strength(0.5))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
Since it relies on distance, I thought finding and selecting the appropriate links in the graph’s data and shrinking it, such as
graph_data.links[indx].distance = 0;
would compress it. When I think about it, I would have to refresh the graph in some way with this new data. However, that is not ideal as I do not want the graph to rebuild itself every time I select a component. Is there a way to change these distances without having to feed a redrawn graph newly modified data, such as selecting the link in the simulated graph directly rather than the passed data?
Advertisement
Answer
However, that is not ideal as I do not want the graph to rebuild itself every time I select a component
You don’t really have to, just update the data and restart the simulation:
<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v6.js"></script>
</head>
<body>
<svg height="500" width="500"></svg>
<script>
var svg = d3.select('svg'),
width = +svg.attr('width'),
height = +svg.attr('height');
var data = {
nodes: [
{ id: 'a' },
{ id: 'b' },
{ id: 'c' },
{ id: 'x' },
{ id: 'y' },
{ id: 'z' },
],
links: [
{ source: 'a', target: 'b', distance: 200 },
{ source: 'b', target: 'c', distance: 200 },
{ source: 'c', target: 'a', distance: 200 },
{ source: 'x', target: 'y', distance: 200 },
{ source: 'y', target: 'z', distance: 200 },
{ source: 'z', target: 'x', distance: 200 },
],
};
var simulation = d3
.forceSimulation()
.force(
'link',
d3
.forceLink()
.id((d) => d.id)
.distance(function (d) {
return d.distance;
})
.strength(0.5)
)
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2));
var link = svg
.append('g')
.attr('class', 'links')
.selectAll('line')
.data(data.links)
.enter()
.append('line')
.attr('stroke', 'black');
var node = svg
.append('g')
.attr('class', 'nodes')
.selectAll('circle')
.data(data.nodes)
.enter()
.append('circle')
.attr('cx', width / 2)
.attr('cy', height / 2)
.attr('r', 20)
.on('click', function (e, d) {
link.data().forEach(function (l) {
if (l.source.id === d.id || l.target.id === d.id) {
l.distance = 0;
} else {
l.distance = 200;
}
});
// re-bind data
simulation.force('link').links(data.links);
// restart simulation
simulation.alpha(1).restart();
});
simulation.nodes(data.nodes).on('tick', ticked);
simulation.force('link').links(data.links);
function ticked() {
node.attr('cx', (d) => d.x).attr('cy', (d) => 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);
}
</script>
</body>
</html>