4 min read

plotly 4.7.1 now on CRAN

I'm excited to announce that plotly 4.7.1 is now on CRAN! Along with some important bug fixes and numerous improvements to the underlying plotly.js library, this release includes an exciting new R-specific feature -- the ability to modify (i.e., update without a full redraw) an existing plotly graph inside a shiny app via the new plotlyProxy() function. In other words, this proxy interface allows one to perform more efficient and responsive updates to a plotly graph within a shiny app. Before I provide an overview of the proxy interface, lets jump right into some of new plotly.js goodies.

New plotly.js goodies

Upgrading the R package from 4.7.0 to 4.7.1 upgrades the corresponding plotly.js version from 1.27.1 to 1.29.2. A number of exciting improvements have been added, just to name a few: select/lasso events for scattergl/scattermapbox trace types, 3D annotations, contour labelling, and even touch events on mobile. For quite a while now, R users have been able to link multiple views (without shiny) using nearly any (SVG) trace type (e.g., scatter, bar, heatmap, etc), but the new brush events on scattergl/scattermapbox trace types allow us to brush way more points and trigger selections via a map. Here is an example of highlighting earthquakes west of Fiji to compare the relative frequency of their magnitude and number of reporting stations (to the overall relative frequency):

And the R code to generate the (self-contained!) HTML:

library(crosstalk)
eqs <- SharedData$new(quakes)

# you need a mapbox API key to use plot_mapbox()
# https://www.mapbox.com/signup/?route-to=https://www.mapbox.com/studio/account/tokens/
map <- plot_mapbox(eqs, x = ~long, y = ~lat) %>%
  add_markers(color = ~depth) %>%
  layout(
    mapbox = list(
      zoom = 2,
      center = list(lon = ~mean(long), lat = ~mean(lat))
    )
  ) %>%
  highlight("plotly_selected")

# shared properties of the two histograms
hist_base <- plot_ly(eqs, color = I("black"), histnorm = "probability density") %>%
  layout(barmode = "overlay", showlegend = FALSE) %>%
  highlight(selected = attrs_selected(opacity = 0.5))

histograms <- subplot(
  add_histogram(hist_base, x = ~mag),
  add_histogram(hist_base, x = ~stations),
  nrows = 2, titleX = TRUE
)

bscols(histograms, map)

Another super cool and easy-to-use plotly.js feature is contour line labelling:

plot_ly(z = volcano, type = "contour", contours = list(showlabels = TRUE)) %>%
  colorbar(title = "Elevation \n in meters")

To learn more about all the new plotly.js improvements, read the plotly.js release notes.

Modifying plotly graphs via plotlyProxy()

The design of plotly's new proxy interface is inspired by similar interfaces in leaflet and DT (thanks Joe Cheng and Yihui Xie!). That is, plotlyProxy() initiates a proxy object just like leafletProxy()/dataTableProxy() by referencing a shiny output ID. However, at least for now, you must use the plotlyProxyInvoke() function to modify a plotlyProxy() object, which requires knowledge/use of a plotly.js method for the updating logic (among them, Plotly.restyle, Plotly.relayout, Plotly.addTraces, and Plotly.deleteTraces are the most widely useful). This simple shiny app uses Plotly.restyle to change the fillcolor of Canada (i.e., polygons) in response to the dropdown.

Notice how the map and the outline of Canada are not effected (i.e., are not redrawn) when the fill color changes. The code for the app is below. Notice how, instead of having renderPlotly() regenerate the map in response to a change in input$color (i.e., the "old" or "naive" way of updating a plotly graph which always uses Plotly.newPlot), it uses Plotly.restyle via plotlyProxyInvoke() to perform a more efficient update.

# you can also run this example via the new plotly_example() function!
# plotly_example("shiny", "proxy_restyle_canada")

library(shiny)
library(plotly)

ui <- fluidPage(
  selectInput("color", "Canada's fillcolor", colors(), selected = "black"),
  plotlyOutput("map")
)

server <- function(input, output, session) {
  
  output$map <- renderPlotly({
    map_data("world", "canada") %>%
      group_by(group) %>%
      plot_mapbox(x = ~long, y = ~lat, color = I("black")) %>%
      add_polygons() %>%
      layout(
        mapbox = list(center = list(lat = ~median(lat), lon = ~median(long)))
      )
  })
  
  observeEvent(input$color, {
    plotlyProxy("map", session) %>%
      plotlyProxyInvoke("restyle", list(fillcolor = toRGB(input$color)))
  })
  
}

shinyApp(ui, server)

It's worth noting that using plotlyProxyInvoke() requires knowledge of the figure reference and the plotly.js API. Thus, it can help to have some knowledge of how your plots are actually represented in JSON, which you can always obtain via plotly_json(), for example:

p <- plot_ly(economics, x = ~pce, y = ~psavert, z = ~unemploy, color = ~as.numeric(date), mode = "markers+lines")
plotly_json(p)

This gives us a glimpse into what the R package actually sends along to the newPlot method to generate the initial view on page load. In this case, the R package generates scatter3d marker and line objects. We could use a similar plotlyProxyInvoke() pattern as before to alter the value of certain attribute(s) of these objects (e.g., marker/line size/width):

# you can also run this example via the new plotly_example() function!
# plotly_example("shiny", "proxy_restyle_economics")

library(shiny)
library(plotly)

ui <- fluidPage(
  sliderInput("marker", "Marker size", min = 0, max = 20, value = 8),
  sliderInput("path", "Path size", min = 0, max = 30, value = 2),
  plotlyOutput("p")
)

server <- function(input, output, session) {
  
  output$p <- renderPlotly({
    plot_ly(
      economics, x = ~pce, y = ~psavert, z = ~unemploy, 
      color = ~as.numeric(date), mode = "markers+lines"
    )
  })
  
  observeEvent(input$marker, {
    plotlyProxy("p", session) %>%
      plotlyProxyInvoke(
        "restyle", 
        # could also do list(marker = list(size = input$marker))
        # but that overwrites the existing marker definition
        # https://github.com/plotly/plotly.js/issues/1866#issuecomment-314115744
        list(marker.size = input$marker)
      )
  })
  
  observeEvent(input$path, {
    plotlyProxy("p", session) %>%
      plotlyProxyInvoke(
        "restyle", list(line.width = input$path)
      )
  })
  
}

shinyApp(ui, server)

At this point, you might be wondering something along these lines of: "how on earth am I supposed to know/remember what attributes a scatter3d line object has?" One option is to search through the figure reference, but I prefer to use the schema() function since it allows me to more easily traverse the reference and get more information about acceptable/default attribute values. For example, when creating this app, I forgot what line attribute controlled its size/width, so I did the following:

I hope you find this new release useful and this post informative for creating more performant shiny apps! If you're interested in seeing more examples of plotlyProxy() in action, see here.