CodyBugstein
CodyBugstein

Reputation: 23312

exit().remove() not working in my D3.js Force Layout Graph

I've created a simple D3 Force Layout graph. Please check it out in the JSFiddle here.

The graph is very basic - it features cities as nodes connected to nodes representing the ocuntry they are in. For simplicity, I've made only six nodes.

D3JS Graph

I've created a function called deleteNodeOnClick() and set it on the nodes like this

 var nodeEnter = node.enter()
        .append('g')
        .attr('class', 'node')
        .on("click", deleteNodeOnClick)

When you click on a node in the graph, that node gets removed from the data (actually for simplicity the first node gets removed from the data for now) however it does not get removed from the visual graph. You can look in the console and see that it is in fact removed from the data.

Why not? I am completely stumped.

The Code

var data = {
  nodes: [{
    name: "Canada"
  }, {
    name: "Montreal"
  }, {
    name: "Toronto"
  }, {
    name: "USA"
  }, {
    name: "New York"
  }, {
    name: "Los Angeles"
  }],
  links: [{
    source: 0,
    target: 1
  }, {
    source: 0,
    target: 2
  }, {
    source: 3,
    target: 4
  }, {
    source: 3,
    target: 5
  }, ]
};

var node;
var link;
var force;
var width = 400,
  height = 400;
  
var svg = d3.select("body").append("svg")
    .attr("width", window.innerWidth)
    .attr("height", window.innerHeight);

force = d3.layout.force()
  .size([width, length])
  .nodes(data.nodes)
  .links(data.links)
  .gravity(.1)
  .alpha(0.01)
  .charge(-400)
  .friction(0.5)
  .linkDistance(100)
  .on('tick', forceLayoutTick);

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

node = svg.selectAll('.node')
        .data(data.nodes, function(d){
            return d.name;
        });

    node.exit().remove();

    var nodeEnter = node.enter()
                        .append('g')
                        .attr('class', 'node')
                        .on("click", deleteNodeOnClick)
                        //.attr('r', 8)
                        //.attr('cx', function(d, i){ return (i+1)*(width/4); })
                        //.attr('cy', function(d, i){ return height/2; })
                        .call(force.drag);

  nodeEnter
        .append("circle")
        .attr("cx", 0)
        .attr("cy", 0)
        .attr("r", 10)
        .style("fill", "purple");

    nodeEnter
        .append("text")
        .text(function(d) { return d.name })
        .attr("class", "label")
        .attr("dx", 0)
        .attr("dy", ".35em");
        
 force.start();
 
 
 function forceLayoutTick(){
            
            node.attr("transform", function(d) {
                    
                // Keep in bounding box
                d.x = Math.max(10, Math.min(width - 10, d.x)); 
                d.y = Math.max(10, Math.min(height - 10, d.y));

                return "translate(" + d.x + "," + d.y + ")"; 
            });

            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; });
    };
    
    function deleteNodeOnClick(d){
        var dataBefore = JSON.parse(JSON.stringify(data.nodes));
        // Just delete the first node, for demonstration purposes
      data.nodes.splice(0, 1);
      console.info("Node should be removed", dataBefore, data.nodes);
    }

CSS

#graph {
  width: 100%;
  height: 100%;
}

#graph svg {
  background-color: #CCC;
}

.link {
  stroke-width: 2px;
  stroke: black;
}

.node {
  background-color: darkslategray;
  stroke: #138;
  width: 10px;
  height: 10px;
  stroke-width: 1.5px;
}

.node text {
  pointer-events: none;
  font: 10px sans-serif;
}

.label {
  display: block;
}

Upvotes: 0

Views: 1681

Answers (2)

Gerardo Furtado
Gerardo Furtado

Reputation: 102174

In D3, changing the data doesn't automagically change the SVG (or canvas, or HTML...) elements. You have to "repaint" your dataviz.

The good news is that you have (almost) all the selections. So, just to show you the general idea, I put all the rendering code inside a draw function, which is called on click:

function deleteNodeOnClick(d){
    data.nodes = data.nodes.filter(function(e){
        return e.name !== d.name;
    });
    draw();
}

Check the demo:

var data = {
    nodes: [{
        name: "Canada"
    }, {
        name: "Montreal"
    }, {
        name: "Toronto"
    }, {
        name: "USA"
    }, {
        name: "New York"
    }, {
        name: "Los Angeles"
    }],
    links: [{
        source: 0,
        target: 1
    }, {
        source: 0,
        target: 2
    }, {
        source: 3,
        target: 4
    }, {
        source: 3,
        target: 5
    }, ]
};

var node;
var link;
var force;
var width = 400,
    height = 400;

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



draw();

function draw() {

    force = d3.layout.force()
        .size([width, height])
        .nodes(data.nodes)
        .links(data.links)
        .alpha(0.01)
        .charge(-400)
        .friction(0.5)
        .linkDistance(100)
        .on('tick', forceLayoutTick);

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

    node = svg.selectAll('.node')
        .data(data.nodes, function(d) {
            return d.name;
        });

    node.exit().remove();

    var nodeEnter = node.enter()
        .append('g')
        .attr('class', 'node')
        .on("click", deleteNodeOnClick)
        //.attr('r', 8)
        //.attr('cx', function(d, i){ return (i+1)*(width/4); })
        //.attr('cy', function(d, i){ return height/2; })
        .call(force.drag);

    nodeEnter
        .append("circle")
        .attr("cx", 0)
        .attr("cy", 0)
        .attr("r", 10)
        .style("fill", "purple");

    nodeEnter
        .append("text")
        .text(function(d) {
            return d.name
        })
        .attr("class", "label")
        .attr("dx", 0)
        .attr("dy", ".35em");

    force.start();


    function forceLayoutTick() {

        node.attr("transform", function(d) {

            // Keep in bounding box
            d.x = Math.max(10, Math.min(width - 10, d.x));
            d.y = Math.max(10, Math.min(height - 10, d.y));

            return "translate(" + d.x + "," + d.y + ")";
        });

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

};

function deleteNodeOnClick(d) {
    data.nodes = data.nodes.filter(function(e) {
        return e.name !== d.name;
    });
    draw();
}
#graph svg {
  background-color: #CCC;
}

.link {
  stroke-width: 2px;
  stroke: black;
}

.node {
  background-color: darkslategray;
  stroke: #138;
  width: 10px;
  height: 10px;
  stroke-width: 1.5px;
}

.node text {
  pointer-events: none;
  font: 10px sans-serif;
}

.label {
  display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<body></body>

Of course, as I said, that is just to give you the general idea: for instance, the click doesn't remove the links. But now you know how to put it to work.

Upvotes: 3

Mark Schultheiss
Mark Schultheiss

Reputation: 34168

Your function does not do anything to the graph: (just the data)

function deleteNodeOnClick(d){
    var dataBefore = JSON.parse(JSON.stringify(data.nodes));
    // Just delete the first node, for demonstration purposes
    data.nodes.splice(0, 1);
    console.info("Node should be removed", dataBefore, data.nodes);
}

re-render or remove from the graph...

Example, just to remove (specific) "dot"

function deleteNodeOnClick(d) {
  var theElement = node.filter(function(da, i) {
      if (i === d.index) {
        console.log(da);
        return true;
      }
    })
  console.dir(theElement);
  theElement.remove();
  var dataBefore = JSON.parse(JSON.stringify(data.nodes));
  // Just delete the first node, for demonstration purposes
  data.nodes.splice(d.index, 1);
  console.info("Node should be removed", dataBefore, data.nodes);
}

Note that does NOT remove the "line" connecting the OTHER node, I will leave that exercise up to you to do.

More compact version without all the logging etc.

function deleteNodeOnClick(d) {
   d3.select(node[0][d.index]).remove();
   data.nodes.splice(d.index, 1);
 }

(added for OTHERS visiting this site) For d3 version 4 this would be

function deleteNodeOnClick(d) {
   d3.select(node._groups[0][d.index]).remove();
   data.nodes.splice(d.index, 1);
 }

Upvotes: 0

Related Questions