Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

D3 update on node removal always remove the last entry in SVG DOM

I'm seeing a weird behaviour in my D3 application and after hours of trying to figure out what's happening I hope someone can point me at the thing I obviously do wrong.

I have simplified the app down to be very simple and still exhibit the problem. As you'll see it's derived from all the great D3 examples out there. The simple scenario I have an issue with is: select a node (by clicking on it) and, upon hitting the delete key remove said node along with all related links and labels of both the node and the links.

The code pasted below is nearly there since it decreases the number of Nodes and Links exactly as anticipated (given any particular graph) but there is one issue: both the node and link labels are not the correct ones and end up distributed over different circles...

Any idea as to what might be going on would be greatly appreciated!

Code:

var width = 960,
    height = 700,
    colors = d3.scale.category20();

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

var force = d3.layout.force()
    .gravity(.05)
    .distance(200)
    .charge(-150)
    .size([width, height]);

var jsonnodes, jsonlinks;
var node, link, label;
var selected_node = null,
    mousedown_node = null,
    mousedown_link = null;

d3.json("graph.json", jsondatacallback);


//
// Functions
//

function jsondatacallback(error, json) {
jsonnodes = json.nodes;
jsonlinks = json.links;
force.nodes(jsonnodes)
        .links(jsonlinks);

//
// Nodes
//
node = svg.selectAll(".node")
        .data(jsonnodes);    
node.enter().append("g")
        .attr("class", "node")
        .on('mousedown', function(d) {
            mousedown_node = d;
            if (mousedown_node === selected_node)
                selected_node = null;
            else
                selected_node = mousedown_node;
        })
        .call(force.drag);
node.append("circle")
        .attr('r', 11)
        .style('stroke', function(d) {
            return d3.rgb(colors(d.name)).darker().toString();
        });
node.append("text")
        .attr("dx", 12)
        .attr("dy", ".35em")
        .text(function(d) {
            return d.name;
        });

//
// Links
//
link = svg.selectAll(".link")
        .data(jsonlinks);  
link.enter().append("line")
        .attr("class", "link");

//
// Labels (for links)
//
label = svg.selectAll(".label")
        .data(jsonlinks);
label.enter().append("text")
        .attr("class", "label");    
label.attr("dx", 12)
        .attr("dy", ".35em")
        .attr("x", function(d) {return (d.source.x + d.target.x) / 2;})
        .attr("y", function(d) {return (d.source.y + d.target.y) / 2;})
        .text(function(e) {
            return Math.random().toString(36).substring(7); ;  
        });

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 + ")";});
    label.attr("x", function(d) {return (d.source.x + d.target.x) / 2;})
            .attr("y", function(d) {return (d.source.y + d.target.y) / 2;});
});

d3.select(window)
        .on("keydown", keydown);
restart();
}

function keydown() {
d3.event.preventDefault();
var lastKeyDown = d3.event.keyCode;

if (!selected_node)
    return;
switch (d3.event.keyCode) {
    case 8: // backspace
    case 46: // delete
        if (selected_node) {
            removeNode(selected_node);
            removeLinks(selected_node);
        }
        selected_node = null;
        restart();
        break;
}
}

function restart() {
//
// nodes
//
node = svg.selectAll(".node")
        .data(jsonnodes);
node.exit().remove();
node.style('fill', function(d) {
            return (d === selected_node) ? d3.rgb(colors(d.name)).brighter().toString() : colors(d.name);
        })
        .on('mousedown', function(d) {
            mousedown_node = d;
            if (mousedown_node === selected_node)
                selected_node = null;
            else
                selected_node = mousedown_node;
            restart();
        });
node.enter().append("g")
        .attr("class", "node")
        .on('mousedown', function(d) {
            mousedown_node = d;
            if (mousedown_node === selected_node)
                selected_node = null;
            else
                selected_node = mousedown_node;
        });
node.enter().append("text")
        .attr("dx", 12)
        .attr("dy", ".35em")
        .text(function(d) {
            return Math.random().toString(36).substring(7);
        });
node.enter().append("circle")
        .attr('r', 11)
        .style('stroke', function(d) {
            return d3.rgb(colors(d.name)).darker().toString();
        });

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

//
// labels
//
label = svg.selectAll(".label")
        .data(jsonlinks);
label.exit().remove();
label.enter().append("text")
        .attr("class", "label")
        .text(function(d) {
            var lbl = d.source.name + "_" + d.target.name;
            return lbl ;
        });    
label.attr("x", function(d) {return (d.source.x + d.target.x) / 2;})
        .attr("y", function(d) {return (d.source.y + d.target.y) / 2;});;

force.start();
}

function removeNode(victim) {
var searchres = findNodeIndex(jsonnodes, victim.name);
if (searchres === null) {
    console.log("Node to be removed not found.");
} else {
    jsonnodes.splice(searchres, 1);
}
}

function removeLinks(victim) {
var searchres = findFirstLinkIndex(jsonlinks, victim.name);
if (searchres !== null) {
    jsonlinks.splice(searchres, 1);
    removeLinks(victim);
}
}

// Returns the position/index in node collection of the node with name value name
function findNodeIndex(coll, name) {
if (coll === null)
    return null;
for (var i=0; i<coll.length; i++) {
    if (coll[i].name === name) {
        return i;
    }
}
return null;
}

// Returns the position/index of the first link matching the provided node name
function findFirstLinkIndex(coll, name) {
if (coll === null)
    return null;
for (var i=0; i<coll.length; i++) {
    if ((coll[i].source.name === name) || (coll[i].target.name === name))
        return i;
}
return null;
}
like image 753
Frodo Avatar asked Mar 07 '14 03:03

Frodo


1 Answers

If you are going to be deleting data elements from the middle of your array, you need to specify a key function to the data join so d3 knows which data should go with which element. Otherwise, the data is matched to elements in the order they are found and when there isn't enough data to go around the last element is the one that ends up removed.

  • Tutorial on key functions and object constancy
  • API and links to more tutorials

Since you're using the name property of each data element as the identifier for removing elements, that is the logical choice for a data key.

node = svg.selectAll(".node")
        .data(jsonnodes, function(d){return d.name;});    
/*...*/
link = svg.selectAll(".link")
        .data(jsonlinks, 
              function(d){return d.source.name + "_" + d.target.name;});
/*...*/
label = svg.selectAll(".label")
        .data(jsonlinks,  
              function(d){return d.source.name + "_" + d.target.name;});
like image 161
AmeliaBR Avatar answered Sep 29 '22 10:09

AmeliaBR