title: D3 update on node removal always remove the last entry in SVG DOM
body: <p>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.</p>
  
  <p>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.</p>
  
  <p>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...</p>
  
  <p>Any idea as to what might be going on would be greatly appreciated!</p>
  
  <p>Code:</p>
  
  <pre><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&lt;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&lt;coll.length; i++) {
      if ((coll[i].source.name === name) || (coll[i].target.name === name))
          return i;
  }
  return null;
  }
  </code></pre>