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