简体   繁体   中英

Adding and removing nodes and links from force diagram in d3 based on filter dropdown

I'm trying to make a force diagram, with a couple of drop down boxes which filter the data on display. The first one (which is where I'm almost at now) checks for the type, and only shows nodes & links which have a source or target matching the type.

What I have now, is the ability to select the filter, and the graph updates, it removes unnecessary nodes, and reformats the remaining ones to be correct. But it only works the first time. If I 're-filter' it starts to go haywire.

Here's my full code, I'm very new to javascript (&d3), and I've been unashamedly stealing from bl.ocks.org, so please feel free to answer in 'noob'. Thanks in advance.

Also, I've put this on a jsfiddle: http://jsfiddle.net/J85Vu/

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
    <title>Enterprise Collaboration Map</title>
    <script type="text/javascript" src="d3.v3.js"></script>
    <script type="text/javascript" src="jquery-1.10.2.min.js"></script>

    <style type="text/css">

        path.link {
          fill: none;
          stroke: #666;
          stroke-width: 1.5px;
        }

        marker#a {
          fill: green;
        }
        path.link.a {
          stroke: green;
        }
        circle.a {
          fill: green;
          stroke: #333;
          stroke-width: 1.5px;
        }
        marker#b {
          fill: blue;
        }
        path.link.b {
          stroke: blue;
        }
        circle.b {
          fill: blue;
          stroke: #333;
          stroke-width: 1.5px;
        }
        marker#c {
          fill: orange;
        }
        path.link.c {
          stroke: orange;
        }
        circle.c {
          fill: orange;
          stroke: #333;
          stroke-width: 1.5px;
        }

        circle {
          fill: #ccc;
          stroke: #333;
          stroke-width: 1.5px;
        }


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

        text.shadow {
          stroke: #fff;
          stroke-width: 3px;
          stroke-opacity: .8;
        }

    </style>
  </head>
  <body>
    <select class="BU">
        <option value="a">A</option>
        <option value="b">B</option>
        <option value="c">C</option>
    </select>


    <script type="text/javascript">


        var links = [
        {source:"one",target:"two", type:"a", typeKBP:"a"},
        {source:"two",target:"three", type:"a", typeKBP:"a"},
        {source:"three",target:"four", type:"a", typeKBP:"a"},
        {source:"four",target:"five", type:"a", typeKBP:"b"},
        {source:"five",target:"six", type:"b", typeKBP:"b"},
        {source:"six",target:"seven", type:"b", typeKBP:"b"},
        {source:"seven",target:"eight", type:"b", typeKBP:"b"},
        {source:"eight",target:"nine", type:"b", typeKBP:"c"},
        {source:"nine",target:"ten", type:"c", typeKBP:"c"},
        {source:"ten",target:"one", type:"c", typeKBP:"a"},
        {source:"one",target:"three", type:"a", typeKBP:"a"},
        {source:"two",target:"four", type:"a", typeKBP:"a"},
        {source:"three",target:"five", type:"a", typeKBP:"b"},
        {source:"four",target:"six", type:"a", typeKBP:"b"},
        {source:"five",target:"seven", type:"b", typeKBP:"b"},
        {source:"six",target:"eight", type:"b", typeKBP:"b"},
        {source:"seven",target:"nine", type:"b", typeKBP:"c"},
        {source:"eight",target:"ten", type:"b", typeKBP:"c"},
        {source:"nine",target:"one", type:"c", typeKBP:"a"},
        {source:"ten",target:"two", type:"c", typeKBP:"a"},
        {source:"one",target:"four", type:"a", typeKBP:"a"},
        {source:"two",target:"five", type:"a", typeKBP:"b"},
        {source:"three",target:"six", type:"a", typeKBP:"b"},
        {source:"four",target:"seven", type:"a", typeKBP:"b"},
        {source:"five",target:"eight", type:"b", typeKBP:"b"},
        {source:"six",target:"nine", type:"b", typeKBP:"c"},
        {source:"seven",target:"ten", type:"b", typeKBP:"c"},
        {source:"eight",target:"one", type:"b", typeKBP:"a"},
        {source:"nine",target:"two", type:"c", typeKBP:"a"},
        {source:"ten",target:"three", type:"c", typeKBP:"a"}
        ];

        var inputlinks=[];
        var nodes = {};

        inputlinks.push(links);
        // Compute the distinct nodes from the links.
        links.forEach(function(link) {
          link.source = nodes[link.source] || (nodes[link.source] = {name: link.source, type:link.type});
          link.target = nodes[link.target] || (nodes[link.target] = {name: link.target, type:link.typeKBP});
        });

        var w = 1024,
            h = 800;
        //setup initial force layout
        var force = d3.layout.force()
            .gravity(0.4)
            .size([w, h])
            .nodes(d3.values(nodes))
            .links(links)
            .linkDistance(100)
            .charge(-1000)
            .on("tick", tick)
            .start();

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

        // Per-type markers, as they don't inherit styles.
        svg.append("svg:defs").selectAll("marker")
            .data(["a","b","c"])
          .enter().append("svg:marker")
            .attr("id", String)
            .attr("viewBox", "0 -5 10 10")
            .attr("refX", 15)
            .attr("refY", -1.5)
            .attr("markerWidth", 6)
            .attr("markerHeight", 6)
            .attr("orient", "auto")
          .append("svg:path")
            .attr("d", "M0,-5L10,0L0,5");

        var path = svg.append("svg:g").selectAll("path")
            .data(force.links())
          .enter().append("svg:path")
            .attr("class", function(d) { return "link " + d.type; })
            .attr("marker-end", function(d) { return "url(#" + d.type + ")"; });

        var circle = svg.append("svg:g").selectAll("circle")
            .data(force.nodes())
          .enter().append("svg:circle")
            .attr("r", 6)
            .attr("class", function(d) { return d.type; })
            .call(force.drag);

        var text = svg.append("svg:g").selectAll("g")
            .data(force.nodes())
          .enter().append("svg:g");

        // A copy of the text with a thick white stroke for legibility.
         text.append("svg:text")
            .attr("x", 8)
            .attr("y", ".31em")
            .attr("class", "shadow")
            .text(function(d) { return d.name; });

         text.append("svg:text")
            .attr("x", 8)
            .attr("y", ".31em")
            .attr("class","write")
            .text(function(d) { return d.name; });

        //jQuery update parts for drop downs.
        $(document).ready(function(){
            $('.BU').on('change',function(){
                curBU=$('.BU').val();
                //alert('The selected BU is ' + curBU);

                //Filter links and rebuild nodes based on this.
                minLinks={};
                minLinks=links.filter(function(d){
                    if ((d.type==curBU) || (d.typeKBP==curBU)) {
                        return d
                    }
                })
                //new nodes
                nodes2={};
                nodes2=force.nodes().filter(function(d){return d3.keys(minLinks.filter(function(e){return e.source.name==d.name || e.target.name==d.name;})).length>0});

                // minLinks.forEach(function(d) {
                    // d.source = nodes2[d.source] || (nodes2[d.source] = {name: d.source, type:d.type});
                    // d.target = nodes2[d.target] || (nodes2[d.target] = {name: d.target, type:d.typeKBP});
                // });

                force
                    .nodes(nodes2)
                    .links(minLinks)
                    .start();

                //circle.remove();
                newCirc=circle.data(force.nodes());
                newCirc.enter().append("svg:circle")
                    .attr("r", 6)
                    .attr("class", function(d) { return d.type; })
                    .call(force.drag);
                newCirc.exit().remove();

                newPath=path.data(force.links());
                newPath
                    .enter().append("svg:path")
                    .attr("class", function(d) { return "link " + d.type; })
                    .attr("marker-end", function(d) { return "url(#" + d.type + ")"; });
                newPath.exit().remove();

                newText=text.data(force.nodes());
                newText.exit().remove();
                newText.select(".shadow").text(function(d){return d.name;});
                newText.select(".write").text(function(d){return d.name;});

            });

        });

        // Use elliptical arc path segments to doubly-encode directionality.
        function tick() {
          path.attr("d", function(d) {
            var dx = d.target.x - d.source.x,
                dy = d.target.y - d.source.y,
                dr = Math.sqrt((dx * dx)/2 + (dy * dy));
            return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
          });

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

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


    </script>
  </body>
</html>

In your update code you should be reselecting the existing nodes before doing a data bind with the new data. In the current code you're using the variables circle and path which are actually referring to the newly appended nodes of the enter selection.

Reselecting before every data join is the best way to ensure you are joining with the very latest actual state in the DOM:

svg.selectAll("path")
    .data(force.links());

svg.selectAll("circle")
    .data(force.nodes());

It is probably a good idea for you to class your circles and paths in some way that will let you select for them more directly so you don't accidentally pick up other paths or circles in the svg.

Also, be careful about operating on the enter selection versus the update selection. This is especially true considering that you are not defining a key for your data join, which means that index will be used by default resulting in existing nodes being updated with new data. In your code, for example, you're only setting the class attribute on the newly appended nodes where you probably want to update it on all nodes.

This tutorial is a good starting point for understanding this better: Thinking with Joins .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM