Mike Perrenoud
Mike Perrenoud

Reputation: 67898

Adding a node does not link properly to existing node

I have a d3 graph that leverages the Force Layout. When I have a group of nodes up front it lays out just fine. Specifically I mean the nodes stay nicely separated and the links are correct.

A demo of my issue is on jsFiddle here.

I've also included a code snippet below that appears to work like the jsFiddle.

However, if I start with one node and then add another down the line with the Add Person button, you'll notice the first node (even though it's referenced in the link) does not respond, nor can it be moved.

It seems like the latter is the real issue, in that it can't be moved.

What I've Tried

  1. Executing a force.resume() instead of the force...start() at the top when adding a new person to the graph. OUTCOME: this causes an error, Error: Invalid value for <g> attribute transform="translate(NaN,NaN)" to occur and I've not yet figured out why.
  2. Adding a force.resume() at the end of the method, and not adding the on('tick' ... again when adding a new person to the graph. I couple this by executing the force...start() at the top regardless of the resume. OUTCOME: that causes the first node to bounce around again (promising), but the added node stays in the top left corner as if it's not connected.

var scope = {};

scope.nodes = [];
scope.links = [];

var width = 960,
    height = 500;

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

var force = d3.layout.force()
    .charge(-150)
    .linkDistance(150)
    .size([width, height]);

function renderGraph(resume) {
    force.nodes(scope.nodes)
        .links(scope.links)
        .start();

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

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

    node.append("image")
        .attr("xlink:href", function (d) {
        return d.avatar || 'https://github.com/favicon.ico'
    })
        .attr("x", -56)
        .attr("y", -8)
        .attr("width", 64)
        .attr("height", 64);

    node.append("text")
        .attr("dx", 12)
        .attr("dy", ".35em")
        .text(function (d) {
        return d._id === scope.user.profile._id ? 'You' : d.firstName + ' ' + d.lastName
    });

    force.on("tick", function () {
        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("transform", function (d) {
            return "translate(" + d.x + "," + d.y + ")";
        });
    });
}

scope.user = {
    profile: {
        _id: 1,
        firstName: 'Bob',
        lastName: 'Smith'
    }
};
scope.nodes.push(scope.user.profile);
renderGraph();

var b = document.getElementById("addButton");
b.onclick = addPerson;

function addPerson() {
    scope.nodes.push({
        _id: 2,
        firstName: 'Jane',
        lastName: 'Smith'
    });
    scope.links.push({
        source: 0,
        target: scope.nodes.length - 1
    });
    renderGraph();
}
.link {
    stroke: #ccc;
}
.node text {
    pointer-events: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<button id="addButton">Add Person</button>

Upvotes: 1

Views: 73

Answers (2)

Cool Blue
Cool Blue

Reputation: 6476

I was a bit slow but I'll post my answer anyway because it's set out a little differently to the accepted answer...

There is no need to reconnect the arrays to the force layout each time so I moved it outside the render function, I also added good housekeeping by alowing for removal of deleted objects, but apart from that, not much to add.

var scope = {};

scope.nodes = [];
scope.links = [];

var width = 600,
    height = 190;

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

var force = d3.layout.force()
    .charge(-150)
    .linkDistance(150)
    .size([width, height])
	.nodes(scope.nodes)
    .links(scope.links)
        ;

function renderGraph(resume) {
    force
        .start();

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

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

    newNode.append("image")
        .attr("xlink:href", function (d) {
        return d.avatar || 'https://github.com/favicon.ico'
    })
        .attr("x", -56)
        .attr("y", -8)
        .attr("width", 64)
        .attr("height", 64);

    newNode.append("text")
        .attr("dx", 12)
        .attr("dy", ".35em")
        .text(function (d) {
        return d._id === scope.user.profile._id ? 'You' : d.firstName + ' ' + d.lastName
    });

    force.on("tick", function () {
        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("transform", function (d) {
            return "translate(" + d.x + "," + d.y + ")";
        });
    });
}

scope.user = {
    profile: {
        _id: 1,
        firstName: 'Bob',
        lastName: 'Smith'
    }
};
scope.nodes.push(scope.user.profile);
renderGraph();

var b = document.getElementById("addButton");
b.onclick = addPerson;

function addPerson() {
    scope.nodes.push({
        _id: 2,
        firstName: 'Jane',
        lastName: 'Smith'
    });
    scope.links.push({
        source: 0,
        target: scope.nodes.length - 1
    });
    renderGraph();
}
.link {
    stroke: #ccc;
}
.node text {
    pointer-events: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<button id="addButton">Add Person</button>

Upvotes: 1

Cyril Cherian
Cyril Cherian

Reputation: 32327

Here is the problem:

When you do

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

The node has the g element and the tick expects the selection i.e:

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

It should have been:

var link = svg.selectAll(".link")
    .data(scope.links);//this selection is expected in the tick function

link.enter().append("line")
    .attr("class", "link");

var node = svg.selectAll(".node")
    .data(scope.nodes);//this selection is expected in the tick function

//attaching text/circle everything pertaining to the node n the g group.
var nodeg = node.enter().append("g")
    .attr("class", "node")
    .call(force.drag);

Working code here

Hope this helps!

Upvotes: 1

Related Questions