7 Graphics
Chapter section list
- Using
renderPlot()
to create interactive plots, plots that respond to mouse events. - Couple of other useful techniques, including
- making plots with dynamic width and height and
- displaying images with
renderImage()
.
7.1 Interactivity
One of the coolest things about plotOutput()
is that as well as being an output that displays plots, it can also be an input that responds to pointer events. That allows you to create interactive graphics where the user interacts directly with the data on the plot.
7.1.1 Basics
A plot can respond to four different mouse events:
click
,dblclick
: double click,hover
: the mouse stays in the same place for a little while, andbrush
: a rectangular selection tool
To turn these events into Shiny inputs, you supply a string to the corresponding plotOutput()
argument, e.g. plotOutput("plot", click = "plot_click")
. This creates an input$plot_click
that you can use to handle mouse clicks on the plot.
R Code 7.1 : Click inside the image to get mouse coordinates
Note the use of req()
, to make sure the app doesn’t do anything before the first click, and that the coordinates are in terms of the underlying wt
and mpg
variables.
Structure of the following sections
The following sections describe the events in more details.
click
events- other point events (
dblclick
,hover
) brush
event (defining a rectangular area:xmin
,xmax
,ymin
, andymax
)- examples of plot updating events
- limitation of interactive graphics in Shiny
7.1.2 Clicking
The point events return a relatively rich list containing a lot of information. The most important components are x
and y
, which give the location of the event in data coordinates.
I’m not going to talk about this data structure, since you’ll only need it in relatively rare situations.
Resource 7.1 : List of point events
Learn about the complex details for point events using an app in the Shiny gallery.
If you want to see just the returned values for the input click in R Code 7.1 replace the last line with the cat()
arguments to input$plot_click
.
Instead of explaining all the details, we’ll use the nearPoints()
helper, which returns a data frame containing rows near the click, taking care of a bunch of fiddly details. You have to click near a point otherwise the function would do nothing because it’s name is nearPoints()
and not nearestPoint()
.
Another way to use nearPoints()
is with allRows = TRUE
and addDist = TRUE
. That will return the original data frame with two new columns:
dist_
gives the distance between the row and the event (in pixels).selected_
says whether or not it’s near the click event (i.e. whether or not its a row that would be returned whenallRows = FALSE
).
Code Collection 7.1 : Using nearPoints()
R Code 7.2 : nearPoints()
example with base::plot()
R Code 7.3 : nearPoints()
example with ggplot2::ggplot()
: nearPoints()
example as a ggplot2::ggplot()
with allRows = TRUE
and addDist = TRUE
You may experiment and try a different (combination of) arguments, for instance changing the threshold
and / or maxpoints
, turning addDist
and / or allRows
on or off.
You might wonder exactly what nearPoints()
returns. This is a good place to use base::browser()
, which was discussed in Section 5.2.3.
- Replace the
outputoutput$data <- renderTable()
function with:
output$data <- renderTable({
req(input$plot_click)
browser()
nearPoints(mtcars, input$plot_click) })
- Restart the app.
- Click inside the plot in the resulted web page.
- Then you will see in the console the following text:
> runApp('apps-07/click-browser')
Listening on http://127.0.0.1:5962
Called from: eval(expr, p)
Browse[1]> n
debug at /Users/petzi/Documents/Meine-Repos/learning-shiny/apps-07/click-browser/app.R#16: nearPoints(mtcars, input$plot_click)
Browse[1]>
- Continue in the second
Browse[1]>
line, where the cursor stops, with
[1]> nearPoints(mtcars, input$plot_click)
Browse
mpg cyl disp hp drat wt qsec vs am gear carb Toyota Corona 21.5 4 120.1 97 3.7 2.465 20.01 1 0 3 1
- The last line will differ depending where the mouse click occurred.
7.1.3 Other point events
The same approach works equally well with click
, dblclick
, and hover
: just change the name of the argument. If needed, you can get additional control over the events by supplying clickOpts()
, dblclickOpts()
, or hoverOpts()
instead of a string giving the input id. These are rarely needed, so I won’t discuss them here; see the documentation for details.
You can use multiple interactions types on one plot. Just make sure to explain to the user what they can do: one downside of using mouse events to interact with an app is that they’re not immediately discoverable.
7.1.4 Brushing
Another way of selecting points on a plot is to use a brush, a rectangular selection defined by four edges. In Shiny, using a brush is straightforward once you’ve mastered click
and nearPoints()
: you just switch to brush
argument and the brushedPoints()
helper.
Code Collection 7.2 : Brush examples
R Code 7.4 : Draggable ‘brush’ to draw a box around a rectangular area
Play around with this app. Use for instance brushOpts()
to control the color (fill and stroke). You will find an example solution in the next tab. Or restrict brushing to a single dimension with direction = “x” or “y” (useful, e.g., for brushing time series).
R Code 7.5 : Using brushOpts()
to define the brushing options
This example demonstrates how to use brushOpts()
to enable brushing on plots. The brushOpts()
function is used to define the brushing options, such as the id
for the brush, and other parameters like fill
, stroke
, and opacity
to customize the appearance of the brush
R Code 7.6 : Use brushOpts()
to enable brushing on plots and link multiple plots together.
This example demonstrates how to use brushOpts()
to enable brushing on plots and link multiple plots together. Here on of the arguments of the brushOpts()
function is resetOnNew
to reset the brush when the plot is updated. It uses observeEvent()
and observe()
to link two actions (here brushing and double clicking) in the left panel and linking the two plots in the middle and right panel.
7.1.5 Modifying the plot
So far we’ve displayed the results of the interaction in another output. But the true beauty of interactivity comes when you display the changes in the same plot you’re interacting with. Unfortunately this requires an advanced reactivity technique that you have not yet learned about: reactiveVal()
. We’ll come back to reactiveVal()
in (XXX_16?), but I wanted to show it here because it’s such a useful technique. You’ll probably need to re-read this section after you’ve read (XXX_16?), but hopefully even without all the theory you’ll get a sense of the potential applications1.
As you might guess from the name, reactiveVal()
is rather similar to reactive()
. You create a reactive value by calling reactiveVal()
with its initial value, and retrieve that value in the same way as a reactive:
val <- reactiveVal(10)
val()[1] 10 #>
The big difference is that you can also update a reactive value, and all reactive consumers that refer to it will recompute. A reactive value uses a special syntax for updating — you call it like a function with the first argument being the new value:
val(20)
val()[1] 20 #>
That means updating a reactive value using its current value looks something like this:
val(val() + 1)
val()[1] 21 #>
Unfortunately if you actually try to run this code in the console you’ll get an error because it has to be run in an reactive environment. That makes experimentation and debugging more challenging because you’ll need to use browser()
or something similar to pause execution within the call to shinyApp()
. This is one of the challenges we’ll come back to later in (XXX_16?).
For now, let’s put the challenges of learning reactiveVal()
aside, and show you why you might bother. Imagine that you want to visualize the distance between a click and the points on the plot. In the app below, we start by creating a reactive value to store those distances, initializing it with a constant that will be used before we click anything. Then we use observeEvent()
to update the reactive value when the mouse is clicked, and a ggplot()
that visualizes the distance with point size.
Code Collection 7.3 : Modifying plots
7.1.6 Interactivity limitations
It’s important to understand the basic data flow in interactive plots in order to understand their limitations.
Procedure 7.1 : The basic flow of an interactive action in Shiny
- JavaScript captures the mouse event.
- Shiny sends the mouse event data back to R, telling the app that the input is now out of date.
- All the downstream reactive consumers are recomputed.
plotOutput()
generates a new PNG and sends it to the browser.
For local apps, the bottleneck tends to be the time taken to draw the plot. Depending on how complex the plot is, this may take a significant fraction of a second. But for hosted apps, you also have to take into account the time needed to transmit the event from the browser to R, and then the rendered plot back from R to the browser.
It’s not possible to create Shiny apps where action and response is perceived as instantaneous (i.e. the plot appears to update simultaneously with your action upon it)
A better interactivity experience is with {plotly} (Sievert 2020). There is also a book by the sam author (2019).
7.2 Dynamic image size
It is possible to make the plot size reactive, so the width and height changes in response to user actions. To do this, supply zero-argument functions to the width
and height
arguments of renderPlot()
— these now must be defined in the server, not the UI, since they can change. These functions should have no argument and return the desired size in pixels. They are evaluated in a reactive environment so that you can make the size of your plot dynamic.
R Code 7.9 : Dynamic width and height of images
- When you resize the plot, the data stays the same: you don’t get new random numbers.
- In real apps, you’ll use more complicated expressions in the width and height functions.
7.3 Images
You can use renderImage()
if you want to display existing images (not plots). For example, you might have a directory of photographs that you want shown to the user. The following app illustrates the basics of ? by showing cute puppy photos. The photos come from https://unsplash.com, my favorite source of royalty free stock photographs.
renderImage()
needs to return a list. The only crucial argument is src
, a local path to the image file. You can additionally supply:
- A contentType, which defines the MIME type of the image. If not provided, Shiny will guess from the file extension, so you only need to supply this if your images don’t have extensions.
- The
width
andheight
of the image, if known. - Any other arguments, like
class
oralt
will be added as attributes to the<img>
tag in the HTML.
You must also supply the deleteFile
argument. Unfortunately renderImage()
was originally designed to work with temporary files, so it automatically deleted images after rendering them. This was obviously very dangerous, so the behavior changed in Shiny 1.5.0. Now Shiny no longer deletes the images, but instead forces you to explicitly choose which behavior you want.
You can learn more about renderImage()
, and see other ways that you might use it at https://shiny.rstudio.com/articles/images.html.
R Code 7.10 : Display existing images (not plots) FAILED!
renderImage()
not working with shinylive-r
The above code does not work. Hopefully I will learn the solution form the answer to my Posit post.
Solution 7.1. : File comment header and base64 encoding
There are two steps for the solution of the failed image rendering in R Code 7.10:
- “To include files in a Shinylive app in a Quarto document, the files need to be included in the shinylive-r chunk using the
## file: {filename}
comment header.” See the answer to my question shinylive-r: cannot include R file in the Posit forum by Garrick Aden-Buie. — This posting was very helpful for me. I have read the documentation in Shinylive applications embedded in Quarto documents but I didn’t understand
- The images in
shinylive
has to be base64 encoded. See Multiple Files in Shinylive applications embedded in Quarto documents. But the encoding process needed extra tools and was cumbersome until Garrick Aden-Buie developed a Quarto extension. See his answer to my qeustion Again: Shinylive with renderImage() & new base64 Quarto extension in the Posit forum. The Quarto extension with its documentation can be found in the quarto-base64 GitHub Repo.
Before I will present the code solution of R Code 7.10 there are two important requirements to fulfill:
- Download the base64 Quarto extension and install it on your system.
- Add the extension to your Quarto documents either as YAML header at the start of your document or into your
_quarto.yml
file like this:
filters:
- shinylive
- base64
Before we will interact with the full functional Shiny app, I will present the real code snippets producing the successful outcome.
```{shinylive-r}
#| standalone: true
#| viewerHeight: 900
#| components: [editor, viewer]
#| layout: vertical
## file: app.R
{{< include apps_07/display-images-success/app.R >}}
## file: puppy-photos-success/eoqnr8ikwFE.jpg
## type: binary
{{< base64 apps_07/display-images/puppy-photos-success/eoqnr8ikwFE.jpg "image/jpeg" >}}
## file: puppy-photos-success/KCdYn0xu2fU.jp
## type: binary
{{< base64 apps_07/display-images/puppy-photos-success/KCdYn0xu2fU.jpg "image/jpeg" >}}
## file: puppy-photos-success/TzjMd7i5WQI.jpg
## type: binary
{{< base64 apps_07/display-images/puppy-photos-success/TzjMd7i5WQI.jpg "image/jpeg" >}}
```
7.4 My Summary
In this chapter I learned to interact with plots and to render pre-fabricated images. The rendering process of external images poses a problem with shinylive
because the code chunks are self-contained and sealed-off from the standard R environment.
After some help via the Posit forum I could manage to call multiple files (R scripts, Shiny apps, images etc.) and to refer to these files inside the shinylive-r
chunk.
shinylive
comment header and Quarto include
shortcode
In all the following chapters I will not include the code for Shiny apps directly into the shinylive-r
code chunk. Instead I will embed Shiny apps (and all other used files) via a reference with the file comment procedure followed by a Quarto include shortcode. For details see the explanation in Solution 7.1 and the example in Listing / Output 7.4.
References
I have already used
reactiveValues
in my own summary in Chapter 3 as a solution for my practice example that was for me easier to understand than thereactive()
function. I took the idea from a [StackOverflow question]https://stackoverflow.com/questions/71367314/extract-a-value-from-reactive-data-frame-in-shiny (and the answer).↩︎