haz
haz

Reputation: 2288

D3 force-directed graph adding new nodes causes x & y to be NaN

When I click on a node, I want a new node to be added to it. They should both have labels (I'm trying to build a thesaurus visualization).

I'm very new to D3, so I apologize if you have to explain things in a bit more detail.

This is my code so far:

var width = 960;
var height = 500;

var force = d3.layout.force()
    .gravity(0.05)
    .distance(100)
    .charge(-100)
    .size([width, height])
    .nodes([{ "name": "One", "group": 1 }])
    .start();

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

var nodes = force.nodes();
var links = force.links();

var link = svg.selectAll(".link")
    .data(links)
    .enter().append("line")
    .attr("class", "link");

var node = svg.selectAll(".node")
    .data(force.nodes())
    .enter().append("g")
    .attr("class", "node")
    .call(force.drag);

node.append("circle")
    .attr("r", 10)
    .on("mousedown", onClick);

node.append("text")
    .attr("dx", 12)
    .attr("dy", ".35em")
    .text(d => d.name);

force.on("tick", function() {
    link.attr("x1", d => d.source.x)
        .attr("y1", d => d.source.y)
        .attr("x2", d => d.target.x)
        .attr("y2", d => d.target.y);

    node.attr("transform", d => {
        // d.x & d.y are NaN for new nodes
        return "translate(" + d.x + "," + d.y + ")";
    });
});

restart();

function restart() {
    node = node.data(nodes);

    node.enter().append("g")
        .attr("class", "node")
        .call(force.drag);
    node.append("circle")
        .attr("r", 10)
        .on("mousedown", onClick);
    node.append("text")
        .attr("dx", 12)
        .attr("dy", ".35em")
        .text(d => d.name);
    node.exit().remove();

    link = link.data(links);
}

function onClick(clicked_node) {
    console.log("click!");
    console.log(clicked_node);
    var new_node = { name: "Test", group: 2 };
    nodes.push(new_node);
    // Has x & y set to NaN after adding
    links.push({ source: clicked_node, target: new_node });
    restart();
}

As soon as I click on the first node, causing the node

{ name: "Test", group: 2 };

to be added, D3 throws errors within

node.attr("transform", d => {
    // d.x & d.y are NaN for new nodes
    return "translate(" + d.x + "," + d.y + ")";
});

because the d.x and d.y for this new node are NaN.

I tried setting them explicitly:

{ name: "Test", group: 2, x: clicked_node.y, y: clicked_node.y };

But I get the same error. In the inspector, when this node is added to the screen, the x and y values becomes the px and py values instead!

I don't understand why this happens.

Upvotes: 2

Views: 1078

Answers (1)

Mark
Mark

Reputation: 108512

You are missing one line of code. After you add your new node you need to restart the simulation for d3 to calculate it's position:

function restart() {
    node = node.data(nodes);

    node.enter().append("g")
        .attr("class", "node")
        .call(force.drag);
    node.append("circle")
        .attr("r", 10)
        .on("mousedown", onClick);
    node.append("text")
        .attr("dx", 12)
        .attr("dy", ".35em")
        .text(d => d.name);
    node.exit().remove();

    link = link.data(links);

    force.start(); //<-- start simulation
}

Running code:

<!DOCTYPE html>
<html>

  <head>
    <script data-require="[email protected]" data-semver="3.5.17" src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.js"></script>
  </head>

  <body>
    <script>
      var width = 960;
var height = 500;

var force = d3.layout.force()
    .gravity(0.05)
    .distance(100)
    .charge(-100)
    .size([width, height])
    .nodes([{ "name": "One", "group": 1 }])
    .start();

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

var nodes = force.nodes();
var links = force.links();

var link = svg.selectAll(".link")
    .data(links)
    .enter().append("line")
    .attr("class", "link");

var node = svg.selectAll(".node")
    .data(force.nodes())
    .enter().append("g")
    .attr("class", "node")
    .call(force.drag);

node.append("circle")
    .attr("r", 10)
    .on("mousedown", onClick);

node.append("text")
    .attr("dx", 12)
    .attr("dy", ".35em")
    .text(d => d.name);

force.on("tick", function() {
    link.attr("x1", d => d.source.x)
        .attr("y1", d => d.source.y)
        .attr("x2", d => d.target.x)
        .attr("y2", d => d.target.y);

    node.attr("transform", d => {
        // d.x & d.y are NaN for new nodes
        return "translate(" + d.x + "," + d.y + ")";
    });
});

restart();

function restart() {
    node = node.data(nodes);

    node.enter().append("g")
        .attr("class", "node")
        .call(force.drag);
    node.append("circle")
        .attr("r", 10)
        .on("mousedown", onClick);
    node.append("text")
        .attr("dx", 12)
        .attr("dy", ".35em")
        .text(d => d.name);
    node.exit().remove();

    link = link.data(links);
    
    force.start();
}

function onClick(clicked_node) {
    console.log("click!");
    console.log(clicked_node);
    var new_node = { name: "Test", group: 2 };
    nodes.push(new_node);
    // Has x & y set to NaN after adding
    links.push({ source: clicked_node, target: new_node });
    restart();
}
    </script>
  </body>

</html>

Upvotes: 2

Related Questions