2

I'm attempting to put multiple D3 force layouts on a page at the same time. The number of force layouts is ideally variable, depending on the number of roots returned from a dynamic API. I have followed the answers on this question regarding multiple force layouts and have successfully put each layout in a separate div, in a separate svg.

However, the issue is twofold:

1) The svgs seem to be drawn at the same time, causing conflicts in the alpha cooling parameter (on "tick" of each graph). Thus, the only layout that is positioned the way it is intended is the last svg drawn on the page. The tick function contains code that shapes the force layout similar to a weeping willow tree, with the root node sitting on top and the children falling below it.

2) Setting a loop to iterate on the full results list from the API causes D3 to crash, and an error "Uncaught TypeError: Cannot read property 'textContent' of null."

I think the ideal solution would be to draw each force layout after the previous one has been successfully rendered, in a way that does not cause the alpha cooling parameters (on "tick") to conflict, or overloading the D3 library with too many instances of the force layout at once. Does someone have insight into this issue? Here is my code:

/* ... GET THE RESULTS FROM THE API ...*/
function handleRequest2(json) {
        allroots = json[1]['data']['children'];
        (function() {
            var index = 0;
            function LoopThrough() {
                currentRoot = allroots[index];  
                if (index < allroots.length) {
                    /* DRAW THE GRAPH */
                    draw_graphs(currentRoot, index);  
                    ++index;
                    LoopThrough();
                    };
            }

        LoopThrough();
        })(); 

    }
//Force Layout Code
function draw_graphs(root, id) {
var root_id = "map-" + id.toString();
var force;
var vis;
var link;
var node;
var w = 980;
var h = 1000;
var k = 0;
// Create a separate div to house each SVG graph
  div = document.createElement("div");
  div.style.width = "980px";
  div.style.height = "1000px";
  div.style.cssFloat="left";
  div.id = root_id;
  $(div).addClass("chattermap-map");
  // Append the div to the chart container
  $('#chart').append(div);

force = d3.layout.force()
  .size([w, h])
  .charge(-250)
  .gravity(0)
  .on("tick", tick);
  // Create the SVG and append it to the created div
vis = d3.select("#"+root_id)
  .append("svg:svg")
  .attr("width", w)
  .attr("height", h)
  .attr("id",root_id);


  // Put the Reddit JSON in the correct format for the Force Layout
nodes = flatten(root),
links = optimize(d3.layout.tree().links(nodes));
// Calculations for the sizing of the nodes
avgNetPositive = getAvgNetPositive();
maxNetPositive = d3.max(netPositiveArray);
minNetPositive = d3.min(netPositiveArray);
// Create a logarithmic scale that sizes the nodes
radius = d3.scale.pow().exponent(.3).domain([minNetPositive,maxNetPositive]).range([5,30]);
// Fix the root node to the top of the svg
root.data.fixed = true;
root.data.x = w/2;
root.data.y = 50;
  // Start the force layout.
force
  .nodes(nodes)
  .links(links)
  .start();
  // Update the links
  link = vis.selectAll("line.link")
    .data(links, function(d) { return d.target.id; });
  // Enter any new links.
  link.enter().insert("svg:line", ".node")
    .attr("class", "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; });
  // Exit any old links.
  link.exit().remove();
  // Update the nodes
  node = vis.selectAll("circle.node")
    .data(nodes, function(d) {return d.id; })
    .style("fill", function(d) {
      return '#2960b5';
    });
    // Enter any new nodes.
  node.enter().append("svg:circle")
    .attr("class", "node")
    .attr("cx", function(d) {return d.x; })
    .attr("cy", function(d) {return d.y; })
    .attr("r", function(d) {
        //Get the net positive reaction
        var netPositive = d.ups - d.downs;
        var relativePositivity = netPositive/avgNetPositive;
        //Scale the radii based on the logarithmic scale defined earlier
        return radius(netPositive);
    })
    .style("fill", function(d) {
      return '#2960b5';
    })
    // Allow dragging on click
    .call(force.drag);
    // Exit any old nodes.
  node.exit().remove();
  //This will add the name of the author to the node HTML
  node.append("author").text(function(d) {return d.author});
  //Add the body of the comment to the node
  node.append("comment").text(function(d) {return Encoder.htmlDecode(d.body_html)}); 
  //Add the UNIX timestamp to the node
  node.append("timestamp").text(function(d) {return moment.unix(d.created_utc).fromNow();})
  //On load, assign the root node to the tooltip
  numberOfNodes = node[0].length;
  rootNode = d3.select(node[0][parseInt(numberOfNodes) - 1]);
  rootNodeComment = rootNode.select("comment").text();
  rootNodeAuthor = rootNode.select("author").text();
  rootNodeTimestamp = rootNode.select("timestamp").text();

  // Create the tooltip div for the comments
tooltip_div = d3.select("#"+root_id).append("div")
    .attr("class", "tooltip")               
      .style("opacity", 1);
//Add the HTML to the tooltip for the root
  tooltip_div .html("<span class='commentAuthor'>" + rootNodeAuthor + "</span><span class='bulletTimeAgo'>&bull;</span><span class='timestamp'>" + rootNodeTimestamp + "</span><br>" + rootNodeComment)
    //Position the tooltip based on the position of the current node, and it's size
    .style("left", (rootNode.attr("cx") - (-rootNode.attr("r")) - (-9)) + "px")   
    .style("top", (rootNode.attr("cy") - 15)  + "px");    

  node.on("mouseover", function() {
    currentNode = d3.select(this);
    currentTitle = currentNode.select("comment").text();
    currentAuthor = currentNode.select("author").text();
    currentTimestamp = currentNode.select("timestamp").text();
  tooltip_div.transition()        
     .duration(200)      
     .style("opacity", 1);
  // Add the HTML for all other tooltips on mouseover
  tooltip_div .html("<span class='commentAuthor'>" + currentAuthor + "</span><span class='bulletTimeAgo'>&bull;</span><span class='timestamp'>" + currentTimestamp + "</span><br>" + currentTitle)
              //Position the tooltip based on the position of the current node, and it's size
              .style("left", (currentNode.attr("cx") - (-currentNode.attr("r")) - (-9)) + "px")   
              .style("top", (currentNode.attr("cy") - 15)  + "px");    
   });

  // Fade out the tooltip on mouseout
  node.on("mouseout", function(d) {       
      tooltip_div.transition()        
          .duration(500)      
          .style("opacity", 1);
  });
  // Optimize the JSON output of Reddit for D3
  function flatten(root) {

    var nodes = [], i = 0, j = 0;
    function recurse(node) {

      if (node['data']['replies'] != "" && node['kind'] != "more") {
          node['data']['replies']['data']['children'].forEach(recurse);
      }
      if (node['kind'] !="more") {
          //Add an ID value to the node starting at 1
          node.data.id = ++i;
          node.data.name = node.data.body;
          //Put the replies in the key 'children' to work with the tree layout
          if (node.data.replies != "") {

               node.data.children = node.data.replies.data.children;
               //Remove the extra 'data' layer for each child
               for (j=0; j < node.data.children.length; j++) {
                  node.data.children[j] = node.data.children[j].data;
               }

          } else {
              node.data.children = "";
          }
          var comment = node.data;
          nodes.push(comment);
      }
    }
    recurse(root);
    return nodes;  
  }
  // Optimize the JSON for use with Links
  function optimize(linkArray) {
      optimizedArray = [];
      for (k=0; k < linkArray.length; k++) {
          if(typeof linkArray[k].target.count == 'undefined') {
              optimizedArray.push(linkArray[k]);
          }
      }
      return optimizedArray;
  }
  // Get the average net positive upvotes for use in sizing
  function getAvgNetPositive() {
    var sum = 0;
    netPositiveArray = []
    //Select all the nodes
    var allNodes = d3.selectAll(nodes)[0];
    //For each node, get the net positive votes and add it to the sum
    for (i=0; i < allNodes.length; i++) {
      var netPositiveEach = allNodes[i]["ups"] - allNodes[i]["downs"];
      sum += netPositiveEach;
      netPositiveArray.push(netPositiveEach);
    }
    var avgNetPositive = sum/allNodes.length;
    return avgNetPositive;
  }
  function tick(e) {
     var kx = .4 * e.alpha, ky = 1.4 * e.alpha;
     links.forEach(function(d, i) {
        d.target.x += (d.source.x - d.target.x) * kx;
        d.target.y += (d.source.y + 80 - d.target.y) * ky;
    });
    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; });

    node.attr("cx", function(d) { return d.x; })
        .attr("cy", function(d) { return d.y; });
  }
  // // Remove the animation effect of the force layout
  // while ((force.alpha() > 1e-2) && (k < 150)) {
  //     force.tick(),
  //     k = k + 1;
  // }
}

Thanks in advance!

Community
  • 1
  • 1
Jeff E
  • 181
  • 11

1 Answers1

1

You should be able to make several force layouts work at the same time if you encapsulate them in their own namespace, e.g. through separate functions. You can however also do what you want by listening to the end event -- see the documentation. This way, you can "chain" the layouts, starting each one once the previous has finished.

Regarding the other error, it looks like this would be caused by incomplete/faulty data.

Lars Kotthoff
  • 107,425
  • 16
  • 204
  • 204
  • Lars, encapsulating each layout in its own namespace/function is exactly what I am aiming to accomplish with the code posted here. Each layout is placed in it's own SVG in a separate div with a unique ID. Is there anything you see in the code that would suggest that the namespacing/separation is not adequate? – Jeff E Oct 14 '13 at 13:23
  • I guess the problem is that the force layout doesn't allow you to specify a namespace for the `tick` event. You could have just a single `tick` handler that updates all graphs. – Lars Kotthoff Oct 14 '13 at 13:33
  • Thanks Lars. Could you point me in the right direction towards writing a function like that? – Jeff E Oct 14 '13 at 14:14
  • All you need to do is put the contents of your current tick handlers into a single function. Obviously this may require some refactoring with your current approach. – Lars Kotthoff Oct 14 '13 at 14:42