d3.jsgraphforce-layout

Prevent D3 adding duplicates on update


The below is a "working" D3 animation.

Child nodes successfully disappear on mouse-click... HOWEVER, duplicates nodes ("circles") are added. If you play around with running the following code, then collapsing and opening nodes you will see nodes appearing on top of other ones!

The duplication of circle-elements can be seen in chrome-inspection too.

Just simple collapse and open is what we're after here. Your help is much appreciated ! Thank you.

(ignore the node labels - they're not important)

<html>
<head>
<style>
    .node {
        cursor: pointer;
        font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
        font-weight: 300;
    }

    .node .text {
        fill: white;
    }

    .ORG .circle {
        fill: #1d3649;
    }

    .EMR .circle {
        fill: #B2D0F5;
        stroke: #5596e6;
        stroke-dasharray: 3px, 3px;
        opacity: .5;
    }

    .EMR .circle:hover {
        fill: #5596e6;
    }

    .link {
        fill: none;
        stroke: #eee;
        stroke-width: 1.5px;
        font: 10px sans-serif;
    }

    .link.active {
        stroke: #ddd;
        stroke-width: 2;
    }

    .arrow {
        fill: #666;
    }

    .arrow.active {
        stroke-width: 0 !important;
    }

</style>
</head>
<body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
    var dataset = {
        "nodes": [{
            "id": 223,
            "type": "Parent",
            "properties": {

            }
        }, {
            "id": 136525,
            "type": "Child",
            "properties": {
                "patient": "6090",
                "batch": "70"
            }
        }, {
            "id": 146525,
            "type": "Child",
            "properties": {
                "patient": "6090",
                "batch": "70"
            }
        }, {
            "id": 156525,
            "type": "Child",
            "properties": {
                "patient": "6090",
                "batch": "70"
            }
        }, {
            "id": 166525,
            "type": "Child",
            "properties": {
                "patient": "6090",
                "batch": "70"
            }
        }, {
            "id": 176525,
            "type": "Child",
            "properties": {
                "patient": "6090",
                "batch": "70"
            }
        }, {
            "id": 136448,
            "type": "Child",
            "properties": {
                "patient": "6094",
                "batch": "70"
            }
        }, {
            "id": 136328,
            "type": "Child",
            "properties": {
                "patient": "6082",
                "batch": "70"
            }
        }, {
            "id": 136305,
            "type": "Child",
            "properties": {
                "patient": "6096",
                "batch": "70"
            }
        }, {
            "id": 136303,
            "type": "Child",
            "properties": {
                "patient": "6093",
                "batch": "70"
            }
        }, {
            "id": 136299,
            "type": "Child",
            "properties": {
                "patient": "6091",
                "batch": "70"
            }
        }, {
            "id": 136261,
            "type": "Child",
            "properties": {
                "patient": "6089",
                "batch": "70"
            }
        }, {
            "id": 136212,
            "type": "Child",
            "properties": {
                "patient": "6087",
                "batch": "70"
            }
        }, {
            "id": 136115,
            "type": "Child",
            "properties": {
                "patient": "6078",
                "batch": "70"
            }
        }, {
            "id": 136113,
            "type": "Child",
            "properties": {
                "patient": "6088",
                "batch": "70"
            }
        }, {
            "id": 135843,
            "type": "Child",
            "properties": {
                "patient": "6072",
                "batch": "70"
            }
        }, {
            "id": 555,
            "type": "Grandchild",
            "properties": {

            }
        }],
        "edges": [{
            "id": 0,
            "from": 136113,
            "to": 555,
            "properties": {

            }

        },{
            "id": 0,
            "from": 136525,
            "to": 555,
            "properties": {

            }
        },{
            "id": 0,
            "from": 146525,
            "to": 555,
            "properties": {

            }
        },{
            "id": 0,
            "from": 156525,
            "to": 555,
            "properties": {

            }
        },{
            "id": 0,
            "from": 166525,
            "to": 136448,
            "properties": {

            }
        },{
            "id": 0,
            "from": 176525,
            "to": 223,
            "properties": {

            }
        },{
            "id": 0,
            "from": 223,
            "to": 136525,
            "properties": {

            }
        }, {
            "id": 0,
            "from": 223,
            "to": 136448,
            "properties": {

            }
        }, {
            "id": 0,
            "from": 223,
            "to": 136328,
            "properties": {

            }
        }, {
            "id": 0,
            "from": 223,
            "to": 136305,
            "properties": {

            }
        }, {
            "id": 0,
            "from": 136525,
            "to": 136303,
            "properties": {

            }
        }, {
            "id": 0,
            "from": 223,
            "to": 136299,
            "properties": {

            }
        }, {
            "id": 0,
            "from": 223,
            "to": 136261,
            "properties": {

            }
        }, {
            "id": 0,
            "from": 223,
            "to": 136212,
            "properties": {

            }
        }, {
            "id": 0,
            "from": 223,
            "to": 136115,
            "properties": {

            }
        }, {
            "id": 0,
            "from": 223,
            "to": 136113,
            "properties": {

            }
        }, {
            "id": 0,
            "from": 223,
            "to": 135843,
            "properties": {

            }
        }]
    }

    var width = 0.975 * $(window).width(),
        height = 0.95 * $(window).height();

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

    var force = d3.layout.force()
        .size([width, height])
        //gravity(0.2)
        .linkDistance(height / 6)
        .charge(function(node) {
            if (node.type !== 'ORG') return -2000;
            return -30;
        });

    // build the arrow.
    svg.append("svg:defs").selectAll("marker")
        .data(["end"]) // Different link/path types can be defined here
        .enter().append("svg:marker") // This section adds in the arrows
        .attr("id", function(d) {
            return d;
        })
        .attr("viewBox", "0 -5 10 10")
        .attr("refX", 12)
        .attr("refY", 0)
        .attr("markerWidth", 9)
        .attr("markerHeight", 5)
        .attr("orient", "auto")
        .attr("class", "arrow")
        .append("svg:path")
        .attr("d", "M0,-5L10,0L0,5");

    var json = dataset;

    var edges = [];
    json.edges.forEach(function(e) {
        var sourceNode = json.nodes.filter(function(n) {
                return n.id === e.from;
            })[0],
            targetNode = json.nodes.filter(function(n) {
                return n.id === e.to;
            })[0];

        edges.push({
            source: sourceNode,
            target: targetNode,
            value: e.id
        });
    });

    var colors = {};
    colors[23] = "lightblue";
    colors[25] = "lightgreen";
    colors[48] = "lightyellow";
    colors[28] = "lightblue";
    colors[5] = "lightgreen";
    colors[3] = "lightyellow";
    colors[99] = "lightblue";
    colors[61] = "lightgreen";
    colors[12] = "lightyellow";

    for(var i=0; i<json.nodes.length; i++) {
        json.nodes[i].collapsing = 0;
        json.nodes[i].collapsed = false;
        json.nodes[i].radius = json.nodes[i].id % 100;
        if(colors[json.nodes[i].radius] != undefined)
            json.nodes[i].color = colors[json.nodes[i].radius];
        else
            json.nodes[i].color = "lightbrown";
    }

    var link = svg.selectAll(".link");
    var node = svg.selectAll(".node");

    force.on("tick", function() {
        // make sure the nodes do not overlap the arrows
        link.attr("d", function(d) {
            // Total difference in x and y from source to target
            diffX = d.target.x - d.source.x;
            diffY = d.target.y - d.source.y;

            // Length of path from center of source node to center of target node
            pathLength = Math.sqrt((diffX * diffX) + (diffY * diffY));

            // x and y distances from center to outside edge of target node
            offsetX = (diffX * d.target.radius) / pathLength;
            offsetY = (diffY * d.target.radius) / pathLength;

            return "M" + d.source.x + "," + d.source.y + "L" + (d.target.x - offsetX) + "," + (d.target.y - offsetY);
        });

        node.attr("transform", function(d) {
            return "translate(" + d.x + "," + d.y + ")";
        });
    });

    update();

    function update(){
        var nodes = json.nodes.filter(function(d) {
            return d.collapsing == 0;
        });

        var links = edges.filter(function(d) {
            return d.source.collapsing == 0 && d.target.collapsing == 0;
        });

        link = svg.selectAll(".link").data(links);
        link.exit().remove();

        link.enter().append("path")
            .attr("class", "link")
            .attr("marker-end", "url(#end)");

        node = svg.selectAll(".node").data(nodes);
        node.exit().remove();

        node.enter().append("g")
            .attr("class", function(d) {
                return "node " + d.type
            });

        node.append("circle")
            .attr("class", "circle")
            .attr("r", function(d) {
                //d.radius = 30;
                return d.radius
            })
            .attr("fill", function(d) {
                //d.radius = 30;
                return d.color;
            })
            .attr("stroke", function(d) {
                //d.radius = 30;
                return "darkgray";
            });
         // return a radius for path to use



        node.append("text")
            .attr("x", 0)
            .attr("dy", ".35em")
            .attr("text-anchor", "middle")
            .attr("class", "text")
            .text(function(d) {
                return d.type
            });

        // On node hover, examine the links to see if their
        // source or target properties match the hovered node.
        node.on('mouseover', function(d) {
            link.attr('class', function(l) {
                if (d === l.source || d === l.target)
                    return "link active";
                else
                    return "link inactive";
            });
        });

        // Set the stroke width back to normal when mouse leaves the node.
        node.on('mouseout', function() {
            link.attr('class', "link");
        })
            .on('click', click);

        /** this is NOT the problem **/
        function click(d) {
            if (!d3.event.defaultPrevented) {
                var inc = d.collapsed ? -1 : 1;
                recurse(d);

                function recurse(sourceNode){
                    //check if link is from this node, and if so, collapse
                    edges.forEach(function(l) {
                        if (l.source.id === sourceNode.id){

                            l.target.collapsing += inc;
                            recurse(l.target);


                        }
                    });
                }
                d.collapsed = !d.collapsed;
            }
            update();
        }
        force
            .nodes(nodes)
            .links(links)
            .start();
    }

</script>

</body>
</html>

[code based on this: http://jsfiddle.net/sheilak/9wvmL8q8/


Solution

  • The problem is here:

    node.enter().append("g")...
    
    node.append("circle")...
    
    node.append("text")...
    

    Although you are adding the group to the enter selection, you're adding the circle and text to the node selection which is the transitioning elements

    try:

    var nodeEnter = node.enter().append("g")...
    
    nodeEnter.append("circle")...
    
    nodeEnter.append("text")...
    

    In this case, nodeEnter will be equal to the last created element in the chain, which in this case is the g element.