0

I’m trying to send R data from the server to the client using JavaScript using Shiny's onInputChange and addCustomMessageHandler functionality described here:

https://ryouready.wordpress.com/2013/11/20/sending-data-from-client-to-server-and-back-using-shiny/

My goal is to add tooltips to the links in a forceNetwork diagram in Shiny using data that will be stored in an R variable passed to the ui through JavaScript. My application is supposed to accept 2 CSV files (one with nodes data, one with links data) and then plot it in a forceNetwork with tooltips for the links. I would need to retrieve the tooltip column stripped from the nodes and links data when forceNetwork makes the forceNetwork object. Everything works fine except for the tooltip functionality. What’s stumping me, is

  • how to pass the subsetted data with just tooltip information to the ui without getting an error for exposing a reactive value in the server, and
  • how to use that data and javascript to make tooltips for the forceNetwork links.

If this were not a reactive graph, I would just append the tooltip column to the fn forceNetwork object after creating it. However, that doesn’t seem to make it into the graph. I’m looking at instead passing tooltip data to a tag in the ui and then assigning it to display as the tooltip for the links.

Here is the code:

library(shiny)
library(networkD3)

server <- function(input, output, session) {

  # User uploads CSV for nodes (file has name, group, tooltip columns)
  mydata_n <- reactive({
    req(input$file_n) 

    inFile <- input$file_n 
    df <- read.csv(inFile$datapath)
    return(df)
  })

# User uploads CSV for links (file has source, target, value, tooltip columns)
  mydata_l <- reactive({
    req(input$file_l) 

    inFile <- input$file_l
    df <- read.csv(inFile$datapath)

    # The source and target columns have names rather than zero-indexed row numbers as forceNetwork requires, so fix them using nodes file as reference
    df$source <- match(df$source, mydata_n()$name)
    df$target <- match(df$target, mydata_n()$name)
    df[1:2] <- df[1:2]-1
    return(df)
  })

  # Render tables showing content of uploaded files 

  output$table_n <- renderTable({
    mydata_n()
  })

  output$table_l <- renderTable({
    mydata_l()
  })

  # make network with data

  output$net <- renderForceNetwork({
    fn <- forceNetwork(
      Links = mydata_l(), Nodes = mydata_n(), Source = "source",
      Target = "target", Value = "value", NodeID = "name",
      Group = "group", opacity = 1, zoom = FALSE, bounded = F, linkWidth = 1, linkColour = "#939393", charge = -80
    ) 
  }

    )

 # This part is broken. When a links file is uploaded, subset it to make a linkTooltips df with just tooltip data and pass it to the browser using myCallbackHandler
  observe({
    input$file_l
    linkTooltips <- mydata_l()["tooltip"]
    session$sendCustomMessage(type = "myCallbackHandler", linkTooltips)
  })

    # Show table output

}

ui <- fluidPage(

  # This is where the linkTooltips data should be assigned to display as a tooltip, but I'm not sure how to access that R variable in javascript and assign each tooltip to the appropriate link. My start (based on an answer to a previous question) is below.
  tags$head( tags$script('Shiny.addCustomMessageHandler("myCallbackHandler",
                         function(linkTooltips) {
                        d3.selectAll(".link")
                        .attr("title", "linkTooltips");
                         });
                         ')
  ),

  titlePanel("ForceNetD3"),
  mainPanel(forceNetworkOutput("net"), 

            # start input
            fluidRow(column( 12, wellPanel( h3("Upload a file"),
          fileInput('file_n', 'Choose CSV File for Nodes', accept=c('text/csv', 'text/comma-separated-values,text/plain', '.csv')),
          fileInput('file_l', 'Choose CSV File for Links', accept=c('text/csv', 'text/comma-separated-values,text/plain', '.csv'))

            )

            )),

            fluidRow( 
              tabsetPanel(
                tabPanel( "Nodes Data", tableOutput(outputId = "table_n")), 
                tabPanel( "Links Data", tableOutput(outputId = "table_l"))
            )

            # end input

            ))
  )



shinyApp(ui = ui, server = server)

I'd really appreciate if someone can point me in the right direction.

CJ Yetman
  • 8,373
  • 2
  • 24
  • 56
  • Do you have an example of where you "append the tooltip column to the fn forceNetwork object after creating it" and it "doesn’t seem to make it into the graph."? I can't imagine why that would be. – CJ Yetman Dec 20 '17 at 10:55
  • also, it would be a lot easier to answer your question if you made it more clear/specific and gave a [*minimal* reproducible example](https://stackoverflow.com/a/5963610/4389763) – CJ Yetman Dec 20 '17 at 13:10

1 Answers1

1

Add these two lines to the code in your renderForceNetwork function...

fn$x$links$tooltip <- mydata_l()$tooltip
htmlwidgets::onRender(fn, 'function(el, x) { d3.selectAll(".link").append("svg:title").text(function(d) { return d.tooltip; }); }')

With that, your SVG lines/edges will have titles that will show up as tooltips when you hover over them (and all the other stuff you have with addCustomMessageHandler etc. is unnecessary).

I predict, you're going to ask next how to integrate tipsy.js? Add this to the code in your renderForceNetwork function (instead of what's above)...

fn$x$links$tooltip <- mydata_l()$tooltip
fn$x$nodes$tooltip <- mydata_n()$tooltip
htmlwidgets::onRender(fn, 'function(el, x) {
    d3.selectAll(".node circle, .link")
        .attr("title", function(d) { return d.tooltip; });
    tippy("[title]");
}')

and then make sure that your fluidPage command includes...

tags$head(tags$script(src = "https://unpkg.com/tippy.js@2.0.2/dist/tippy.all.min.js"))

here's a full working example...

library(shiny)
library(networkD3)
library(htmlwidgets)

server <- function(input, output, session) {
  
  # User uploads CSV for nodes (file has name, group, tooltip columns)
  mydata_n <- reactive({
req(input$file_n) 

inFile <- input$file_n 
df <- read.csv(inFile$datapath)
return(df)
  })
  
  # User uploads CSV for links (file has source, target, value, tooltip columns)
  mydata_l <- reactive({
req(input$file_l) 

inFile <- input$file_l
df <- read.csv(inFile$datapath)

# The source and target columns have names rather than zero-indexed row numbers as forceNetwork requires, so fix them using nodes file as reference
df$source <- match(df$source, mydata_n()$name)
df$target <- match(df$target, mydata_n()$name)
df[1:2] <- df[1:2]-1
return(df)
  })
  
  # Render tables showing content of uploaded files 
  
  output$table_n <- renderTable({
mydata_n()
  })
  
  output$table_l <- renderTable({
mydata_l()
  })
  
  # make network with data
  
  output$net <- renderForceNetwork({
fn <- forceNetwork(
  Links = mydata_l(), Nodes = mydata_n(), Source = "source",
  Target = "target", Value = "value", NodeID = "name",
  Group = "group", opacity = 1, zoom = FALSE, bounded = F, linkWidth = 1, linkColour = "#939393", charge = -80
) 
fn$x$links$tooltip <- mydata_l()$tooltip
fn$x$nodes$tooltip <- mydata_n()$tooltip
htmlwidgets::onRender(fn, 'function(el, x) {
         d3.selectAll(".node circle, .link")
         .attr("title", function(d) { return d.tooltip; });
         tippy("[title]");
}'
)
  }
  )
  
}

ui <- fluidPage(
  tags$head(tags$script(src = "https://unpkg.com/tippy.js@2.0.2/dist/tippy.all.min.js")),
  titlePanel("ForceNetD3"),
  mainPanel(forceNetworkOutput("net"), 
        
        # start input
        fluidRow(column( 12, wellPanel( h3("Upload a file"),
                                        fileInput('file_n', 'Choose CSV File for Nodes', accept=c('text/csv', 'text/comma-separated-values,text/plain', '.csv')),
                                        fileInput('file_l', 'Choose CSV File for Links', accept=c('text/csv', 'text/comma-separated-values,text/plain', '.csv'))
                                        
        )
        
        )),
        
        fluidRow( 
          tabsetPanel(
            tabPanel( "Nodes Data", tableOutput(outputId = "table_n")), 
            tabPanel( "Links Data", tableOutput(outputId = "table_l"))
          )
          
          # end input
          
        ))
  )



shinyApp(ui = ui, server = server)

and here's some R code to generate a nodes.csv and links.csv to test it with...

links <- read.csv(header = T, text ="
source,target,value,tooltip
first,second,1,link1
first,third,1,link2
second,third,1,link3
third,fourth,1,link4
")
write.csv(links, "links.csv", row.names = F)

nodes <- read.csv(header = T, text ="
name,group,tooltip
first,1,node1
second,1,node2
third,1,node3
fourth,1,node4
")
write.csv(nodes, "nodes.csv", row.names = F)

(Side note: So that it's easier for people to help you, and so that this can be useful to other people who read it, I strongly encourage you to make minimal (meaning that you cut out as much unnecessary code as possible while still demonstrating the problem), reproducible (meaning that you include example data and anything else needed to run your code) examples. See here for a good explanation of that.)

CJ Yetman
  • 8,373
  • 2
  • 24
  • 56
  • Worked like a charm. I was going the `onInputChange` route because I thought `htmlwidgets` was not available on my R install at work for security reasons. I have since resolved that. And am very glad I did -- doing an implementation of this using the `` tags in Shiny and accessing object properties manually was a huge pain. Thanks again. – user3462317 Jan 23 '18 at 22:02