Home > database >  d3js v7: add node to forceSimulation
d3js v7: add node to forceSimulation

Time:07-19

I am trying to dynamically add nodes to a forceSimulation in d3. The code snippet below should first generate the nodes from a static object graph and then after 5 seconds a new node should be added to graph and the Force-directed graph in d3 should be updated with the new node. But there is an error in my implementation and instead all the nodes jump to the top-left corner. My current "solution" uses the merge syntax as described in another similar SO question: https://stackoverflow.com/a/40028002/14923227 and here. I believe this is the right way to go about it but I am not sure how to implement it with my contrived example below.

var graph = {
  "nodes": [
    {
      "id": 1,
      "name": "A"
    },
    {
      "id": 2,
      "name": "B"
    },
    {
      "id": 3,
      "name": "C"
    }
  ],
  "links": [
    {
      "source": 0,
      "target": 1
    },
    {
      "source": 1,
      "target": 2
    },
    {
      "source": 0,
      "target": 2
    }
  ]
};

var svg = d3.select("svg"),
      width =  svg.attr("width"),
      height =  svg.attr("height");
      
var simulation = d3.forceSimulation()
    .force("charge", d3.forceManyBody().strength(-400))
    .force("center", d3.forceCenter(width / 2, height / 2))
    .nodes(graph.nodes)
    .on("tick", ticked)
    .force("link", d3.forceLink(graph.links))
      
var link = svg.selectAll(".link")
                .data(graph.links)
                .join("line")
                .classed("link", true)
                .style("stroke", "#aaa")

var node = svg.selectAll(".node")
                .data(graph.nodes);

var g = node.enter()
            .append('g')
            .attr('class', 'node');

g.append('circle')
      .attr("r", 20)
      .style("fill", "#d9d9d9");

g.append('text')
      .attr('class', 'text')
      .text(function(d) { return d.name });

node.exit().remove()

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

    svg.selectAll('.node')
         .attr("cx", function (d) { return d.x; })
         .attr("cy", function(d) { return d.y; })
         .attr('transform', function(d) {
              return 'translate('   d.x   ','   d.y   ')';
         })
}

// problem here: add new node after five seconds
setTimeout(() => {
    graph.nodes.push({"id": 4, "name": "X"}); // new node 'X'
    
    // this line gives error
    //graph.links.push({"source": 2, "target": 3}); // connect 'X' with 'C'

    // Update the nodes
    node = node.data(graph.nodes);

    // Enter any new nodes
    nodeEnter = node.enter()
                    .append("g")
                    .attr('class', 'node');

    nodeEnter.append('circle')
        .attr("r", 20)
        .style("fill", "#d9d9d9");

    nodeEnter.append('text')
        .attr('class', 'text')
        .text(function(d) { return d.name });
    
    node = nodeEnter.merge(node);

    nodeEnter.exit().remove();

    simulation.restart();
  }, 5000)
<!DOCTYPE html>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v7.min.js"></script>
<body>
<svg width="960" height="600"></svg>
</body>

CodePudding user response:

While the enter method will create new elements in the DOM, it won't add new elements to the simulation. You'll need to specify a new nodes array to update the simulation:

  simulation.nodes(graph.nodes);

Of course, since you are using a centering force this will cause the other nodes to jump away (the centering force does not pull nodes to a center, it keeps their center of gravity at the center). You'll want to do something like set the x,y properties of the new node to something sensible, I've set it to the average value of all the other nodes, which won't upset the centering force:

    // sum x,y values, find mean for all nodes:
    var sx = 0; 
    var sy = 0;
    graph.nodes.forEach(function(node) { sx  = node.x; sy  = node.y })
    var mx = sx / graph.nodes.length;
    var my = sy / graph.nodes.length;

   // add new node
    graph.nodes.push({"id": 4, "name": "X", x: mx, y: my}); // new node 'X'

And as restart won't reheat the simulation, I've reheated it with some more alpha on restart. All together, that gives us:

var graph = {
  "nodes": [
    {
      "id": 1,
      "name": "A"
    },
    {
      "id": 2,
      "name": "B"
    },
    {
      "id": 3,
      "name": "C"
    }
  ],
  "links": [
    {
      "source": 0,
      "target": 1
    },
    {
      "source": 1,
      "target": 2
    },
    {
      "source": 0,
      "target": 2
    }
  ]
};

var svg = d3.select("svg"),
      width =  svg.attr("width"),
      height =  svg.attr("height");
      
var simulation = d3.forceSimulation()
    .force("charge", d3.forceManyBody().strength(-400))
    .force("center", d3.forceCenter(width / 2, height / 2))
    .nodes(graph.nodes)
    .on("tick", ticked)
    .force("link", d3.forceLink(graph.links))
      
var link = svg.selectAll(".link")
                .data(graph.links)
                .join("line")
                .classed("link", true)
                .style("stroke", "#aaa")

var node = svg.selectAll(".node")
                .data(graph.nodes);

var g = node.enter()
            .append('g')
            .attr('class', 'node');

g.append('circle')
      .attr("r", 20)
      .style("fill", "#d9d9d9");

g.append('text')
      .attr('class', 'text')
      .text(function(d) { return d.name });

node.exit().remove()

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

    svg.selectAll('.node')
         .attr("cx", function (d) { return d.x; })
         .attr("cy", function(d) { return d.y; })
         .attr('transform', function(d) {
              return 'translate('   d.x   ','   d.y   ')';
         })
}

// problem here: add new node after five seconds
setTimeout(() => {
    
    var sx = 0;
    var sy = 0;
    graph.nodes.forEach(function(node) { sx  = node.x; sy  = node.y })
    var mx = sx / graph.nodes.length;
    var my = sy / graph.nodes.length;


    graph.nodes.push({"id": 4, "name": "X", x: mx, y: my}); // new node 'X'
    
    // this line gives error
    //graph.links.push({"source": 2, "target": 3}); // connect 'X' with 'C'

    // Update the nodes
    node = node.data(graph.nodes);
    simulation.nodes(graph.nodes);

    // Enter any new nodes
    nodeEnter = node.enter()
                    .append("g")
                    .attr('class', 'node');

    nodeEnter.append('circle')
        .attr("r", 20)
        .style("fill", "#d9d9d9");

    nodeEnter.append('text')
        .attr('class', 'text')
        .text(function(d) { return d.name });
    
    node = nodeEnter.merge(node);

    nodeEnter.exit().remove();

    simulation.alpha(0.3).restart();
  }, 5000)
<!DOCTYPE html>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v7.min.js"></script>
<body>
<svg width="400" height="200"></svg>
</body>

  • Related