6

My goal is to use the d3 force layout to display two different networks that share the same nodes. For example, among four people, you could define a social network and a genealogy network; the nodes would be the same (people) but the links (relationships) could be different. Despite creating two separate force layouts, two separate svg canvases, and trying to define separate variables, the nodes are sharing x and y positional information. Here is a minimal example, in which dragging nodes on one network changes their positions in the other network: http://amath.colorado.edu/student/larremore/nodesSharingPositiond3

Below, I posted the function that is called to create one of the networks, and the code to create the other is very similar, but uses different variable names in all instances. The commented code for creating both networks can be found in http://amath.colorado.edu/student/larremore/nodesSharingPositiond3/lib/minimal.js and the script used to define variables can be found in /driver/minimalScript.js <-- I don't have enough reputation to link this directly. My apologies!

Somewhere in the way d3.force works, positional information is global or being selected globally, or something. Could anyone shed light on this? I am interested both in a solution to keeping the positional information separated as well as in understanding how d3.force handles and updates position computations.

function makeYNet() {

// This populates the YactiveLinks variable with the proper YLinks. The goal is to be able to only display links whose value is above some threshold.
for (var i=0; i<YLinks.length; i++) {
    if (YLinks[i].link2 > thr) {
        YactiveLinks.push(YLinks[i]);
    }
}

// Add nodes and links to forceY
forceY
.nodes(YNodes)
.links(YactiveLinks);

// Draw lines
var Ylink = svgY.selectAll("line.link")
.data(YactiveLinks)
.enter()
.append("line")
.attr("class", "link")
.style("stroke-width", 2.0);

// Draw nodes
var Ynode = svgY.selectAll("circle.node")
.data(YNodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", radius)
.attr("high",0)
.attr("id", function(d,i) {
      return ("idy" + i);
      })
.style("fill", function(d) { return color(d.group1); })
.call(forceY.drag)
;

// Define tick 
forceY.on("tick", function() {
          Ylink
          .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; });

          Ynode.attr("cx", function(d) { return d.x; })
          .attr("cy", function(d) { return d.y; });
          });

// Start 'er up
forceY.start();
}
VividD
  • 10,456
  • 6
  • 64
  • 111
DBLarremore
  • 61
  • 1
  • 5
  • I am trying to do the same thing as you: sync the node positions of two force layouts on the same page. I have two force layouts which works thanks to the reply by Alex Reynolds. However, I do not get where to set the hooks for the position sync. Since the links to DBLaremore's approach do not work anymore, has anyone a hint where to start or maybe links to another example? – tty56 Jun 17 '13 at 13:24

2 Answers2

5

I wrote a tool that allows browsing biological regulatory networks, showing two SVG panels side-by-side. Each panel contains a force-layout network, as drawn by the d3.js API.

I found that the key to making this work is to give every element in the DOM a unique name, where there can be duplication.

In my case, I used _left and _right as suffices to every panel element, where the element is in the left or right panel, respectively. It is a lot of work to keep track of, but the network renderer can target its calls and events to the correct element and network.

In your case:

.attr("id", function(d,i) {
      return ("idx" + i);
      })

You want to replace the return value with something that uniquely addresses the network that the node is associated with. Whether you use a index numbering scheme or a suffix-based approach, like I did, the trick is to make sure all id names are unique.

Alex Reynolds
  • 95,983
  • 54
  • 240
  • 345
  • 1
    Your site's example is a good one to mimic—looks great! However, I am unable to fix my problem by changing the ids. I have tried using different ids, as well as no ids, and in both cases the displays are still updating each other. I have checked that the names for left (X) and right (Y) differ in the names of the layout, nodes, links, and svg canvases. Any other suggestions? I don't know if this is relevant, but if you drag and hold one network for long enough, the other will cool/freeze and then no longer be linked (apparently). – DBLarremore Nov 30 '12 at 19:56
  • I don't know what else would work. I do know that making sure every single element in each network is unique is key to making this work. If there are any duplicate `id` names, things break. Wish I could be more help! – Alex Reynolds Nov 30 '12 at 20:00
  • Another things I have noticed that (perhaps) provides a clue: After some amount of time/dissipation, the layout will freeze the nodes in their current location. If you grab the left layout and move it around until the right freezes, you will be unable to move the right layout indirectly any longer. Then, if you grab the right layout, you can use it to influence the left **only** until the left freezes. To me, this implies that while the positions of the layout can be affected, the cooling timer is not affected by that movement. The nodes move in both plots, but only one registers dynamics. – DBLarremore Nov 30 '12 at 23:34
  • I haven't posted code that does this (yet) but I can compartmentalize the two layouts by using the d3 asynchronous json call, d3.json(...) to load in the data. This appears to work, though it's not the best solution. I am still curious as to why my simple example from earlier (and @Alex-Reynolds' fix) did not work... – DBLarremore Dec 03 '12 at 20:57
0

Javascript is notorious for obfuscating whether state is shared or not. Your minimal example is no longer available, but when I encountered the same problem, my conclusion was that I was only making shallow copies where I needed complete clones. (I'm talking about the links and nodes that D3 takes as input.)

Without a true copy, the resulting behavior will be a bit random, as you discovered, depending on whether D3 code choses to modify any shared data "in situ" or not. In general, D3 tries not to create copies, for performance reasons.

This is why the json call workaround works: by completely stringifying all the data, you are de facto forcing a clone operation.