Collapsible D3 force directed graph with non-tree

2019-02-19 09:12发布

问题:

I have a D3 force directed graph using non-tree data and ID associations vs index. I cannot seem to find an example of this structure of data in a collapsible force layout. Basically, when you click a node, the data for that node should collapse/expand like this example: http://bl.ocks.org/mbostock/1062288. The last answer to this questions got close but is linking nodes by index rather than id: How to create d3.js Collapsible force layout with non tree data?

Here is a fiddle of my code https://jsfiddle.net/5w86q4Lm/

Below is the code I have so far

  var width = 960,
  height = 500;

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

d3.json("js/graph.json", function(error, json) {
  if (error) throw error;

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

  var link = svg.append("g").selectAll("path")
    .data(edges)
    .enter().append("path")
    .attr("class", "link")
    .attr("marker-end", "url(#end)");

  var node = svg.selectAll(".node")
    .data(json.nodes)
    .enter().append("g")
    .attr("class", function(d) {
      return "node " + d.type
    });

  force
    .nodes(json.nodes)
    .links(edges)
    .start();

  node.append("circle")
    .attr("class", "circle")
    .attr("r", function(d) {
      d.radius = 30;
      return d.radius
    }); // 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");
  });

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

And an example of my JSON

{  
   "nodes":[  
      {  
         "id":223,
         "type":"Parent",
         "properties":{  

         }
      },
      {  
         "id":136525,
         "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"
         }
      }
   ],
   "edges":[  
      {  
         "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":223,
         "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":{  

         }
      }
   ]
}

回答1:

The technique from the linked answer can be applied to your own code without major changes, because in both cases you can access the source and target property from each link, which are what the click function which controls the collapsing depends on.

Here is a working fiddle which makes the following changes to your code:

  • move the code for defining and adding nodes and links into an update method so it can be called multiple times, like the linked answer
  • copy the code from the linked answer for initialising the collapsing/collapsed properties and for filtering the nodes and links before reinitialising the graph
  • copy the click method for handling the collapsing, but I modified this to recursively handle multiple levels of child nodes (I also modified your test data to test this case)

Code:

var width = 960,
  height = 500;

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

  for(var i = 0; i < json.nodes.length; i++) {
    json.nodes[i].collapsing = 0;
    json.nodes[i].collapsed = false;
  }

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

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

  link = link.data(links)

  link.exit().remove();

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

  node = 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
    }); // 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);

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