2

I'm trying to bind a network visualization from D3.js to a custom output in Shiny. For some reason, it seems like my rendering functions are not being called. Here is my code:

rbindings.js

var forceNetworkOB = new Shiny.OutputBinding();

forceNetworkOB.find = function(scope) {
  return $(scope).find("svg.rio-force-network");
};

forceNetworkOB.renderValue = function(el, graph) {

  alert('rendering')

  //actual rendering code here...

};

Shiny.outputBindings.register(forceNetworkOB, "jumpy.forceNetworkOB");

CustomIO.R

renderForceNetwork <- function(expr, env=parent.frame(), quoted=FALSE) {

  func <- exprToFunction(expr, env, quoted)

  function() {

    # Never called
    browser()

    graph <- func()

    list(nodes = graph$nodes,
         links = graph$edges
    )
  }
}

forceNetwork <- function(id, width = '400px', height = '400px') {

  tag('svg', list(id = id, class = 'rio-force-network', width = width, height = height))

}

ui.R

library(shiny)
source('customIO.R')

shinyUI(fluidPage(

  tags$script(src = 'js/d3.min.js'),
  tags$script(src = 'js/rbindings.js'),

  titlePanel('Network Visualization'),

  tabsetPanel(
    tabPanel('D3.js Force Layout',
      forceNetwork('vis.force', width = '800px', height = '800px'),
    )
  )

))

and server.R

library(shiny)

source('cytoscape.R')
source('customIO.R')

shinyServer(function(session, input, output) {


  # Load the network
  network <- networkFromCytoscape('network.cyjs')

  output$vis.force <- renderForceNetwork({

    # Never called
    print('rendering')
    browser()

    list(
      nodes = data.frame(name = network$nodes.data$Label_for_display, group = rep(1, nrow(network$nodes.data))),
      edges = data.frame(from = network$edges[,1], to = network$edges[,2])
    )

  })

})

As you can see from the comments, the browser() lines in my R rendering functions are never called, as well as the alert() in the js rendering function. With some js debugging I can see that my custom binding is correctly giving the svg element to render to as well as its id. This might be something simple but I cannot figure it out.

JaredL
  • 1,372
  • 2
  • 11
  • 27

1 Answers1

6

Well, although the code seems all legitimate, you really have to dig into the source code of Shiny to find the culprit.

When Shiny initializes, it calls initShiny(), which then calls bindOutputs. Now, here is what the bindOutputs function looks like:

function bindOutputs(scope) {

  if (scope === undefined)
    scope = document;

  scope = $(scope);

  var bindings = outputBindings.getBindings();

  for (var i = 0; i < bindings.length; i++) {
    var binding = bindings[i].binding;
    var matches = binding.find(scope) || [];
    for (var j = 0; j < matches.length; j++) {
      var el = matches[j];
      var id = binding.getId(el);

      // Check if ID is falsy
      if (!id)
        continue;

      var bindingAdapter = new OutputBindingAdapter(el, binding);
      shinyapp.bindOutput(id, bindingAdapter);
      $(el).data('shiny-output-binding', bindingAdapter);
      $(el).addClass('shiny-bound-output'); // <- oops!
    }
  }

  // Send later in case DOM layout isn't final yet.
  setTimeout(sendImageSize, 0);
  setTimeout(sendOutputHiddenState, 0);
}

The line I marked with <- oops is what causes all the problem. This really isn't a bug of Shiny per se: this line of code relies on jQuery to add a class to el, which is the svg DOM element that you've created with forceNetwork() function.

The class shiny-bound-output is important for the actual binding to work.

The problem is that $.addClass doesn't work for <svg>. For a good reference, see this article or this question on stackoverflow.

So, as a result, your <svg> element lacks the required shiny-bound-output class that would make your custom OutputBinding to function correctly.


Solution:

Don't use <svg> as the container of your output. Use a <div> instead. It means, you should change your forceNetwork function to:

forceNetwork <- function(id, width = '400px', height = '400px') {    
  tag('div', list(id = id, class = 'rio-force-network', width = width, height = height))
}

You can easily append a <svg> using d3.select(...).append('svg') and set the width and height there.

(Remember to modify your find() function in rbindings.js as well).


The final word

If you somehow add d3.select(...).append('svg') to your javascript code, remember to clear() the <div> container in the output binding's renderValue() function before the actual d3 drawing. Otherwise every time renderForceNetwork is called, it will add a new <svg> element to your <div> container.

Community
  • 1
  • 1
Xin Yin
  • 2,896
  • 21
  • 20