Toggling Error Bars in Plotly

Plotly charts can be enhanced with custom controls. One use of controls is to update an existing chart. This post will show how to add a couple of buttons to a chart, toggling error bars on and off. It should be noted that coloring by group affects the order (and hence validity) of error bars in a Plotly plot so a workaround has to be employed.

Generate Some Demo Data

1set.seed(12345)
2d <- data.frame(
3 time = rep(0:9, 10),
4  group = sample(paste0("Group_", seq(5)), size = 1000, replace = TRUE),
5  value = rnorm(n = 1000) + 1
6) |>
7  dplyr::group_by(time, group) |>
8  dplyr::summarise(mean = mean(value), se = sd(value) / sqrt(dplyr::n())) |>
9  dplyr::ungroup()

Initializing Chart and Adding Traces

We'll build up the chart from the demo data by first initializing an empty plot as follows:

1p <- plotly::plot_ly(type = 'scatter', mode = 'lines+markers')

The next step is to add a series of traces, one for each group, without error bars:

1for (x in unique(d$group)) {
2  p <- p |>
3    plotly::add_trace(data = d |> dplyr::filter(group == x), visible = TRUE,
4                      x = ~time, y = ~mean, type = 'scatter', mode = 'lines+markers', color = ~group)
5} 

and then a second series of traces, this time with error bars but with the parameter visible=FALSE set to ensure that they are not visible when the chart is initially drawn:

1for (x in unique(d$group)) {
2  p <- p |>
3    plotly::add_trace(data = d |> dplyr::filter(group == x), visible = FALSE,
4                      x = ~time, y = ~mean, type = 'scatter', mode = 'lines+markers', color = ~group,
5                      error_y = ~list(array = se, color = group))
6}

Since we have 5 groups, our plotly chart now contains 5 traces without error bars and 5 traces with error bars. It actually contains one additional empty trace corresponding to the initial plotly::plot_ly() call when the empty chart was built. This can be deduced using plotly::plotly_build() to observe the list object sent to plotly for plotting. At this stage the following code reveals a total of 11 traces:

1p_obj <- plotly::plotly_build(p)
2print(length(p_obj$x$data))

Building the Menu Buttons

Plotly does not have the option of a toggle switch or toggle button so we'll add two buttons - one to plot without error bars and one to plot with error bars. The control works by changing the visible status so that only a subset of plots are visible. As mentioned above, we have 5 traces without error bars and 5 with. We also have the empty trace (the first trace), so in order to see just traces without error bars we'll want to show only traces 2-6 and in order to see just traces with error bars we'll want to show only traces 7-11. Here is the code to build the plotly menu buttons:

 1num_traces <- length(unique(d$group))
 2menu <- list(
 3  active = 0,
 4  type = 'buttons',
 5  direction = 'right',
 6  x = 1.2,
 7  y = 0.1,
 8  buttons = list(
 9    list(
10      label = 'off',
11      method = 'update',
12      args = list(list(visible = c(F, rep(c(TRUE, FALSE), each = num_traces))))
13    ),
14    list(
15      label = 'on',
16      method = 'update',
17      args = list(list(visible = c(F, rep(c(FALSE, TRUE), each = num_traces))))
18    )
19  )
20)
21
22annotation <- list(list(text = "Error Bars", x = 1.18, y = 0.13, xref = "paper", yref = "paper", showarrow = FALSE))

Plotting

Finally, the plot can be created by adding the menu items and annotation to the existing plotly object:

1p |>
2  plotly::layout(updatemenus = list(menu), annotations = annotation)