0

I am working on Shiny app. One of the plots in my app is an area plot of Chicago evictions over time. I have permanent annotations on the plot for the peak and most recent values in the data (2012 and 2019, respectively). I would like to make it so that when you hover over the plot with your mouse, another annotation for whatever year your mouse is closest to also appears. Right now, the annotations based on the hover input flash briefly and then disappear.

Here's a paired-down version of the app with just the troublesome plot so you can see its behavior, and here's the relevant code:

# In ui
plotOutput("evict_plot", hover = "hover")

# Current code
output$evict_plot <- renderPlot({
    yr <- ifelse(is.null(input$hover), 0, round(input$hover$x))
    evict_base + 
      {if(yr == 2010)annotate("text", x = 2010.57, y = 25296, 
                                    label = 'atop("23,058 evictions", bold("81.1% back rent cases"))',
                                    family = "lato", parse = TRUE)} +
      < additional if statements >

I thought using a reactive value might solve the problem...

# Attempt using reactive value
output$evict_plot <- renderPlot({
    yr <- reactiveVal()
    yr(0)
    observeEvent(input$hover,
                 yr(round(input$hover$x)))
    evict_base + 
      {if(yr() == 0 | yr() == 2010)annotate("text", x = 2010.57, y = 25296, 
                                    label = 'atop("23,058 evictions", bold("81.1% back rent cases"))',
                                    family = "lato", parse = TRUE)} +
      < additional if statements >

But it didn't. When I first open the app with the reactive value implementation, 2010 was annotated, as I would expect per the code above:

Chicago evictions over time

But, as soon as I hovered my mouse over the plot, the app started to hang:

Chicago evictions over time, Shiny hanging

Does anyone have ideas about how I can use the hover input to add an annotation for a year that stays put until you move your mouse closer to a different year (vs. just flashing briefly, like it does now)? I'm going to experiment with hoverOpts, but I feel like there should we a way to do it with just the basic hover syntax, since I don't need any of the additional functionality overOpts offers.

Thank you!

3
  • 1
    Can you share a piece of the data, and the code to produce the ggplot? Commented Mar 5, 2024 at 8:12
  • Sure thing! I just created a GitHub repo with the data and paired-down app code: github.com/fvescia/hover-trouble/tree/main Commented Mar 5, 2024 at 17:13
  • I think this is not possible to add a ggplot::annotate on hover. Don't you want to use plotly? Otherwise I can (try to) show you how to do with a HTML annotation. Commented Mar 5, 2024 at 17:44

1 Answer 1

1

Here is a way with HTML annotations:

library(dplyr)
library(tidyr)
library(ggplot2)
library(shiny)

#evictions <- readr::read_csv("https://raw.githubusercontent.com/fvescia/hover-trouble/main/data/eviction_data_tract.csv")

back_rent <- evictions %>%
  pivot_longer(col = c("case_type_single_action", "case_type_joint_action"),
               names_to = "back_rent",
               values_to = "comp_cases") %>%
  select(filing_year, tract, back_rent, comp_cases) %>%
  group_by(filing_year, back_rent) %>%
  summarize(comp_cases = sum(comp_cases)) %>%
  mutate(back_rent = case_when(
    back_rent == "case_type_single_action" ~ as.factor("No"),
    back_rent == "case_type_joint_action" ~ as.factor("Yes")
  ))

back_rent2 <- back_rent %>%
  group_by(filing_year) %>%
  reframe(
    y = rep(sum(comp_cases), 2),
    percent = rep(comp_cases[1] * 100 / sum(comp_cases), 2)
  )

gg <- ggplot(back_rent, aes(x = filing_year, y = comp_cases)) +
  geom_area(aes(fill = back_rent)) +
  scale_x_continuous(limits = c(2010, 2020.75),
                     breaks = seq(from = 2010, to = 2019, by = 1), expand = c(0, 0)) +
  scale_y_continuous(breaks = seq(from = 0, to = 25000, by = 7500)) +
  scale_fill_manual(values = c("No" = "#fce39e", "Yes" = "#fccb41")) +
  annotate("text", x = 2012, y = 27000,
           label = 'atop("24,762 evictions", bold("80.97% back rent cases"))',
           family = "lato", parse = TRUE) +
  annotate("text", x = 2019, y = 17822,
           label = 'atop("15,584 evictions", bold("78.81% back rent cases"))',
           family = "lato", parse = TRUE) +
  labs(y = "Completed eviction filings", fill = "Back rent case") +
  theme(
    text = element_text(family = "lato"),
    panel.background = element_blank(),
    axis.ticks = element_blank(),
    axis.title.x = element_blank(),
    legend.position = "none"
  )


css <- HTML(
  "
  .annotation {
    position: absolute;
  pointer-events:none;
  z-index: 100;
  background: rgba(245,245,245,0.8);
  border: 1px solid black;
  border-radius: 5px;
  font-size: 16px;
}"
)

shinyApp(
  ui = fluidPage(
    tags$head(tags$style(css)),
    div(
      style = "position: relative;",
      plotOutput("ggplot", hover = hoverOpts("plot_hover")),
      uiOutput("hoverinfo")
    )
  ),

  server = function(input, output, session) {
    output$ggplot <- renderPlot({
      gg
    })

    output$hoverinfo <- renderUI({
      hover <- input[["plot_hover"]]
      if(is.null(hover)) return(NULL)
      point <- nearPoints(
        back_rent2, hover, threshold = 50, maxpoints = 1, yvar ="y"
      )
      if(nrow(point) == 0) return(NULL)
      left_pct <-
        (point[["filing_year"]] - hover$domain$left) /
        (hover$domain$right - hover$domain$left)
      top_pct <-
        (hover$domain$top - point[["y"]]) /
        (hover$domain$top - hover$domain$bottom)
      left_px <-
        (hover$range$left + left_pct * (hover$range$right - hover$range$left)) /
        hover$img_css_ratio$x
      right_px <- hover$range$right / hover$img_css_ratio$x - left_px
      top_px <-
        (hover$range$top + top_pct * (hover$range$bottom - hover$range$top)) /
        hover$img_css_ratio$y
      t <- 18/2*3 + 5
      style <-
        ifelse(top_px>t,
               paste0(ifelse(left_pct<0.5,
                             paste0("left: ", left_px + 9 + 5),
                             paste0("right: ", right_px + 9 + 10)), "px; ",
                      "top: ", top_px - t, "px;"),
               paste0("top: ", top_px + 9 + 5, "px; ",
                      "left: ", left_px - 50, "px; ",
                      "min-width: 100px;"))

      tooltip <- HTML(
        sprintf("%s evictions", point[["y"]]), "<br/>",
        sprintf(
          "<b>%s%% back rent cases</b>",
          formatC(point[["percent"]], format = "f", digits = 1)
        )
      )
      div(
        class = "annotation",
        style = style,
        p(tooltip)
      )
    })
  }
)

enter image description here

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks so much for working out an HTML solution, Stéphane! I don't know Plotly (yet!), and this project is due imminently, so ggplot was the way to go for now, but it's helpful to know Plotly is worth considering for any similar projects I do in the future. Thanks again!

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.