An introduction to click events in Shiny
Some of the most sophisticated interactive data visualizations are based on the idea that user queries can themselves be defined visually. For example, to select a date range, we could directly interact with a time series plot, rather than relying on a slider input. Or, instead of a long dropdown menu of items, a user could select items by clicking on bars in a bar plot. There are many variations of this idea, but they all leverage graphical (rather than textual) displays to define queries. The advantage of this approach is that it increases information density – the selection inputs themselves encode data.
To implement this in Shiny, we first need a way of registering user
interactions on plots themselves. We will consider two types of plot interaction
mechanisms: clicks and brushes. These can be specified by adding click
or
brush
events to plotOutput
objects.
This creates a UI with a single plot on which we will be able to track user clicks,
ui <- fluidPage(
plotOutput("plot", click = "plot_click")
)
Here, plot_click
is an ID that can be used as input$plot_click
in the
server. We could name it however we want, but we need to be consistent
across the UI and server (just like ordinary, non-graphical inputs).
Before, we just needed to place the input$id
items within render
and
reactive
server components, and the associated outputs would automatically
know to redraw each time the value of any input was changed. Clicks are treated
slightly differently. We have to both (a) recognize when a click event has
occurred and (b) extract relevant information about what the click was referring
to.
For (a), we generally use observeEvent
,
observeEvent(
$plot_click,
input
... things to do when the plot is clicked ... )
This piece of code will be run anytime the plot is clicked.
For (b), we can use the nearPoints
helper function. Suppose the plot was
made using the data.frame x
. Then
nearPoints(x, input$click)
will return the samples in x
that are close to the clicked location. We
will often use a variant of this code that doesn’t just return the closeby
samples – it returns all samples, along with their distance from the
clicked location,
nearPoints(x, input$click, allRows = TRUE, addDist = TRUE)
We are almost ready to build a visualization whose outputs respond to graphical queries. Suppose we want a scatterplot where point sizes update according to their distance from the user’s click. Everytime the plot is clicked, we need to update the set of distances between samples and the clicked point. We then need to rerender the plot to reflect the new distances. This logic is captured by the block below,
server <- function(input, output) {
dist <- reactiveVal(rep(1, nrow(x)))
observeEvent(
input$plot_click,
dist(reset_dist(x, input$plot_click))
)
output$plot <- renderPlot({
scatter(x, dist())
})
}
The code above uses one new concept, the reactiveVal
on the first line of
the function. It is a variable that doesn’t directly depend on any inputs,
which can become a source node for downstream reactive
and render
nodes
in the reactive graph. Anytime the variable’s value is changed, all
downstream nodes will be recomputed. A very common pattern is use an
observeEvent
to update a reactiveVal
every time a graphical query is
performed. Any plots that depend on this value will then be updated. For
example,
<- reactiveVal(initial_val) # initialize the reactive value
val
observeEvent(
...some input event...
...do some computation...val(new_value) # update val to new_val
)
# runs each time the reactiveVal changes
renderPlot({
val() # get the current value of the reactive value
})
So, revisiting the dist
in the earlier code block, we see that it is
initialized as a vector of 1
’s whose length is equal to the number of rows of
x
. Everytime the plot is clicked, we update the value of dist
according to
the function reset_dist
. Finally, the changed value of dist
triggers a rerun
of renderPlot
. Let’s look at the full application in action. It makes a
scatterplot using the cars dataset and resizes points every time the plot is
clicked.
library(tidyverse)
library(shiny)
# wrapper to get the distances from points to clicks
reset_dist <- function(x, click) {
nearPoints(x, click, allRows = TRUE, addDist = TRUE)$dist_
}
# scatterplot plot with point size dependent on click location
scatter <- function(x, dists) {
x %>%
mutate(dist = dists) %>%
ggplot() +
geom_point(aes(mpg, hp, size = dist)) +
scale_size(range = c(6, 1))
}
ui <- fluidPage(
plotOutput("plot", click = "plot_click")
)
server <- function(input, output) {
dist <- reactiveVal(rep(1, nrow(mtcars)))
observeEvent(
input$plot_click,
dist(reset_dist(mtcars, input$plot_click))
)
output$plot <- renderPlot(scatter(mtcars, dist()))
}
shinyApp(ui, server)
The reset_dist
function uses nearPoints
to compute the distance between
each sample and the plot, each time the plot is clicked. The associated reactive
value dist
gets changed, which triggers scatterplot
to run, and it is
encoded using size in the downstream ggplot2 figure.
We can make the plot more interesting by outputting a table showing the
original dataset. Using the same dist()
call, we can sort the table by
distance each time the plot is clicked.
library(tidyverse)
library(shiny)
mtcars <- add_rownames(mtcars)
reset_dist <- function(x, click) {
nearPoints(x, click, allRows = TRUE, addDist = TRUE)$dist_
}
scatter <- function(x, dists) {
x %>%
mutate(dist = dists) %>%
ggplot() +
geom_point(aes(mpg, hp, size = dist)) +
scale_size(range = c(6, 1))
}
ui <- fluidPage(
plotOutput("plot", click = "plot_click"),
dataTableOutput("table")
)
server <- function(input, output) {
dist <- reactiveVal(rep(1, nrow(mtcars)))
observeEvent(
input$plot_click,
dist(reset_dist(mtcars, input$plot_click))
)
output$plot <- renderPlot(scatter(mtcars, dist()))
output$table <- renderDataTable({
mtcars %>%
mutate(dist = dist()) %>%
arrange(dist)
})
}
shinyApp(ui, server)