Custom tooltips for plotly

Posted on December 23, 2018 by Stéphane Laurent

I’ve found several questions on Stackoverflow asking for tooltips on the outliers of a plotly boxplot. Here I provide a solution using Shiny and the qTip2 Javascript library.

Below is the Shiny app. Download the two files jquery.qtip.min.css and jquery.qtip.min.js and put them in the www subfolder.

library(plotly)
library(shiny)
library(shinyjs)
library(htmlwidgets)

### Prepare data ----
set.seed(666)
# group A
data_a <- data.frame(Class = "red", Group = "A",
                     Sample = 1:50, 
                     x1 = rnorm(50, mean=0, sd=.5), 
                     x2 = rnorm(50, mean=0.5, sd=1.5), 
                     x3 = rnorm(50, mean=5, sd=.5), 
                     x4 = rnorm(50, mean=0, sd=3.5),
                     x5 = rnorm(50, mean=-6, sd=.5))
# group B
data_b <- data.frame(Class = "red", Group = "B",
                     Sample = 1:50, 
                     x1 = rnorm(50, mean=0, sd=5.5), 
                     x2 = rnorm(50, mean=0.5, sd=7.5), 
                     x3 = rnorm(50, mean=5, sd=.5), 
                     x4 = rnorm(50, mean=0, sd=.5),
                     x5 = rnorm(50, mean=-6, sd=2.05))
# data in graphable format
dat <- reshape2::melt(rbind(data_a, data_b), 
                      id.vars = c("Class", "Group", "Sample"))

# Plotly 'on hover' event ----
addHoverBehavior <- c(
  "function(el, x){",
  "  el.on('plotly_hover', function(data) {",
  "    if(data.points.length==1){",
  "      $('.hovertext').hide();",
  "      Shiny.setInputValue('hovering', true);",
  "      var d = data.points[0];",
  "      var left_px = d.xaxis.d2p(d.x) + d.xaxis._offset;",
  "      var top_px = d.yaxis.l2p(d.y) + d.yaxis._offset;",
  "      var rect = document.getElementById('plotly').getBoundingClientRect();",
  "      Shiny.setInputValue('left_px', left_px);",
  "      Shiny.setInputValue('top_px', top_px);",
  "      Shiny.setInputValue('left_pct', left_px/rect.width);",
  "      Shiny.setInputValue('top_pct', top_px/rect.height);",
  "      Shiny.setInputValue('d_y', d.y);",
  "      Shiny.setInputValue('d_text', d.text);",
  "    }else{",
  "      $('#hover_info').qtip('hide');",
  "    }",
  "  });",
  "  el.on('plotly_unhover', function(data) {",
  "    Shiny.setInputValue('hovering', false);",
  "  });",
  "}")

### Shiny app ----
js_qTip <- "
$('#hover_info').qtip({
  overwrite: true,
  content: {
    text: $('#tooltiptext').clone()
  },
  position: {
    my: '%s',
    at: '%s',
    target: [%s,%s],
    container: $('#plotly')
  },
  show: {
    ready: true
  },
  hide: {
    target: $('#plotly')
  },
  style: {
    classes: 'myqtip'
  }
});
"

ui <- fluidPage(
  useShinyjs(),
  tags$head(
    tags$link(rel = "stylesheet", href = "jquery.qtip.min.css"),
    tags$script(src = "jquery.qtip.min.js"),
    tags$style("
      .myqtip {
        font-size: 15px;
        line-height: 18px;
        background-color: rgba(54,57,64,0.8);
        border-color: rgb(54,57,64);
        color: white;
      }"
    )
  ),
  
  div(
    id = "tooltiptext", style = "display: none"
  ),
  div(
    style = "position: relative",
    plotlyOutput("plotly"),
    div(id = "hover_info", style = "position: absolute;")
  )
  
)

server <- function(input, output){
  output[["plotly"]] <- renderPlotly({
    plot_ly(dat, type = "box", 
            x = ~variable, y = ~value, 
            text = paste0("<b> group: </b>", dat$Group, "<br/>",
                          "<b> sample: </b>", dat$Sample, "<br/>"),
            hoverinfo = "y") %>%
      onRender(addHoverBehavior)
  })
  
  observeEvent(input[["hovering"]], {
    if(isTRUE(input[["hovering"]])){
      tooltip <- paste0(input[["d_text"]], 
                        "<b> value: </b>", formatC(input[["d_y"]]))
      pos <- ifelse(input[["left_pct"]] < 0.5,
                    ifelse(input[["top_pct"]] < 0.5, 
                           "top left",
                           "bottom left"),
                    ifelse(input[["top_pct"]] < 0.5,
                           "top right",
                           "bottom right"))
      runjs(
        paste0(
          sprintf(
            "$('#tooltiptext').html('%s');", tooltip
          ),
          sprintf(js_qTip, pos, pos, input[["left_px"]], input[["top_px"]])
        )
      )
    }
  })
  
}

shinyApp(ui = ui, server = server)

ggplotly

Beware if you use ggplotly. It pertubs the order of the rows of the dataset. Do in this way: