Hover tooltip in Shiny using pure ggplot2 solution - r

The goal here is to render a tooltip on hover using a pure ggplot2 solution without any plotly of javascript hacks.
Here is a naive tentative solution (that does not work)
library(tidyverse)
library(shiny)
shinyApp(
ui = fluidPage(
plotOutput("plotCars", hover="hover", width=700,height=300),
verbatimTextOutput("info")),
server = function(input, output) {
hovered <- reactive(nearPoints(mtcars, input$hover, maxpoints = 1) %>%
rownames_to_column("model"))
output$plotCars <- renderPlot({
ggplot(mtcars, aes(x=wt, y=mpg)) +
geom_point() +
geom_point(color="red",data=hovered()) +
geom_label(aes(label=model),data=hovered(),
hjust="inward",vjust="inward",
size=4,color="red",alpha=0.5)+
xlab("Weight(1000 lbs)")+ylab("Miles/gallon")
})
output$info <- renderPrint({
hovered()
})
})
The problem here is that as soon as the plot is re-rendered including the hover information (e.g. a label), the hover event is automatically reset to NULL, thus invalidating the plot.
In practice the above solution almost works, the tooltip for the hovered point is briefly shown, but immediately the input$hover event is invalidated by the new plot and the re-rendering of the plot removed the tooltip since a that point the hover event is now NULL. In fact the tooltip blinks once and then disappears.

The solution is to keep the previous value of the hovered data point avoiding the invalidation. This objective can be achieved using the observeEvent() method and a reactiveVal(). The solution works like this:
the hovered point information is a reactive value (reactiveVal()), initialized with a zero-row tibble with the same columns as the plotted data set.
This initial value allow a smooth visualization in a ggplot2 layer, that would not be possible initializing it to NULL.
the value is updated in response to an hover event, the function observeEvent() by default ignores when an event become NULL (ignoreNULL = TRUE), therefore when the input$hover is invalidated to NULL the value is not updated and remain the same as before
in the plot rendering, the hovered() value is initially a zero-row tibble (but still having the right columns to be compatible with the plot default data) thus not showing anything, later when an hover near a point is performed it will contain the point information.
library(tidyverse)
library(shiny)
shinyApp(
ui = fluidPage(
plotOutput("plotCars", hover="hover", width=700,height=300),
verbatimTextOutput("info")),
server = function(input, output) {
hovered <- reactiveVal(mtcars %>% filter(FALSE) %>% rownames_to_column("model"))
observeEvent(input$hover, {
hovered(nearPoints(mtcars, input$hover, maxpoints = 1) %>%
rownames_to_column("model"))
})
output$plotCars <- renderPlot({
ggplot(mtcars, aes(x=wt, y=mpg)) +
geom_point() +
geom_point(color="red",data=hovered()) +
geom_label(aes(label=model),data=hovered(),
hjust="inward",vjust="inward",
size=4,color="red",alpha=0.5)+
xlab("Weight(1000 lbs)")+ylab("Miles/gallon")
})
output$info <- renderPrint({
hovered()
})
})

Related

R Shiny: How to temporarily disable reactivity?

I am building a UI containing DT tables and sliders (both as inputs), as well as plot outputs. The tables are used to make a selection out of several. The user can only select one cell to make a choice.
I want the user to be able to store the setting of tables and sliders because they are quite complex. The idea is that the user can then switch back and forth between two stored settings, for example, and see how the resulting plots change. When a user restores a setting, the tables and sliders get updated, which updates the plot(s).
The problem is that the plot is not updated once, but usually twice. It seems that there is a delay somewhere in the logic, causing Shiny to first react to the update of the sliders, then to the update of the tables, so that the plot is re-plotted in two steps. This is very annoying for two reasons: (1) it causes the calculation to re-run twice, making the app react twice as slow and (2) it's impossible to see the changes directly in the plot because the original plot is first replaced by an intermediate plot which has no meaning to the user.
To illustrate the problem, I created this minimum working example, where I reduced complexity to just one table and one slider. I added a 3 second Sys.sleep to simulate a long calculation because obviously one would not see the problem otherwise:
library(shiny)
library(DT)
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
actionButton("button", "Preset"),
# No problem with selectInput:
# selectInput("select", "x", choices = names(iris)[1:4], selected = "Sepal.Length"),
DT::dataTableOutput("table"),
sliderInput("slider", "bins", min = 1, max = 50, value = 30)
),
mainPanel(
plotOutput("distPlot")
)
)
)
server <- function(input, output, session) {
observeEvent(input$button, {
# updateSelectInput(session, "select", selected = "Petal.Width")
selectRows(DT::dataTableProxy("table"), 4)
updateSliderInput(session, "slider", value = 15)
})
output$table <- DT::renderDataTable(
DT::datatable(
data.frame(x = names(iris)[1:4]),
rownames = FALSE,
selection = "single",
options = list(searching = FALSE, paging = FALSE, info = FALSE, ordering = FALSE)
)
)
output$distPlot <- renderPlot({
req(input$table_rows_selected)
# x <- iris[[input$select]]
x <- iris[[input$table_rows_selected]]
bins <- seq(min(x), max(x), length.out = input$slider + 1)
# Simulate long calculation:
Sys.sleep(3)
hist(x, breaks = bins, col = 'darkgray', border = 'white')
})
}
shinyApp(ui = ui, server = server)
Clicking first on the cell "Sepal.Length" in the table, then on the button "Preset" will load the preset and demonstrate the problem.
It seems that this is a timing issue/race condition, because sometimes, it works OK and the plot is updated only once (only in the minimal example, not the actual app). Usually the first time after starting the app. But in that case, just click on "Sepal.Length" again and change the slider position, then click on the "Preset" button and usually the plot will update twice.
I noticed that the problem does not appear when I replace the table with a selectInput. But the tables have a certain meaning: they stand for morphological fields (see package morphr), so I'd rather stick with tables to have the right appearance.
I could obviuously also disable reactivity using isolate() as suggested here: R Shiny: how to prevent duplicate plot update with nested selectors? and then e.g. introduce a button "Update plot". But I would prefer to keep the app reactive to changes in the sliders and tables, because that's a very useful user experience and one reason for me to use Shiny instead of PHP/python/etc.
My first idea to solve the problem was to introduce a reactive value:
server <- function(input, output, session) {
updating <- reactiveVal(FALSE)
# ...
}
then change the value before and after the updates to the inputs:
observeEvent(input$button, {
updating(TRUE)
selectRows(DT::dataTableProxy("table"), 4)
updateSliderInput(session, "slider", value = 15)
updating(FALSE)
})
and add an if statement in the renderPlot() code, e.g. with validate:
output$distPlot <- renderPlot({
validate(need(!updating(), ""))
# ...
})
But that has no effect, because the entire code in the observeEvent(input$button) runs first, setting updating to TRUE and immediately back to FALSE. But the code inside renderPlot() is executed later (after the invalidation has occurred) and updating is always FALSE when it runs.
So, at the moment I have few ideas how to solve this. It would be best if one could somehow disable reactivity for the plot, then update the inputs, enable reactivity again and trigger a plot update programmatically. But is this possible?
Any other ideas for a workaround?
I'm not sure to understand the issue. Does this solve the problem:
library(shinyjs)
ui <- fluidPage(
useShinyjs(),
......
observeEvent(input$button, {
runjs("Shiny.setInputValue('slider', 15); Shiny.setInputValue('table_rows_selected', 4);")
selectRows(DT::dataTableProxy("table"), 4)
updateSliderInput(session, "slider", value = 15)
})

Shiny Interactive Graph plot showing row names

I am using Shiny and ggplot2 for an interactive graph. I also used "plot1_click" to get x and y locations.
output$selected <- renderText({
paste0("Value=", input$plot1_click$x, "\n",
"Names=", input$plot1_click$y) }) #how to get names???
This is part of server code. Here what I want is, instead of printing the "y" coordinates, I want to print the corresponding name written in y-axis. Is there any possible way for it??
As far as I know clicking points is not supported in plotOutput. Click events will only return coordinates of the click location. Those coordinates can however be used to figure out the nearest point.
This shiny app from the shiny gallery pages uses the function shiny::nearPoints which does exactly that. Here is a minimal example.
library(shiny)
library(ggplot2)
shinyApp(
fluidPage(
plotOutput("plot", click = "plot_click"),
verbatimTextOutput('print')
),
server = function(input, output, session){
output$plot <- renderPlot({ggplot(mtcars, aes(wt, mpg)) + geom_point()})
output$print = renderPrint({
nearPoints(
mtcars, # the plotting data
input$plot_click, # input variable to get the x/y coordinates from
maxpoints = 1, # only show the single nearest point
threshold = 1000 # basically a search radius. set this big enough
# to show at least one point per click
)
})
}
)
The verbatimTextOutput shows you the nearest point to the clicked location. Notice that nearPoints only works with ggplots like that. But the help page suggests that there is also a way to use this with base graphics.

R Shiny application: Modifying plot without re-rendering it

I've been looking into ways to update a plot within an R Shiny application without having to re-render the whole plot. I'm working with temporal data which is animated via a Slider Input (animationOptions(playButton = TRUE)). The idea is to somehow highlight the part of the plot which is selected via the Slider Input. Re-rendering the whole plot at every animation step would make the whole application uselessly slow.
The most elegant solution with ggplot2 would have been, if shiny offered a way to add layers to the ggplot (e.g. + geom line()) and integrated this layer seamlessly into the plot without re-rendering it. Sadly, this does not seem to work. A bit of a hack could include creating a second ggplot-instance with exactly the same x/y-dimensions and overlapping the two plots.
EDIT:
I've just learnt that there are more javascript oriented plotting methods than ggplot2. For example, using dygraphs and adding a layer of dyShading, the selected area gets highlighted nicely. The basic question remains the same though, since changing the start- and end values of dyShading() seems to require re-rendering the whole plot.
library(shiny)
library(dygraphs)
library(xts)
data <- data.frame(
datetime = as.POSIXct("2016-06-20 17:00:00", tz = "UTC") + 1:100*60,
y = rnorm(100)
)
data_xts <- as.xts(data[,-1], data[,1])
minDatetime <- min(data$datetime)
maxDatetime <- max(data$datetime)
minY = min(data$y)
maxY = max(data$y)
plotlimits <- lims(x = c(minDatetime, maxDatetime), y = c(minY, maxY))
ui <- fluidPage(
sliderInput("timeslider", "Time Slider",
min = minDatetime,
max = maxDatetime,
value = c(minDatetime, minDatetime+10*60),
animate = animationOptions(interval=200)
),
dygraphOutput("dyplot")
)
server <- function(input, output) {
data_fil <- reactive({
data[data$datetime <= input$timeslider[2] & data$datetime >= input$timeslider[1],]
})
output$dyplot <- renderDygraph({
dygraph(data_xts) %>%
dyShading(
from = as.character(input$timeslider[1]),
to = as.character(input$timeslider[2]),
color = "tomato")
})
}
shinyApp(ui = ui, server = server)

Conditional reactivity Shiny

Reactive expressions in Shiny propagate changes where they need to go. We can suppress some of this behaviour with isolate, but can we suppress changes being propagated based on our own logical expression?
The example I give is a simple scatterplot, and we draw a crosshair with abline where the user clicks. Unfortunately, Shiny considers the result to be a new plot, and our click value is reset to NULL... which in turn is treated as an update to the value to be propagated as usual. The plot is redrawn, and NULL is passed to both arguments of abline.
My hack (commented out below) is to place a condition in the renderPlot call which updates some non-reactive variables for the plotting coordinates, only when the click values are non-NULL. This works fine for trivial plots, but it actually results in the plot being drawn twice.
What's a better way to do this? Is there a correct way?
Server file:
library(shiny)
shinyServer(function (input, output)
{
xclick <- yclick <- NULL
output$plot <- renderPlot({
#if (!is.null(input$click$x)){
xclick <<- input$click$x
yclick <<- input$click$y
#}
plot(1, 1)
abline(v = xclick, h = yclick)
})
})
UI file:
library(shiny)
shinyUI(
basicPage(
plotOutput("plot", click = "click", height = "400px", width = "400px")
)
)
Winston calls this problem "state" accumulation - you want to display not only the current data, but something generated by the previous plot (the best place to learn about this is at https://www.rstudio.com/resources/videos/coordinated-multiple-views-linked-brushing/)
The basic idea is to create your own set of reactive values, and update them when the user clicks on the plot. They won't be invalidated until the next click, so you don't get circular behaviour.
library(shiny)
shinyApp(
shinyUI(basicPage(plotOutput("plot", click = "click"))),
function(input, output) {
click <- reactiveValues(x = NULL, y = NULL)
observeEvent(input$click, {
click$x <- input$click$x
click$y <- input$click$y
})
output$plot <- renderPlot({
plot(1, 1)
abline(v = click$x, h = click$y)
})
}
)

R Shiny: Invalidate data periodically in reactive() yet not force DB call first time

I have a plot resulting from a DB query. I load data to refresh in the plot in the shiny app. To do this, I am using a combination of reactive() and invalidateLater(), but it is forcing a data load even the first time the plot is rendered. Is there any way to make the app use pre-fetched data for the plot, and invalidate/reactive cycle for background processing?
Sample code below (replace mtcars with some function that forces a long DB query):
library(shiny)
ui <- fluidPage(mainPanel(plotOutput('mpg')))
server <- function(input, output) {
output$mpg <- renderPlot({
p <- ggplot(mtcarsReactive(), aes(x = as.factor(cyl), y = mpg)) +
geom_boxplot()
print(p)
})
mtcarsReactive <- reactive({
invalidateLater(60000)
mtcars
})
}
shinyApp(ui = ui, server = server)
One way to do it is to rely on a global variable. You can define a global variable outside the ui/server functions (for example, firstRun <- 1). Then in your reactive
mtcarsReactive <- reactive({
invalidateLater(600)
if (first == 1) {
first <<- first + 1
return(mtcars)
} else {
return(diamonds)
}
})
Note the <<- assignment, which will assign the value to the global variable, instead of creating a new local variable (if you use normal <-).
I tested this under a newer version of Shiny than yours (because mine requires a session object in invalidateLater), but hopefully it works on your environment.

Resources