1

I am trying to create a force directed network graph in R using the networkD3 package. Everything works well ...

devtools::install_github('christophergandrud/networkD3')
library(networkD3)
links2 <- data.frame(
  Source = c(0, 0, 0, 1, 2, 3), 
  Target = c(1, 4, 5, 3, 4, 5), 
  Value = c(1, 9.9, 10, 8.8, 6.6, 7.2))
nodes2 <- data.frame(ID = 0:5, 
  Group = c(1L, 1L, 1L, 2L, 1L, 2L))

# this works
forceNetwork(Links=links2, Nodes=nodes2, 
  Source="Source", Target="Target", Value="Value", 
  NodeID="ID", Group="Group")

... until I start playing with the linkDistance argument. When I set it to what I want, a function of the Value, I get a network diagram that consists of what appears to be a single node in the far upper left corner of the device.

# this doesn't work
forceNetwork(Links=links2, Nodes=nodes2, 
  Source="Source", Target="Target", Value="Value", 
  NodeID="ID", Group="Group",
  linkDistance="function(d) { return d.value; }")

I would appreciate any suggestions as to how to get the length of the links to vary.

I'm using R version 3.1.3 for Windows in R Studio Version 0.98.1103 with package networkD3 version 0.1.2.2. (I originally experienced the problem using networkD3 version 0.1.2.1 from CRAN. So I installed the latest version from GitHub, and had the same problem.)

CJ Yetman
  • 6,482
  • 2
  • 15
  • 45
Jean V. Adams
  • 4,214
  • 2
  • 24
  • 43
  • I submitted this an issue to **networkD3**, github.com/christophergandrud/networkD3/issues/37. And I received an answer there from @jjallaire that works! First attach `library(htmlwidgets)`, then use the `JS` function in the `linkDistance` argument: `forceNetwork(Links=links2, Nodes=nodes2, Source="Source", Target="Target", Value="Value", NodeID="ID", Group="Group", linkDistance=JS('function(d) {', 'return d.value;', '}'))` – Jean V. Adams May 26 '15 at 13:14

2 Answers2

3

The networkD3 package is awesome!!

After an exhaustive investigation, I've tracked down the bug: The HTML/CSS/JavaScript code that is generated by networkD3 seems to depend on d3js, which provides the d3.min.js file that is central to the generated web code. It is inside d3.min.js that the linkDistance feature is coded, and it is there that the bug occurs.

Unfortunately, as the file name indicates, that JavaScript is minified, but you can access the non-minified source from the d3js site, specifically at https://github.com/mbostock/d3/blob/master/d3.js.

If you look at line 6307 (and surrounding context) of d3.js, you'll find this:

force.linkDistance = function(x) {
  if (!arguments.length) return linkDistance;
  linkDistance = typeof x === "function" ? x : +x;
  return force;
};

This function is called at some point during initialization with x being the string that you originally provided in your R call to forceNetwork() (incidentally, that string is embedded in the index.html file that is generated by networkD3 as JSON data, along with all other input data).

To repeat, x is a string. This means typeof(x) will return "string", not "function", as line 6307 is checking for. Hence, the second alternative of the ternary will be invoked, +x, which is JavaScript-style coercion to number, which will result in NaN. Thus, NaN gets propagated through all the link distances and coordinates and everything gets f*!%ed.

This can be fixed by changing it to:

linkDistance = typeof x === "string" ? eval('('+x+')') : +x;

Obviously I changed "function" to "string", but also I had to add an eval() call to actually pull the function definition out of the string so that all future code that works with linkDistance will work, because it all assumes that it's a function, not a string. Finally, I had to concatenate with parentheses to ensure that the function definition is expressionized, otherwise, if you used function as the leading token of your code string (which you did, and they did in the networkD3 documentation, and that's the most sensible thing to do), then it would be taken as a function statement, meaning it would not be parsed as an actual expression statement, and the eval() call would return undefined, and you'd end up getting NaN when the code would eventually try to use the undefined value in arithmetic expressions, leading to the same errors. (See What is the difference between a function expression vs declaration in JavaScript? and https://javascriptweblog.wordpress.com/2010/07/06/function-declarations-vs-function-expressions/ for a couple good sources on this distinction, and there's probably a million other sources out there as well.)

I've tested the above fix on my machine and it works. I suppose this should be reported as a bug to the d3js people, but I feel I've done enough work here and can't be bothered. Feel free to report this to them, using my post here as a reference.

Community
  • 1
  • 1
bgoldst
  • 30,505
  • 4
  • 34
  • 59
  • I'm pretty sure YOU are awesome and that this qualifies as an answer. Let me just clarify a couple points. (1) You are saying that without digging into `d3js` I can't vary the lengths of the links with the current version of **networkD3**, right? (2) But, if I download the `d3js` source code to my PC and modify it as you suggest, then the **networkD3** function `forceNetwork` will work as advertised, right? – Jean V. Adams May 26 '15 at 03:14
  • I just submitted your suggested change as a new issue: https://github.com/mbostock/d3/issues/2451 – Jean V. Adams May 26 '15 at 03:39
  • @JeanV.Adams (1) correct. (2) correct, although I think this would require modifying the `networkD3` installation, because I assume it packages a copy of `d3.min.js` in its installation. Obviously, you're going to want the problem to be fixed more permanently than in your current installation, so the bug report is the best bet. – bgoldst May 26 '15 at 03:40
  • This issue was closed on **d3js**, https://github.com/mbostock/d3/issues/2451, *"Sorry, but this is not the right place to fix the problem. If you have a string that represents a function, and you want to eval that string to turn it into code, you should do it outside of the D3 API."* I have notified **networkD3**, https://github.com/christophergandrud/networkD3/issues/37. – Jean V. Adams May 26 '15 at 10:24
  • I got an answer that works with the current networkD3 and d3js (see my comment on original post). – Jean V. Adams May 26 '15 at 13:19
2

If you want a value passed through from R to JavaScript to be evaluated (i.e. to resolve a string to a function) you enclose it in the htmlwidgets::JS function. For example:

library(htmlwidgets)
forceNetwork(Links=links2, Nodes=nodes2, 
             Source="Source", Target="Target", Value="Value", 
             NodeID="ID", Group="Group",
             linkDistance=JS('function(d) {', 'return d.value;', '}'))
JJ Allaire
  • 516
  • 3
  • 2