Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

D3: Multiple force layouts in one SVG?

I have seen solutions for creating multiple force layouts on a single page, where each layout is contained in its own SVG; however, I have been unable to find help on how to include multiple force layouts within a single SVG. Each layout has its own data associated with it.

An example of what I am currently doing can be found at http://jsfiddle.net/connorgr/SRAJa/. I have included the key part of the code below. The end result looks an awful lot like the force layout was never activated (or deleted) for all but the last node/link data. Is there any way to prevent this from happening?

I am unable to merge the data together and use only one layout because of the use case for the visualization I'm building.

/**
  * Creates a force layout in svgObj for each element in graphs
  * (svg) svgObj - The SVG to include the force layouts in
  * (json) graphs - An array of {"nodes":[...],"links":[...]} objects
  */
function generateMultiForce(svgObj, graphs) {
  for(var i=0; i < graphs.length; i++) {
    var graph = graphs[i];

    var graphArea = svgObj.append("g");

    var force = d3.layout.force()
        .charge(-200)
        .linkDistance(45)
        .size([width, height])
        .nodes(graph.nodes)
        .links(graph.links)
        .start();

    var link = graphArea.selectAll(".link")
        .data(graph.links)
        .enter().append("line")
          .attr("class", "link");

    var nodeGroup = graphArea.selectAll("g")
        .data(graph.nodes)
        .enter().append("g")
        .call(force.drag);

    var node = nodeGroup.append("circle")
        .attr("class", "node")
        .attr("r", 5)
        .style("fill", function(d) {
          return color(d.group); });

    var text = nodeGroup.append("text")
        .attr("x", 8)
        .attr("y", ".31em")
        .text(function(d) { return d.name; });

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

      nodeGroup.attr("transform", function(d) {
          return "translate("+d.x+","+d.y+")";
        });
    });
  }
}
like image 664
Connor Gramazio Avatar asked May 01 '13 13:05

Connor Gramazio


2 Answers

The following approach restricts force, node, and link data to their corresponding force-directed layouts. In this way, you can add as many layouts as you want to the same SVG without causing inter-node interference. Each layout may be formatted individually. If you do end up wanting the layouts to influence one another, you can edit their respective tick functions.

function layout1(inputNodes, inputLinks) {
   var force = d3.layout.force();
   var nodes = force.nodes();
   var links = force.links();

   var update = function() {
      //append nodes and links from data

      force.on("tick",function(e){
         //tick movement

      }
   }
   for(var i=0; i<inputNodes.length; i++){
      nodes.push(inputNodes[i]);
   }
   for(var i=0; i<inputLinks.length; i++){
      links.push(inputLinks[i]);
   }
   update();
}

Now the second force-directed layout can have an identical structure, and the same variable names as well:

function layout2(inputNodes, inputLinks) {
   var force = d3.layout.force();
   var nodes = force.nodes();
   var links = force.links();

   var update = function() {
      //append nodes and links from data

      force.on("tick",function(e){
         //tick movement

      }
   }
   for(var i=0; i<inputNodes.length; i++){
      nodes.push(inputNodes[i]);
   }
   for(var i=0; i<inputLinks.length; i++){
      links.push(inputLinks[i]);
   }
   update();
}

Finally instantiate with whatever data:

var layout1 = new layout1(inputNodes, inputLinks);
var layout2 = new layout2(inputNodes, inputLinks);

This method can be adopted to creating multiple layouts on the fly. Hope that is close to what you're looking for.

like image 67
interpolack Avatar answered Oct 15 '22 10:10

interpolack


You can write the generateMultiForce function as a jquery plugin - this seems to keep the graphs independent and apply force layout to both:

var width = 600,
    height = 600;

var color = d3.scale.category20();

var data = [
  {
    "nodes": [
      {"name": "Hello"},
      {"name": "World"}
    ],
    "links": [
      {"source": 0, "target": 1, "value": 0.5}
    ]
  },
  {
    "nodes": [
      {"name": "Zero"},
      {"name": "One"},
      {"name": "Two"}
    ],
    "links": [
      {"source": 0, "target": 1, "value": 0.25},
      {"source": 1, "target": 2, "value": 0.5},
      {"source": 2, "target": 0, "value": 0.25}
    ]
  }
];

(function( $ ) {
    $.fn.generateMultiForce = function(svgObj) {

        return this.each(function() {
            var graph = this;
            var graphArea = svgObj.append("g");

            var force = d3.layout.force()
                .charge(-200)
                .linkDistance(45)
                .size([width, height])
                .nodes(graph.nodes)
                .links(graph.links)
                .start();

            var link = graphArea.selectAll(".link")
                .data(graph.links)
                .enter().append("line")
                .attr("class", "link");

            var nodeGroup = graphArea.selectAll("g")
                .data(graph.nodes)
                .enter().append("g")
                .call(force.drag);

            var node = nodeGroup.append("circle")
                .attr("class", "node")
                .attr("r", 5)
                .style("fill", function(d) {
                  return color(d.group); });

            var text = nodeGroup.append("text")
                .attr("x", 8)
                .attr("y", ".31em")
                .text(function(d) { return d.name; });

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

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

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

$(data).generateMultiForce(svgTest);
like image 2
Emma Lewis Avatar answered Oct 15 '22 12:10

Emma Lewis