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>