Henry Marshall
Henry Marshall

Reputation: 870

Dynamically inserted d3 force graph not spreading out

The attached snippet contains a modified version of the official d3 force graph example code. When I insert the graph immediately everything works as expected. If however, I insert the graph dynamically (which you can do by pressing Clear and Redraw in the demo) the nodes do not spread out the same way. Sometimes they even stay in the top-left corner of the svg.

One hack I found was to add simulation.alphaTarget(1).restart() after inserting the graph. This unfortunately takes longer to reach a stable output and can lead to residual tremors (or spinning).

How do make the dynamically inserted graph have the behavior of the graph inserted immediately on page load without my hack?

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));

// I wrapped the d3.json invocation in this function
function drawGraph() {
  d3.json("https://gist.githubusercontent.com/mbostock/4062045/raw/5916d145c8c048a6e3086915a6be464467391c62/miserables.json", function(error, graph) {
    if (error) throw 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; });
    }
  });
}

// When I call it hear, all is well (as you would expect).
drawGraph()

// But when I clear and redraw it (by pressing a button), the nodes
// don't spread out.
function clearRedraw() {
  d3.selectAll("svg > *").remove()
  drawGraph()
}

function hackySolution() {
  d3.selectAll("svg > *").remove()
  drawGraph()
  simulation.alphaTarget(1).restart()
}


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;
}
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
  <button onclick="clearRedraw()">Clear And Redraw</button>
  <button onclick="hackySolution()">Hacky Solution</button>
  <svg width="960" height="600"></svg>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.3/d3.min.js"></script>
</body>
</html>

Upvotes: 1

Views: 1189

Answers (1)

Gerardo Furtado
Gerardo Furtado

Reputation: 102218

That's not a hacky solution! That's the correct, idiomatic way to re-heat the simulation.

The problem here is that when you do...

d3.selectAll("svg > *").remove()

... you are only removing the DOM elements. The simulation, however, is still running, and has cooled down.

Actually, if you wait until the simulation is completely finished (some 5 seconds) before clicking "Clear and Redraw", you're gonna see that the nodes always pile up at the origin (top left corner). Try to click on them: they will move to the center (because the drag function kind of re-heat the simulation, since it has an alphaTarget).

Therefore, you have to re-heat it.

However, instead of using alphaTarget, you should use alpha:

simulation.alpha(0.8).restart()

Here is the code with that change:

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));

// I wrapped the d3.json invocation in this function
function drawGraph() {
  d3.json("https://gist.githubusercontent.com/mbostock/4062045/raw/5916d145c8c048a6e3086915a6be464467391c62/miserables.json", function(error, graph) {
    if (error) throw 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; });
    }
  });
}

// When I call it hear, all is well (as you would expect).
drawGraph()

// But when I clear and redraw it (by pressing a button), the nodes
// don't spread out.
function clearRedraw() {
  d3.selectAll("svg > *").remove()
  drawGraph()
}

function hackySolution() {
  d3.selectAll("svg > *").remove()
  drawGraph()
  simulation.alpha(0.8).restart()
}


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;
}
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
  <button onclick="clearRedraw()">Clear And Redraw</button>
  <button onclick="hackySolution()">Hacky Solution</button>
  <svg width="960" height="600"></svg>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.3/d3.min.js"></script>
</body>
</html>

Alternatively, if you still feel that re-heating the simulation is a hacky solution (which it is not), just move the simulation assignment to inside the drawGraph function:

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

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



// I wrapped the d3.json invocation in this function
function drawGraph() {
  d3.json("https://gist.githubusercontent.com/mbostock/4062045/raw/5916d145c8c048a6e3086915a6be464467391c62/miserables.json", function(error, graph) {
    if (error) throw error;

    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));

    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;
        });
    }
  });
}

// When I call it hear, all is well (as you would expect).
drawGraph()

// But when I clear and redraw it (by pressing a button), the nodes
// don't spread out.
function clearRedraw() {
  d3.selectAll("svg > *").remove()
  drawGraph()
}


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;
}
<button onclick="clearRedraw()">Clear And Redraw</button>
<svg width="960" height="600"></svg>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.3/d3.min.js"></script>

Upvotes: 2

Related Questions