Passing new traces to plotly for use with interactive buttons in user-defined function

I would like to pass new traces to plotly::add_lines in a programmatic way, so that buttons appear for each new trace. For example, say my data frame is this:

df <- data.frame(var = rep(c("A","B","C","D"), each = 20),
  value = rnorm(80),
  time = rep(c(1:20),4)) %>%
  tidyr::spread(var, value)

To get buttons on my plotly figure, I would use updatemenus within the plotly::layout function as follows:

updatemenus <- list(
  list(
    active = -1,
    type= 'buttons',
    direction = "right",
    xanchor = 'center',
    yanchor = "top",
    pad = list('r'= 0, 't'= -25, 'b' = 0),
    buttons = list(
      list(
        label = "A",
        method = "update",
        args = list(list(visible = c(T, F, F, F)),
                    list(title = "A"))),
      list(
        label = "B",
        method = "update",
        args = list(list(visible = c(F, T, F, F)),
                    list(title = "B"))),
      list(
        label = "C",
        method = "update",
        args = list(list(visible = c(F, F, T, F)),
                    list(title = "C"))),
      list(
        label = "D",
        method = "update",
        args = list(list(visible = c(F, F, F, T)),
                    list(title = "D"))),
      list(
        label = "All",
        method = "update",
        args = list(list(visible = c(T, T, T, T)),
                    list(title = "All")))
      
    )
  )
)

Then, to display the figure with buttons for adding and subtracting lines:

df %>%  
  plot_ly(type = 'scatter', mode = 'lines') %>%
  add_lines(x=~time, y=~A, name="Data") %>%
  add_lines(x=~time, y=~B, name="Data") %>%
  add_lines(x=~time, y=~C, name="Data") %>%
  add_lines(x=~time, y=~D, name="Data") %>%
  layout(updatemenus=updatemenus)

What I would like to do is write a function that adds columns in a data.frame to the updatemenus list while also adding them to the plotly figure through new add_lines additions. Has anyone attempted this before? Is it possible to pass a list with this information to piped plotly functions?

Sean

Nice! This is almost a reprex. You know you can select and deselect plotly traces by clicking on the legend anyway? So adding buttons is a bit redundant. Also, I think you would be better to work with the gathered df, instead of spreading it.

Anyway, you can do what you want with a loop. You can build the plotly data structure bit by bit, like you can with a ggplot. For example

		p <- plot_ly(type = 'scatter', mode = 'lines')
		plotvars <- names(df)[2:ncol(df)]
		for (i in plotvars){
			temp <- df %>%
				rename(data=one_of(i)) %>%
				select(time, data)
			p <- p %>%
				add_lines(data=temp, x=~time, y=~data, name=i)
		}
		p %>%
			layout(updatemenus=updatemenus)
1 Like

Thanks! I'd rather use buttons here because they allow for automatically isolating one of many lines rather than deselecting a single line.

Your loop works great! Is it possible to do something similar for the updatemenus chunk? That is, to add trace names as list items that describe buttons.

I was able to figure it out! I created a function that writes column names into the correct list structure for creating buttons in a plotly figure. Apologies ahead of time if my method offends your programming sensibilities...there's likely an easier way.

library(plotly)
library(dplyr)
library(stringr)

df <- data.frame(var = rep(c("A","B","C","D"), each = 20),
                 value = rnorm(80),
                 time = rep(c(1:20),4)) %>%
  tidyr::spread(var, value)

# Add column names to a plotly figure as buttons by writing them into the correct list structure
updatemenu <- function(df){
  
  plotvars <- names(df)[2:ncol(df)]
  
  
  base_params <- 'list(
  list(
  active = -1,
  type= "buttons",
  direction = "right",
  xanchor = "center",
  yanchor = "top",
  pad = list("r"= 0, "t"= -25, "b" = 0),
  buttons = list(
  %s)
  )
  )'

  menu <- ""
  for (i in 1:length(plotvars)){
    #Create logical statement for which series to view on click
    col_id <- grep(plotvars[i], colnames(df))
    vis_logical <- c(F, rep(NA, length(plotvars)))
    vis_logical[col_id] <- T
    vis_logical[is.na(vis_logical)] <- F
    vis_logical <- paste0("c(",stringr::str_flatten(vis_logical, ","),")")
    
    menu_item <- sprintf('
      list(
        label = "%s",
        method = "update",
        args = list(list(visible = %s),
                    list(title = "%s")))',plotvars[i],
                         vis_logical,
                         plotvars[i])
    
    
    if (plotvars[i] != max(plotvars)){
      menu <- stringr::str_glue(stringr::str_glue(menu,menu_item),",")
    } else {
      menu <- stringr::str_glue(menu,menu_item)
    }
    
  }
  
  out <- sprintf(base_params, menu)
  
  return(out)
}


updated <- eval(parse(text = updatemenu(df)))


p <- plot_ly(type = 'scatter', mode = 'lines')
plotvars <- names(df)[2:ncol(df)]
for (i in 1:length(plotvars)){
  temp <- df %>%
    rename(data=one_of(plotvars[i])) %>%
    select(time, data)
  
    p <- p %>%
    add_lines(data=temp, x=~time, y=~data, name=i) 
}

p %>% 
  layout(updatemenus = updated)
1 Like

1000 points for using sprintf and stringr!

1 Like