rplotlyhovertooltipstacked-area-chart

When using plotly in R with "hovermode = 'x'" enabled, is it possible to skip hoverinfo when y = 0 or y = NA?


I'm using R/plotly to create a stacked filled area plot summarizing several accounts opened at different times, whose balances sometimes go to zero. Here's a simplified example using three accounts ("X", "Y", and "Z"):

df <- data.frame(cbind(
    year <- c("2022-01-01","2022-02-01","2022-03-01","2022-04-01","2022-05-01","2022-06-01","2022-07-01","2022-08-01","2022-09-01","2022-10-01","2022-11-01","2022-12-01"),
    x <- c(NA,NA,NA,1,2,3,2,3,4,5,3,4),
    y <- c(1,2,2,1,2,4,3,2,NA,NA,NA,NA),
    z <- c(NA,NA,NA,1,2,5,6,5,5,NA,NA,NA)
)) %>%
    setNames(c("DATE","X","Y","Z")) %>%
    mutate(DATE = as.Date(DATE, format = "%Y-%m-%d")) %>%
    mutate(X = as.numeric(X), Y = as.numeric(Y), Z = as.numeric(Z)) %>%
    pivot_longer(., -DATE, names_to = "LEGEND", values_to = "QUANTITY") %>%
    mutate(LEGEND = factor(LEGEND, levels = c("X","Y","Z")))

When I create a graph using plotly, I prefer to use the hovermode = "x" parameter to compare all quantities at a given time at once. However, when hovering over periods when an account is empty (i.e., QUANTITY = NA), I still get a tooltip box with hoverinfo showing y = 0. With lots of accounts, this can clutter things up pretty quickly.

dfplot <- plot_ly(data = df, x = ~DATE, y = ~QUANTITY, color = ~LEGEND, type = "scatter", mode = "none", stackgroup = "one", fill = "tonexty") %>%
    layout(
        autosize = TRUE,
        yaxis = list(range = c(0,15)),
        hovermode = "x",
        showlegend = TRUE
    )
dfplot

X and Z are given values of "0", despite being NA

Is there a way to get rid of these extra tooltips? Or to conditionally skip hoverinfo if y = 0 or NA?

I've been able to skip hoverinfo for entire traces, and I have been able to get the behavior I want with simple scatter traces (y = 0 is ignored in these situations). I'm not sure if this is something that is specific to stacked filled area plots or not. Any help would be greatly appreciated!


Solution

  • Update

    As I stated in my comment. I thought you meant the Y trace, not y for each trace. Here is my update.

    plot_ly(data = df, x = ~DATE, y = ~QUANTITY, color = ~LEGEND, type = "scatter", 
            mode = "none", stackgroup = "one", fill = "tonexty") %>%
      layout(autosize = TRUE, yaxis = list(range = c(0, 15)), hovermode = "x",
             showlegend = TRUE) %>%  
      htmlwidgets::onRender("function(el){
        el.on('plotly_hover', function(d) {
          out = [];          /* traces to drop from hover */
          keep = [];         /* traces to keep in the hover */
          fixer = [2, 1, 0]; /* data order Z, X, Y; trace order X, Y, Z */
          for(i = 0; i < 3; i++) {  /* which traces stay? which go? */
            j = fixer[i];
            if(d.points[i].y == 0) {out.push(j);}
            else {keep.push(j);}
          }
          if(out.length != 0) {                    /* drop from hover */
            Plotly.restyle(el.id, {hoverinfo: 'none', hovertemplate: null}, out);
          }
          if(keep.length > 0 && keep.length < 3) { /* keep */
            Plotly.restyle(el.id, {hoverinfo: 'all', hovertemplate: '%{y}'}, keep);
          } else if(keep.length == 3) {             /* keep */
            Plotly.restyle(el.id, {hoverinfo: 'all'}, keep);
          }
        })
      }")
    

    enter image description here enter image description here

    enter image description here


    originally

    I can drop the 'Y' trace from the x-unified hover, but it also drops the x-axis. I tried many different ways to get the x-axis back, but I haven't found a way that works.

    This uses the library htmlwidgets. However, since it's only for one function, I just appended it to the function. (I didn't call the library.)

    I've piped the function from the graph but did not change any of your code.

    Here's what you can do to drop the Y trace where there is no or a 0 value.

    plot_ly(data = df, x = ~DATE, y = ~QUANTITY, color = ~LEGEND, type = "scatter", 
            mode = "none", stackgroup = "one", fill = "tonexty") %>%
      layout(autosize = TRUE, yaxis = list(range = c(0, 15)), hovermode = "x",
             showlegend = TRUE) %>%  
      htmlwidgets::onRender("function(el){
        el.on('plotly_hover', function(d) {
          if(d.points[1].y == 0) {
              hinfo = {hoverinfo: 'none'};
          } else {
              hinfo = {hoverinfo: 'all'};
          }
          Plotly.restyle(el.id, hinfo, 1);
          Plotly.restyle(el.id, {hoverinfo: 'all', hovertemplate: '%{y}'}, [0, 2]);
        })
      }")
    

    enter image description here enter image description here

    enter image description here

    enter image description here

    You had assigned the graph to dfplot. You can always just pipe the function I provided to the plot after you named it like this (it does the same thing).

    dfplot %>% 
      htmlwidgets::onRender("function(el){
        el.on('plotly_hover', function(d) {
          if(d.points[1].y == 0) {
              hinfo = {hoverinfo: 'none'};
          } else {
              hinfo = {hoverinfo: 'all'};
          }
          Plotly.restyle(el.id, hinfo, 1);
          Plotly.restyle(el.id, {hoverinfo: 'all', hovertemplate: '%{y}'}, [0, 2]);
        })
      }")
    

    By the way, I noticed a few things in your data creation that might make things a bit easier for you. If you already know this, just ignore me!

    Odd things happen when you use <- inside of a function. When creating an object, use either; assignment inside a function use =.

    In my example, note that cbind and setNames weren't used. Additionally, X, Y, and Z were transformed to numeric in one call. ., isn't in pivot_longer. Lastly, the last mutate was replaced with the argument names_transform in pivot_longer.

    df1 <- data.frame(
      DATE = c("2022-01-01","2022-02-01","2022-03-01","2022-04-01","2022-05-01",
               "2022-06-01","2022-07-01","2022-08-01","2022-09-01","2022-10-01",
               "2022-11-01","2022-12-01"),
      X = c(NA,NA,NA,1,2,3,2,3,4,5,3,4),
      Y = c(1,2,2,1,2,4,3,2,NA,NA,NA,NA),
      Z = c(NA,NA,NA,1,2,5,6,5,5,NA,NA,NA)) %>%
      mutate(DATE = as.Date(DATE, format = "%Y-%m-%d"), 
             across(X:Z, as.numeric)) %>%
      pivot_longer(-DATE, names_to = "LEGEND", values_to = "QUANTITY", 
                   names_transform = list(LEGEND = as.factor))
    all.equal(df, df1) # [1] TRUE