Search code examples
rfunctionggplot2automationplotly

Creating buttons for a dropdown automatically with Plotly


I have a dataframe listing the number of individual engagements made with 49 different committees over 5 years. An example with a reduced number of categories is provided below:

library(dplyr)
df <- data.frame(Year = c(2019, 2020, 2021, 2022, 2023),
                 Committees = c("House Foreign Affairs",
                                "House Foreign Affairs",
                                "House Foreign Affairs",
                                "House Foreign Affairs",
                                "House Foreign Affairs",
                                "House Judiciary",
                                "House Judiciary",
                                "House Judiciary",
                                "House Judiciary",
                                "House Judiciary",
                                "Senate Appropriations",
                                "Senate Appropriations",
                                "Senate Appropriations",
                                "Senate Appropriations",
                                "Senate Appropriations",
                                "Senate Oversight",
                                "Senate Oversight",
                                "Senate Oversight",
                                "Senate Oversight",
                                "Senate Oversight"), 
                 n = c(4,8,4,6,2,7,4,8,2,8,1,4,3,6,4,8,4,3,8,7))

df <- df %>% 
  mutate(across(c(1:2), factor)) %>%
  arrange(Year)

I made a plot to visualize the change in total engagements over these five years with the code below:

library(ggplot2)
plot <- df %>%
ggplot(aes(Year, n, group = Committees, color = Committees)) +
  geom_line() +
  geom_point() +
  theme_bw ()

ggplot line chart

This already looks quite cluttered, and with 49 categories the plot with the original data looks like this.

pain

So, I am trying to turn the plot into a plotly to make the lines more visible, to compress the legend into a dropdown, and to allow for some level of interactivity for my users.

library(plotly)
plot %>%
  ggplotly(x = ~date, y = ~median,
           split = ~city,
           frame = ~frame,  
           type = 'scatter',
           mode = 'lines')

Now, the problem is that making the buttons for 4 categories is simple enough with the code provided here. Also, this answer tackled the problem of having to create 49 different buttons per each category, and this answer further expanded it. However, I am not very well versed with plotly, and the code I built based on these answers just outputs: Error: object 'y_axis_var_names' not found

This is what I ended up with. If you have better methods to make the graph interactive and allow the selection of individual lines, please feel free to suggest it.

create_buttons <- function(df, y_axis_var_names) {
  lapply(
    y_axis_var_names,
    FUN = function(var_name, df) {
      button <- list(
        method = 'restyle',
        args = list(list(y = list(df[, var_name]),x = list(df[, var_name]))),
        label = sprintf('Show %s', var_name)
      )
    },
    df
  )
  
}

y_axis_var_names <- c("House Foreign Affairs",
                        "House Judiciary",
                        "Senate Appropriations",
                        "Senate Oversight")

plot %>%
  ggplotly(type = 'scatter',
           mode = 'lines') %>%
  layout(xaxis = list(domain = c(0.1, 1)),
         yaxis = list(title = "y"),
         updatemenus = list(
      list(
        y = 0.7,
        buttons = create_buttons(plot, y_axis_var_names))))

Solution

  • General Issue

    There is a major issue with your code and particular your usage of ggplotly:

    The idea is that you pass a ggplot object to it and it transforms it into a plotly object.

    The function plot_ly on the other hand creates a plotly object from the data and you have to specify what maps to what.

    Your usage looks like the latter (i.e. mapping by yourself), but using ggplotly is wrong here. This is, by the way, the reason why this example can be reproduced despite lacking some needed columns, because ggplotly has all the information it needs from the ggplot object and chooses to ignore additional parameters which could not be resolved anyways (city, frame, median are nowhere defined in your example)

    Potential Solution

    From what I understand: you want a dropdown with which you can select which lines to plot? If this is the case you can indeed use some custom UI elements to do so and here's an example of how to do that:

    library(plotly)
    
    set.seed(29022024)
    ex <- expand.grid(year = 2019:2023, cat = LETTERS)
    ex$n <- rpois(nrow(ex), 10)
    
    
    ## All categories look cramped
    
    plot_ly(ex) %>%
      add_trace(x = ~ year, y = ~ n, color = ~ cat, type = "scatter", mode = "line",
                colors = "viridis")
    
    ## Add Buttons to Toggle Traces
    ply <- plot_ly(ex) %>%
      add_trace(x = ~ year, y = ~ n, color = ~ cat, type = "scatter", mode = "line",
                colors = "viridis", visible = ~ cat %in% LETTERS[1:5])
    
    btns <- lapply(levels(ex$cat), \(.x) {
      list(method = "restyle",
           label = paste("Toggle", .x),
           args = list(list(visible = !.x %in% LETTERS[1:5]), 
                       list(which(levels(ex$cat) == .x) - 1L)),
           args2 = list(list(visible = .x %in% LETTERS[1:5]), 
                        list(which(levels(ex$cat) == .x) - 1L)))
    })
    
    ply %>%
      layout(
        updatemenus = list(
          list(
            y = 0.8,
            yanchor = "top",
            buttons = btns
          )
        )
      )
    

    A click on the buttons hides / shows the trace, respectively. In the beginning I just show the first 5 traces to de-clutter from the beginning. Then, with the buttons I can decide to add remove traces.

    Update

    As per the comments if you want to show only one trace at a time you adapt your code as follows:

    ply <- plot_ly(ex) %>%
      add_trace(x = ~ year, y = ~ n, color = ~ cat, type = "scatter", mode = "line",
                colors = "viridis", visible = ~ cat == "A")
    
    btns <- lapply(levels(ex$cat), \(.x) {
      list(method = "restyle",
           label = paste("Toggle", .x),
           args = list(list(visible = LETTERS == .x))
      )
    })
    
    ply %>%
      layout(
        updatemenus = list(
          list(
            y = 0.8,
            yanchor = "top",
            buttons = btns
          )
        )
      )
    

    Screenshots

    Add Traces

    Plotly Line Chart with Buttons in a GIF which adds/removes traces upon click

    One Trace a Time

    Plotly Line Chart with Buttons in a GIF which hides/shows traces upon click