多个强制布局导致Tick函数发生冲突

时间:2013-10-14 04:27:45

标签: javascript d3.js force-layout

我正在尝试在页面上同时放置多个D3强制布局。力布局的数量理想地是可变的,这取决于从动态API返回的根的数量。我已经按照on this question regarding multiple force layouts的答案并成功地将每个布局放在一个单独的div中,放在一个单独的svg中。

然而,问题有两个:

1)svgs似乎同时被绘制,导致alpha冷却参数发生冲突(每个图形的“勾号”)。因此,唯一按照预期方式定位的布局是页面上绘制的最后一个svg。 tick函数包含用于形成力布局的代码,类似于垂柳树,根节点位于顶部,子节点位于其下方。

2)设置一个循环来迭代API中的完整结果列表会导致D3崩溃,并出现错误“Uncaught TypeError:无法读取属性'textContent'为null。”

我认为理想的解决方案是在成功渲染上一个力布局之后绘制每个力布局,其方式不会导致alpha冷却参数(“勾号”)发生冲突,或者使D3库过载一次有太多的力布局实例。有人有洞察这个问题吗?这是我的代码:

/* ... 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;
  // }
}

提前致谢!

1 个答案:

答案 0 :(得分:1)

如果将它们封装在自己的命名空间中,您应该能够使多个强制布局同时工作,例如:通过单独的功能。但是,您也可以通过收听end事件来执行您想要的操作 - 请参阅the documentation。通过这种方式,您可以“链接”布局,在前一个布局完成后启动每个布局。

关于其他错误,看起来这可能是由不完整/错误的数据引起的。