How to disable animation in a force-directed graph? [closed]
Asked Answered
C

2

23

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.

Changteh answered 27/11, 2017 at 12:19 Comment(10)
See this example.Gregarine
@Gregarine OP doesn't even need web workers... this is a trivial thing to do.Cowpea
@GerardoFurtado, with a trivial graph, yes the web worker is overkill. But your code below executes a tight loop on the main browser thread. If you needed more iterations to "settle" your simulation, the browser would go unresponsive.Gregarine
But even in that scenario the burden to the browser is smaller than a "regular" force.Cowpea
I'm wondering why this was closed as "why isn't this code working?". This is clearly not the case here. At best, this would be "too broad". Even so, two gold badge users for D3 commented here saw no reason to close this. I just voted to reopen.Cowpea
I'm voting to reopen this question, again... as I stated in the comment above, it's not clear why a mod closed it as off-topic.Cowpea
@GerardoFurtado Given SO's standards the question should probably be closed. However, this would put the post in a long list of popular, highly upvoted questions having highly upvoted answers which are closed or even deleted for not fitting into the narrow ruleset of this site whereby losing valueable information. That's what happens if you favour form over content. There are scores of discussions on Meta about this. Anyways, I second your appeal and also voted to reopen. I am not very confident, though.Cheree
@Cheree I disagree mainly with the reason: "why isn't this code working?". That's clearly not the case here. Too broad may be a bit better, but even so, you know better than me that this kind of question is quite common in the D3 community: OP shares a code and asks for a modification in some D3 aspect of it. I don't believe it is that low regarding S.O. standards.Cowpea
@GerardoFurtado Agreed, that's why I cast my reopen vote. And I am backing your assessment that the close reason, if any, should be too broad. There are so many questions of lowest quality that surely deserve to be nuked and I am an advocate of rigorously downvoting, close-voting and deleting those posts to maintain a high level of quality. I think the main problem with this question is that you have to be a domain expert in D3 to realize that it is actually on the point, concise and well worth a thorough answer.Cheree
@meagar Does this work? Can I actually ping the close voter? Could you rethink your decision to close vote this question reading through the comments and given the upvotes on the question and answers? Since d3 is a fairly narrow tag I doubt we will ever get enough voters to reopen this the regular way.Cheree
B
3

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>
Benignant answered 27/11, 2017 at 13:58 Comment(0)
C
42

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);
Cowpea answered 28/11, 2017 at 0:28 Comment(7)
I would be happy to delete my answer but I am not sure that I can. Maybe I can flag it?Benignant
@Benignant I reckon you shouldn't delete your answer: it has information, and we should never delete information. However, you have a couple of downvotes (you can't see it, but you have 1 upvote and 2 downvotes)... So, you may just edit it explaining that you're simply hiding the simulation. That way you make clear to OP that the user is not seeing the animation, but the animation is running.Cowpea
Thanks for the suggestion! Hopefully, that should direct people to the better answer.Benignant
@GerardoFurtado Are you sure the link to Mike Bostock’s example is (still) correct? In his code I do not see any similarity to the code in your answer.Opulent
@GerardoFurtado how can we create the same graph with clicking and dragging still allowed? Just no gravity/physics?Grace
@Grace You just have to create a custom drag function, not that one in the code (that's just a leftover from the original code).Cowpea
@Opulent Bostock moved all his examples from bl.ocks.org to Observable, which is indeed quite different.Cowpea
B
3

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>
Benignant answered 27/11, 2017 at 13:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.