Skip to content
Advertisement

d3.js Compressing links of a component

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>
User contributions licensed under: CC BY-SA
9 People found this is helpful
Advertisement