1

I'm targeting a specific plotly trace with my R shiny inputs but I'd like to do so without referring to the data behind all the other traces. As seen below, updating the cylinder trace depends on an else condition. I'd like to avoid that, as in my real data, I have many other traces, rather than the one here (the sphere trace).

library(shiny)
library(plotly)
library(purrr)

plot_cylinder <- function(x, y, z, radius, height, color = 'red') {
  theta <- seq(0, 2*pi, length.out = 30)
  z_cyl <- seq(z, z + height, length.out = 2)
  x_cyl <- outer(x + radius * cos(theta), rep(1, length(z_cyl)))
  y_cyl <- outer(y + radius * sin(theta), rep(1, length(z_cyl)))
  z_cyl <- outer(rep(1, length(theta)), z_cyl)
  
  list(
    type = "surface",
    x = x_cyl,
    y = y_cyl,
    z = z_cyl,
    colorscale = list(c(0, color), c(1, color))
  )
}

plot_sphere <- function(x, y, z, r, color = 'blue') {
  theta <- seq(0, 2*pi, length.out = 30)
  phi <- seq(0, pi, length.out = 30)
  x_sphere <- x + r * outer(cos(theta), sin(phi))
  y_sphere <- y + r * outer(sin(theta), sin(phi))
  z_sphere <- z + r * outer(rep(1, length(theta)), cos(phi))
  
  list(x = x_sphere, y = y_sphere, z = z_sphere, color = color)
}

# Static sphere data
sphere_radius <- 7.24
sphere_data <- plot_sphere(0, 0, 10, sphere_radius)

cylinder_data <- plot_cylinder(0, 0, 0, 3, 10)

ui <- fluidPage(
  titlePanel("Cylinder Plotting"),
  sidebarLayout(
    sidebarPanel(
      sliderInput("radius", "Cylinder Radius:", min = 1, max = 10, value = 3),
      sliderInput("height", "Cylinder Height:", min = 1, max = 20, value = 10),
      actionButton("initialize", "Initialize Cylinder")
    ),
    mainPanel(
      plotlyOutput("plot")
    )
  )
)

server <- function(input, output, session) {
  
  plot <- plot_ly() |> 
    add_surface(x = sphere_data$x, y = sphere_data$y, 
                z = sphere_data$z, colors = sphere_data$color, 
                opacity = 0.5, name = "sphere") |> 
    add_surface(x = cylinder_data$x, y = cylinder_data$y, 
                z = cylinder_data$z, colorscale = cylinder_data$colorscale, 
                opacity = 0, name = "cylinder")
  
  pb <- plotly_build(plot)
  traces <- map(pb$x$data, "name")
  
  output$plot <- renderPlotly({
    plot
  })
  
  observeEvent(input$initialize, {
    proxy <- plotlyProxy("plot", session)
    
    plotlyProxyInvoke(proxy, "restyle", list(
      opacity = lapply(traces, function(name) if(name %in% "cylinder") 0.1 else 0.5)
      ))
  })
  
  observeEvent(c(input$radius, input$height), {
    proxy <- plotlyProxy("plot", session)
    
    # Get the updated cylinder trace
    cylinder_data <- plot_cylinder(0, 0, 0, input$radius, input$height)
    
    # Update the trace with the new x, y, z values
    plotlyProxyInvoke(proxy, "restyle", list(
      x = lapply(traces, function(name) if(name %in% "cylinder") cylinder_data$x else sphere_data$x),
      y = lapply(traces, function(name) if(name %in% "cylinder") cylinder_data$y else sphere_data$y),
      z = lapply(traces, function(name) if(name %in% "cylinder") cylinder_data$z else sphere_data$z)
    ))
  })
}

shinyApp(ui = ui, server = server)
7
  • 1
    I don't see where traces is defined in your code. Did I miss it? Commented May 31, 2024 at 8:10
  • apologies; corrected Commented May 31, 2024 at 12:16
  • 1
    If I get this right you would like to modify the trace by its name? Here is how to get the trace.name / trace.index mapping. Commented May 31, 2024 at 13:04
  • @ismirsehregal Looks like some packages have changed and made those answers are out-of-date. But how would indices help? Could I do plotlyProxyInvoke(proxy, "restyle", indices, x ...? Commented May 31, 2024 at 19:28
  • 1
    Yes, plotly's restyle function has a traceIndices parameter. I'm currently short on time, but I'll try to come back here to provide a proper answer. Commented Jun 1, 2024 at 6:07

1 Answer 1

2

Plotly's restyle function has a traceIndices parameter:

An efficient means of changing attributes in the data array in an existing plot. When restyling, you may choose to have the specified changes affect as many traces as desired. The update is given as a single object and the traces that are affected are given as a list of traces indices. Note, leaving the trace indices unspecified assumes that you want to restyle all the traces.

Here is how to update only the data of the first trace (index: 0) via plotlyProxyInvoke:

library(shiny)
library(plotly)
library(purrr)

plot_cylinder <- function(x, y, z, radius, height, color = 'red') {
  theta <- seq(0, 2*pi, length.out = 30)
  z_cyl <- seq(z, z + height, length.out = 2)
  x_cyl <- outer(x + radius * cos(theta), rep(1, length(z_cyl)))
  y_cyl <- outer(y + radius * sin(theta), rep(1, length(z_cyl)))
  z_cyl <- outer(rep(1, length(theta)), z_cyl)
  
  list(
    type = "surface",
    x = x_cyl,
    y = y_cyl,
    z = z_cyl,
    colorscale = list(c(0, color), c(1, color))
  )
}

plot_sphere <- function(x, y, z, r, color = 'blue') {
  theta <- seq(0, 2*pi, length.out = 30)
  phi <- seq(0, pi, length.out = 30)
  x_sphere <- x + r * outer(cos(theta), sin(phi))
  y_sphere <- y + r * outer(sin(theta), sin(phi))
  z_sphere <- z + r * outer(rep(1, length(theta)), cos(phi))
  
  list(x = x_sphere, y = y_sphere, z = z_sphere, color = color)
}

# Static sphere data
sphere_radius <- 7.24
sphere_data <- plot_sphere(0, 0, 10, sphere_radius)

cylinder_data <- plot_cylinder(0, 0, 0, 3, 10)

ui <- fluidPage(
  titlePanel("Cylinder Plotting"),
  sidebarLayout(
    sidebarPanel(
      sliderInput("radius", "Cylinder Radius:", min = 1, max = 10, value = 3),
      sliderInput("height", "Cylinder Height:", min = 1, max = 20, value = 10)
    ),
    mainPanel(
      plotlyOutput("plot")
    )
  )
)

server <- function(input, output, session) {
  
  plotly_object <- plot_ly(colors = sphere_data$color) |> 
    add_surface(x = cylinder_data$x, y = cylinder_data$y, 
                z = cylinder_data$z, colorscale = cylinder_data$colorscale, 
                opacity = 0.5, name = "cylinder") |>
    add_surface(x = sphere_data$x, y = sphere_data$y, 
                z = sphere_data$z,
                opacity = 0.5, name = "sphere")
  
  pb <- plotly_build(plotly_object)
  traces <- unlist(map(pb$x$data, "name"))
  trace_indices <- setNames(seq_along(traces) - 1L, traces)
  
  output$plot <- renderPlotly({
    plotly_object
  })
  
  observeEvent(c(input$radius, input$height), {
    proxy <- plotlyProxy("plot", session)
    
    # Get the updated cylinder trace
    cylinder_data <- plot_cylinder(0, 0, 0, input$radius, input$height)
    # Update the trace with the new x, y, z values
    plotlyProxyInvoke(proxy, "restyle", list(
      x = list(cylinder_data$x),
      y = list(cylinder_data$y),
      z = list(cylinder_data$z)
    ), trace_indices[["cylinder"]])
    # if you need to modify multiple traces at once (and leave others as they are):
    # plotlyProxyInvoke(proxy, "restyle", list(
    #   x = list(cylinder_data$x, sphere_data$x + 100L),
    #   y = list(cylinder_data$y, sphere_data$y + 100L),
    #   z = list(cylinder_data$z, sphere_data$z + 100L)
    # ), list(0L, 1L))
  })
}

shinyApp(ui = ui, server = server)
Sign up to request clarification or add additional context in comments.

5 Comments

May you add the index to trace-name mapping from the answer you provided above? I have many traces so I cannot select them simply by number like 0L.
Just use the plotly_build approach shown in your example. Please see my edit.
Thanks! How would you approach it in a case where plotly_build does not give back all traces? Specifically ones added with plotlyProxyInvoke("addTraces"?
Dynamically updating the trace name / trace index mapping will require a updated JS solution (not necessary regarding your example). I don't know yet. I will update my earlier answer once I have a solution. Not sure when I'll have the time to dig into this again.
@its.me.adam I just updated my earlier answer with another approach of extracting the trace name / trace index mapping.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.