xdl
xdl

Reputation: 1110

How to smoothly update a D3 v4 force diagram with new data

I'm looking for a way to introduce new nodes into a force directed directed graph that comes from brand new data (e.g. from a data stream).

In mbostock's examples (either this or this), the nodes are able to smoothly enter and exit because in the initial setup, every node is rendered.

However, if a brand new data point is introduced, the graph renders from scratch again. Is there a way to get a brand new node to transition smoothly into the graph?

See this codepen (it's a direct adaption of the 2nd example) for what I mean; entering and exiting existing nodes is fine, but the transition for when a brand new node enters is jumpy.

  //smooth update
  nodes = [a, b];
  links = [l_ab];
  restart();

  //not as smooth
  var d = {id: id++};
  nodes = [a, b, c, d];
    links = [l_ab, l_bc, l_ca, {
      source: a,
      target: d
    }];
  restart();

Upvotes: 1

Views: 2302

Answers (2)

Benoit Guigal
Benoit Guigal

Reputation: 858

I published a bl.ocks where I worked on smooth transitions between two successive layouts of the graph as data is added/removed. Instead of using a tick function to update the view at each iteration of the force simulation, the new layout is calculated in a Web Worker and the nodes/links are re-positioned with d3 transition mechanism once the simulation has converged. The result allows you to easily track the position of the nodes.

Upvotes: 1

Gerardo Furtado
Gerardo Furtado

Reputation: 102194

The entrance of the brand new node seems "jumpy" for a simple reason: when the simulation starts, that node first appears at the top-left corner (0,0 in the SVG coordinates system).

There are different solutions here, depending on the definition of "smoothly". I reckon that the most obvious way to make it smoothier is setting the initial position of the node to the center of the SVG. That way, the new node will not travel that much to its final position.

We can do it setting the x and y properties of the new node:

var d = {id: id++, x: width/2, y: height/2};

Here is your code with that change:

var svg = d3.select("svg"),
  width = 250
height = 250
color = d3.scaleOrdinal(d3.schemeCategory10);

var a = {
    id: "a"
  },
  b = {
    id: "b"
  },
  c = {
    id: "c"
  },
  nodes = [a, b, c],
  l_ab = {
    source: a,
    target: b
  }
l_bc = {
    source: b,
    target: c
  },
  l_ca = {
    source: c,
    target: a
  }
links = [l_ab, l_bc, l_ca];

var id = 0;

var simulation = d3.forceSimulation(nodes)
  .force('charge', d3.forceManyBody())
  .force('link', d3.forceLink())
  .force('center', d3.forceCenter(width / 2, height / 2))
  .alphaTarget(1)
  .on("tick", ticked)
  .stop()

var g = svg.append("g")
link = g.append("g").attr("stroke", "#000").attr("stroke-width", 1.5).selectAll(".link"),
  node = g.append("g").attr("stroke", "#fff").attr("stroke-width", 1.5).selectAll(".node");

restart();

function restart() {

  // Apply the general update pattern to the nodes.
  node = node.data(nodes, function(d) {
    return d.id;
  });

  node.exit().transition()
    .attr("r", 0)
    .remove();

  node = node.enter().append("circle")
    .attr("fill", function(d) {
      return color(d.id);
    })
    .call(function(node) {
      node.transition().attr("r", 8);
    })
    .merge(node);

  // Apply the general update pattern to the links.
  link = link.data(links, function(d) {
    return d.source.id + "-" + d.target.id;
  });

  // Keep the exiting links connected to the moving remaining nodes.
  link.exit().transition()
    .attr("stroke-opacity", 0)
    .attrTween("x1", function(d) {
      return function() {
        return d.source.x;
      };
    })
    .attrTween("x2", function(d) {
      return function() {
        return d.target.x;
      };
    })
    .attrTween("y1", function(d) {
      return function() {
        return d.source.y;
      };
    })
    .attrTween("y2", function(d) {
      return function() {
        return d.target.y;
      };
    })
    .remove();

  link = link.enter().append("line")
    .call(function(link) {
      link.transition().attr("stroke-opacity", 1);
    })
    .merge(link);

  // Update and restart the simulation.
  simulation.nodes(nodes);
  simulation.force("link").links(links);
  simulation.alpha(1).restart();
}

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

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

restart();

document.getElementById('btnRenderExisting3Node').addEventListener('click', function() {
  nodes = [a, b, c];
  links = [l_ab, l_bc, l_ca];
  restart();
});

document.getElementById('btnRenderExisting2Node').addEventListener('click', function() {
  nodes = [a, b];
  links = [l_ab];
  restart();
});

document.getElementById('btnRenderNewNode').addEventListener('click', function() {
  var d = {
    id: id++,
    x: width / 2,
    y: height / 2
  };
  nodes = [a, b, c, d];
  links = [l_ab, l_bc, l_ca, {
    source: a,
    target: d
  }];
  restart();
});
svg {
  border: 1px black solid
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<div>
  <button id='btnRenderExisting2Node'>Render 2 existing nodes</button>
  <button id='btnRenderExisting3Node'>Render 3 existing nodes</button>
  <button id='btnRenderNewNode'>Render 3 existing nodes and 1 brand new node</button>
</div>
<svg width="250" height="250"></svg>
</div>

Upvotes: 6

Related Questions