How to disable animation in a force-directed graph

2020-02-13 07:56发布

问题:


Want to improve this question? Update the question so it's on-topic for Stack Overflow.

Closed 2 years ago.

Is there any way of disabling the animation in a D3 force-directed graph?

I am working with this example: https://bl.ocks.org/mbostock/4062045

I want to render the graph without the initial animation, that is, showing all the nodes and links in their final positions.

回答1:

EDIT

This method simply hides the animation part of the simulation. Please see Gerardo Furtado's answer which performs the simulation without drawing the intermediary results, meaning that the user doesn't have to wait whilst the solution is slowly evolving.

========

The 'animation' is actually the simulation running. It is possible to play with the time that the simulation runs but this might mean that the nodes get stuck at a local minimum - see the documentation here for more details.

You do have the option of adding a listener to the end event which fires when the simulation has finished. I have created a snippet where the Graph is initially hidden and then appears once it has finished simulating.

An alternative would be to render the chart server-side (if this is an option) and then serve a ready-drawn SVG which could be further manipulated with d3.

var svg = d3.select("svg"),
  width = +svg.attr("width"),
  height = +svg.attr("height");

var color = d3.scaleOrdinal(d3.schemeCategory20);

var simulation = d3.forceSimulation()
  .force("link", d3.forceLink().id(function(d) {
    return d.id;
  }))
  .force("charge", d3.forceManyBody())
  .force("center", d3.forceCenter(width / 2, height / 2))
  .on('end', function() {
    svg.classed('hidden', false)
    d3.select('#loading').remove()
  });

// I wasn't able to get the snippet to load the original data from https://bl.ocks.org/mbostock/raw/4062045/miserables.json so this is a copy hosted on glitch
d3.json("https://cdn.glitch.com/8e57a936-9a34-4e95-a03d-598e5738f44d%2Fmiserables.json", function(error, graph) {
  if (error) {
    console.log(error)
  };

  var link = svg.append("g")
    .attr("class", "links")
    .selectAll("line")
    .data(graph.links)
    .enter().append("line")
    .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("r", 5)
    .attr("fill", function(d) {
      return color(d.group);
    })
    .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);

  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;
}
.links line {
  stroke: #999;
  stroke-opacity: 0.6;
}

.nodes circle {
  stroke: #fff;
  stroke-width: 1.5px;
}

.hidden {
  visibility: hidden
}

img {
    display: block;
    margin-left: auto;
    margin-right: auto;
   }
<script src="https://d3js.org/d3.v4.min.js"></script>
<img id ="loading" src="http://thinkfuture.com/wp-content/uploads/2013/10/loading_spinner.gif" />
<svg width="960" height="600" class="hidden"></svg>



回答2:

Despite this question already having an accepted answer, the proposed solution is not the correct way to disable the animation in a D3 force chart. The browser is still moving the nodes and links at every tick! You just don't see them moving, but the browser is moving them, doing a lot of computation and wasting a lot of time/resources. Also, you don't need server side for this.

My answer proposes a different solution, which actually don't draw the animation. You can see it in this code by Mike Bostock (D3 creator), for instance.

This solution is easy to follow when you understand what is the tick function: it's just a function that computes all the positions in the simulation and advances one step. Despite the vast majority of D3 force-directed graphs drawing the nodes and the links at every tick, you don't need to do that.

Here is what you can do:

  1. Stop the simulation using stop(), immediately after defining it:

    var simulation = d3.forceSimulation(graph.nodes)
        .force("link", d3.forceLink().id(function(d) { return d.id; }))
        .force("charge", d3.forceManyBody())
        .force("center", d3.forceCenter(width / 2, height / 2))
        .stop();//stop the simulation here
    
  2. Make the simulation run without drawing anything. That's the most important step: you don't have to move the elements at each tick. Here, I'm running 300 ticks, which is approximately the default number:

    for (var i = 0; i < 300; ++i) simulation.tick();
    
  3. Then, simply use the properties created by the simulation (x, y, source, target) to draw the circles and the lines, just once:

    node.attr("cx", function(d) { return d.x; })
        .attr("cy", function(d) { return d.y; })
    

Here is the linked blocks with those changes only: http://bl.ocks.org/anonymous/8a4e4e2fed281ea5e2a5c804a9a03783/85ced3ea82a4bed20a2010530562b655d8f6e464

Compare the time of this solution versus the time of the "hiding-the-nodes" solution (the accepted answer). This one here is way faster. In my tests, I got this results:

  • "hiding-the-nodes" solution: around 5000ms
  • This solution: around 200ms

That is, 25 times faster.

PS: for simplicity, I removed the ticked function in the forked blocks. If you want to drag the nodes, just add it back.


EDIT for D3 v5.8

Now that D3 v5.8 allows passing the number of interactions to simulation.tick() you don't even need the for loop anymore. So, instead of:

for (var i = 0; i < 300; ++i) simulation.tick();

You can just do:

simulation.tick(300);