33  Interactivity

Author

Jarad Niemi

No R script is provided for this content because the R script will not work outside of an R Markdown (or Quarto) document. Thus, it is best to look at the R code on this page.

library("tidyverse"); theme_set(theme_bw())
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.2     ✔ readr     2.1.4
✔ forcats   1.0.0     ✔ stringr   1.5.0
✔ ggplot2   3.4.2     ✔ tibble    3.2.1
✔ lubridate 1.9.2     ✔ tidyr     1.3.0
✔ purrr     1.0.1     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library("Sleuth3")

# Tables
library("knitr") # for kable
library("kableExtra")

Attaching package: 'kableExtra'

The following object is masked from 'package:dplyr':

    group_rows
library("formattable")
library("DT")

# Figures
library("maps")

Attaching package: 'maps'

The following object is masked from 'package:purrr':

    map
library("sf")
Linking to GEOS 3.11.0, GDAL 3.5.3, PROJ 9.1.0; sf_use_s2() is TRUE
library("tigris")
To enable caching of data, set `options(tigris_use_cache = TRUE)`
in your R script or .Rprofile.
library("leaflet")
library("scales")

Attaching package: 'scales'

The following objects are masked from 'package:formattable':

    comma, percent, scientific

The following object is masked from 'package:purrr':

    discard

The following object is masked from 'package:readr':

    col_factor
library("plotly")

Attaching package: 'plotly'

The following object is masked from 'package:formattable':

    style

The following object is masked from 'package:ggplot2':

    last_plot

The following object is masked from 'package:stats':

    filter

The following object is masked from 'package:graphics':

    layout
library("gifski")

Rmarkdown documents that produce HTML files can include a variety of features that provide an interactive document for the user. Primarily this interactivity is implemented as will concern stand-alone tables, figures, and animations (movies). Typically this interactivity is available via an R package interface to a javascript library.

We’ll take a look at the construction of tables using the knitr, formattable, and DT packages. Technically, the first two packages provide non-interactive tables while the third provides interactivity. But we’ll start with the first two as they provide some nice functionality to make nice looking HTML tables.

33.1 Tables

We will take a look at the diamonds data set.

dim(diamonds)
[1] 53940    10

These data are too large for interactive scatterplots and thus we will take a random sample of these data.

33.1.1 kable

The kable() function in the knitr package provides an easy display of tables in an HTML document.

By default, the kable function will show the entire table. So, let’s just show the first few lines.

d <- diamonds |>
  group_by(cut) |> # ensure we have all cuts for grouping
  sample_n(3)

d
# A tibble: 15 × 10
# Groups:   cut [5]
   carat cut       color clarity depth table price     x     y     z
   <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
 1  0.77 Fair      F     VS1      66.8    57  3031  5.66  5.76  3.82
 2  0.52 Fair      G     IF       65.5    55  1849  4.98  5.06  3.29
 3  1    Fair      F     SI2      65.5    54  3864  6.2   6.16  4.05
 4  1.09 Good      F     SI1      61      64  4230  6.63  6.58  4.03
 5  0.45 Good      G     IF       59.6    61  1437  5     5.04  2.99
 6  1.29 Good      J     VS2      58.2    61  5898  7.07  7.12  4.13
 7  2.09 Very Good F     SI2      60.2    61 15773  8.32  8.29  5   
 8  2.1  Very Good G     SI2      63.4    58 13007  8.19  8.12  5.17
 9  1.12 Very Good H     SI2      61.7    59  5375  6.67  6.71  4.13
10  1.04 Premium   G     SI1      60.6    61  4742  6.63  6.59  4   
11  1.2  Premium   H     SI2      62.3    58  5280  6.76  6.81  4.23
12  1    Premium   H     VS1      60.7    59  5387  6.44  6.4   3.9 
13  0.32 Ideal     G     SI2      61.8    55   450  4.4   4.43  2.73
14  0.31 Ideal     G     VS2      61.7    55   562  4.37  4.39  2.7 
15  0.41 Ideal     G     IF       62.1    57  1336  4.77  4.76  2.96

Also, by default, the table looks pretty bad, so let’s add some styling.

knitr::kable(d) |> 
  kable_styling()
carat cut color clarity depth table price x y z
0.77 Fair F VS1 66.8 57 3031 5.66 5.76 3.82
0.52 Fair G IF 65.5 55 1849 4.98 5.06 3.29
1.00 Fair F SI2 65.5 54 3864 6.20 6.16 4.05
1.09 Good F SI1 61.0 64 4230 6.63 6.58 4.03
0.45 Good G IF 59.6 61 1437 5.00 5.04 2.99
1.29 Good J VS2 58.2 61 5898 7.07 7.12 4.13
2.09 Very Good F SI2 60.2 61 15773 8.32 8.29 5.00
2.10 Very Good G SI2 63.4 58 13007 8.19 8.12 5.17
1.12 Very Good H SI2 61.7 59 5375 6.67 6.71 4.13
1.04 Premium G SI1 60.6 61 4742 6.63 6.59 4.00
1.20 Premium H SI2 62.3 58 5280 6.76 6.81 4.23
1.00 Premium H VS1 60.7 59 5387 6.44 6.40 3.90
0.32 Ideal G SI2 61.8 55 450 4.40 4.43 2.73
0.31 Ideal G VS2 61.7 55 562 4.37 4.39 2.70
0.41 Ideal G IF 62.1 57 1336 4.77 4.76 2.96

33.1.1.1 Formatting

d |>
knitr::kable(
  caption = "Diamonds data", 
  align = c("rlllrrrrrr")
) |> 
  kable_styling(bootstrap_options = c('striped', 'hover', 'condensed')) |>
  scroll_box(height = "200px")
Diamonds data
carat cut color clarity depth table price x y z
0.77 Fair F VS1 66.8 57 3031 5.66 5.76 3.82
0.52 Fair G IF 65.5 55 1849 4.98 5.06 3.29
1.00 Fair F SI2 65.5 54 3864 6.20 6.16 4.05
1.09 Good F SI1 61.0 64 4230 6.63 6.58 4.03
0.45 Good G IF 59.6 61 1437 5.00 5.04 2.99
1.29 Good J VS2 58.2 61 5898 7.07 7.12 4.13
2.09 Very Good F SI2 60.2 61 15773 8.32 8.29 5.00
2.10 Very Good G SI2 63.4 58 13007 8.19 8.12 5.17
1.12 Very Good H SI2 61.7 59 5375 6.67 6.71 4.13
1.04 Premium G SI1 60.6 61 4742 6.63 6.59 4.00
1.20 Premium H SI2 62.3 58 5280 6.76 6.81 4.23
1.00 Premium H VS1 60.7 59 5387 6.44 6.40 3.90
0.32 Ideal G SI2 61.8 55 450 4.40 4.43 2.73
0.31 Ideal G VS2 61.7 55 562 4.37 4.39 2.70
0.41 Ideal G IF 62.1 57 1336 4.77 4.76 2.96

33.1.1.2 Grouping

groups <- table(d$cut)

d |>
knitr::kable(
  caption = "Diamonds data", 
  align = c("rlllrrrrrr")
) |> 
  kable_styling(bootstrap_options = c('striped', 'hover', 'condensed')) |>
  pack_rows(
    index = setNames(groups, names(groups))
  )
Diamonds data
carat cut color clarity depth table price x y z
Fair
0.77 Fair F VS1 66.8 57 3031 5.66 5.76 3.82
0.52 Fair G IF 65.5 55 1849 4.98 5.06 3.29
1.00 Fair F SI2 65.5 54 3864 6.20 6.16 4.05
Good
1.09 Good F SI1 61.0 64 4230 6.63 6.58 4.03
0.45 Good G IF 59.6 61 1437 5.00 5.04 2.99
1.29 Good J VS2 58.2 61 5898 7.07 7.12 4.13
Very Good
2.09 Very Good F SI2 60.2 61 15773 8.32 8.29 5.00
2.10 Very Good G SI2 63.4 58 13007 8.19 8.12 5.17
1.12 Very Good H SI2 61.7 59 5375 6.67 6.71 4.13
Premium
1.04 Premium G SI1 60.6 61 4742 6.63 6.59 4.00
1.20 Premium H SI2 62.3 58 5280 6.76 6.81 4.23
1.00 Premium H VS1 60.7 59 5387 6.44 6.40 3.90
Ideal
0.32 Ideal G SI2 61.8 55 450 4.40 4.43 2.73
0.31 Ideal G VS2 61.7 55 562 4.37 4.39 2.70
0.41 Ideal G IF 62.1 57 1336 4.77 4.76 2.96

33.1.1.3 Highlighting

d |>
  # Conditional highlighting
  mutate(
    carat = cell_spec(carat, "html", color = ifelse(carat > .7, "red", "black")),
    price = cell_spec(price, "html", color = ifelse(price < 5000, "blue", "black"))
  ) |>
  
  knitr::kable(
    escape  = FALSE,
    caption = "Diamonds data", 
    align   = c("rlllrrrrrr"),
  ) |> 
  kable_styling(bootstrap_options = c('striped', 'hover', 'condensed'))
Diamonds data
carat cut color clarity depth table price x y z
<span style=" color: red !important;" >0.77</span> Fair F VS1 66.8 57 <span style=" color: blue !important;" >3031</span> 5.66 5.76 3.82
<span style=" color: black !important;" >0.52</span> Fair G IF 65.5 55 <span style=" color: blue !important;" >1849</span> 4.98 5.06 3.29
<span style=" color: red !important;" >1</span> Fair F SI2 65.5 54 <span style=" color: blue !important;" >3864</span> 6.20 6.16 4.05
<span style=" color: red !important;" >1.09</span> Good F SI1 61.0 64 <span style=" color: blue !important;" >4230</span> 6.63 6.58 4.03
<span style=" color: black !important;" >0.45</span> Good G IF 59.6 61 <span style=" color: blue !important;" >1437</span> 5.00 5.04 2.99
<span style=" color: red !important;" >1.29</span> Good J VS2 58.2 61 <span style=" color: black !important;" >5898</span> 7.07 7.12 4.13
<span style=" color: red !important;" >2.09</span> Very Good F SI2 60.2 61 <span style=" color: black !important;" >15773</span> 8.32 8.29 5.00
<span style=" color: red !important;" >2.1</span> Very Good G SI2 63.4 58 <span style=" color: black !important;" >13007</span> 8.19 8.12 5.17
<span style=" color: red !important;" >1.12</span> Very Good H SI2 61.7 59 <span style=" color: black !important;" >5375</span> 6.67 6.71 4.13
<span style=" color: red !important;" >1.04</span> Premium G SI1 60.6 61 <span style=" color: blue !important;" >4742</span> 6.63 6.59 4.00
<span style=" color: red !important;" >1.2</span> Premium H SI2 62.3 58 <span style=" color: black !important;" >5280</span> 6.76 6.81 4.23
<span style=" color: red !important;" >1</span> Premium H VS1 60.7 59 <span style=" color: black !important;" >5387</span> 6.44 6.40 3.90
<span style=" color: black !important;" >0.32</span> Ideal G SI2 61.8 55 <span style=" color: blue !important;" >450</span> 4.40 4.43 2.73
<span style=" color: black !important;" >0.31</span> Ideal G VS2 61.7 55 <span style=" color: blue !important;" >562</span> 4.37 4.39 2.70
<span style=" color: black !important;" >0.41</span> Ideal G IF 62.1 57 <span style=" color: blue !important;" >1336</span> 4.77 4.76 2.96

33.1.2 formattable

Another function is formattable() in the formattable package. The default table is reasonable.

d |>
  formattable::formattable() 
carat cut color clarity depth table price x y z
0.77 Fair F VS1 66.8 57 3031 5.66 5.76 3.82
0.52 Fair G IF 65.5 55 1849 4.98 5.06 3.29
1.00 Fair F SI2 65.5 54 3864 6.20 6.16 4.05
1.09 Good F SI1 61.0 64 4230 6.63 6.58 4.03
0.45 Good G IF 59.6 61 1437 5.00 5.04 2.99
1.29 Good J VS2 58.2 61 5898 7.07 7.12 4.13
2.09 Very Good F SI2 60.2 61 15773 8.32 8.29 5.00
2.10 Very Good G SI2 63.4 58 13007 8.19 8.12 5.17
1.12 Very Good H SI2 61.7 59 5375 6.67 6.71 4.13
1.04 Premium G SI1 60.6 61 4742 6.63 6.59 4.00
1.20 Premium H SI2 62.3 58 5280 6.76 6.81 4.23
1.00 Premium H VS1 60.7 59 5387 6.44 6.40 3.90
0.32 Ideal G SI2 61.8 55 450 4.40 4.43 2.73
0.31 Ideal G VS2 61.7 55 562 4.37 4.39 2.70
0.41 Ideal G IF 62.1 57 1336 4.77 4.76 2.96
d |>
  
  # Conditional highlighting
  mutate(
    carat = cell_spec(carat, "html", color = ifelse(carat > .7, "red", "black")),
    price = cell_spec(price, "html", color = ifelse(price < 5000, "blue", "black"))
  ) |>
  
  formattable::formattable(
    list(
      # Width depends on proportion from 0 to max value
      x = color_bar("#C8102E"),    
      y = color_bar("#C8102E"),    
      z = color_bar("#C8102E"),    
      
      # Color depends on proportion from min to max value
      depth = color_tile("#CAC7A7","#524727")
    )
  ) 
carat cut color clarity depth table price x y z
0.77 Fair F VS1 66.8 57 3031 5.66 5.76 3.82
0.52 Fair G IF 65.5 55 1849 4.98 5.06 3.29
1 Fair F SI2 65.5 54 3864 6.20 6.16 4.05
1.09 Good F SI1 61.0 64 4230 6.63 6.58 4.03
0.45 Good G IF 59.6 61 1437 5.00 5.04 2.99
1.29 Good J VS2 58.2 61 5898 7.07 7.12 4.13
2.09 Very Good F SI2 60.2 61 15773 8.32 8.29 5.00
2.1 Very Good G SI2 63.4 58 13007 8.19 8.12 5.17
1.12 Very Good H SI2 61.7 59 5375 6.67 6.71 4.13
1.04 Premium G SI1 60.6 61 4742 6.63 6.59 4.00
1.2 Premium H SI2 62.3 58 5280 6.76 6.81 4.23
1 Premium H VS1 60.7 59 5387 6.44 6.40 3.90
0.32 Ideal G SI2 61.8 55 450 4.40 4.43 2.73
0.31 Ideal G VS2 61.7 55 562 4.37 4.39 2.70
0.41 Ideal G IF 62.1 57 1336 4.77 4.76 2.96

33.1.3 DT

As we will see, with the pagination, datatable() provides the capability to succinctly display much larger tables. So we will use more data

set.seed(20230416)
d <- diamonds |>
  sample_n(1000)

A basic interactive table using DT::datatable().

DT::datatable(d)

Many options can be added

33.1.3.1 Filtering

DT::datatable(d, rownames = FALSE, filter = "top")

33.1.3.2 Buttons

DT::datatable(d, rownames = FALSE, 
              extensions = "Buttons",
              options = list(
                dom = "Bfrtip",
                buttons = c("copy","csv","excel","pdf","print")
              ))

33.1.3.3 Editing

DT::datatable(d, rownames = FALSE, 
              editable = TRUE,
              extensions = "Buttons",
              options = list(
                dom = "Bfrtip",
                buttons = c("copy","csv","excel","pdf","print")
              ))

33.2 Figures

In this section, I am combining graphics, i.e. plots, as well as maps and animations (movies).

33.2.1 Plots

There are a variety of approaches to including interactivity in graphics in rmarkdown documents. We’ll focus on using the plotly library and specifically the ggplotly() function which provides interactivity for ggplot2 created graphics.

33.2.1.1 plotly::ggplotly()

The ggplotly() function from the plotly package provides interactivity for (all?) ggplot2 constructed graphics. The interactivity provide allows the user to

  • resize (zoom, rescale, reset)
  • pan
  • hover (show vs compare)
  • toggle spike lines
  • download
33.2.1.1.1 Boxplot
g <- ggplot(case0501, aes(x = Diet, y = Lifetime)) + 
  geom_boxplot() +
  coord_flip()

ggplotly(g)
33.2.1.1.2 Histogram
g <- ggplot(diamonds, aes(x = price)) + 
  geom_histogram(bins = 100)

ggplotly(g)
33.2.1.1.3 Scatterplot

Here is a static plot of the diamonds data set.

d <- diamonds |> sample_n(1000)

g <- ggplot(d, 
            aes(
              x = carat, 
              y = price,
              shape = cut,
              color = color)) + 
  geom_point() +
  scale_y_log10() + 
  scale_x_log10(breaks = scales::breaks_pretty()) 

g
Warning: Using shapes for an ordinal variable is not advised

ggplotly(g)
Warning: Using shapes for an ordinal variable is not advised

It seems plotly.js does not support multiple legends.

33.2.1.2 dygraphs()

Another package from constructing interactive graphics is dygraphs.

33.2.2 Maps

33.2.2.1 ggplot2()

Maps can be drawn with ggplot2, but these are not interactive.

ggplot(map_data("county","iowa"), 
       aes(x = long, y = lat, fill = subregion)) + 
  geom_polygon(color = "black") +
  guides(fill = "none")

33.2.2.2 leaflet()

An open source R package and JavaScript library for mobile-friendly interactive maps is LeafLet.

World map:

leaflet::leaflet() |> 
  addTiles() 

In order to set the view, you will need the latitude (y) and longitude (x) in decimal format. I typically use Google maps, but there are other options, e.g. LatLong.net.

Here is Ames:

leaflet::leaflet() |> 
  addTiles() |>
  setView(lng = -93.65, lat = 42.0285, zoom = 12) 

Example taken from here.

leaflet::leaflet() |> 
  addTiles() |>
  setView(-93.65, 42.0285, zoom = 17) |>
  addPopups(
    -93.65, 42.0285,
    'Here is the <b>Department of Statistics</b>, ISU'
  )

Modified from here

counties <- tigris::counties(state = "IA", class = "sf")
Retrieving data for the year 2022
leaflet() |>
  addTiles() |>
  addPolygons(data = counties, color = "grey")
Warning: sf layer has inconsistent datum (+proj=longlat +datum=NAD83 +no_defs).
Need '+proj=longlat +datum=WGS84'

33.2.3 Animations

gibbs_bivariate_normal = function(theta0, n_points, rho) {
  theta = matrix(theta0, nrow=n_points, ncol=2, byrow=TRUE)
  v = sqrt(1-rho^2)
  for (i in 2:n_points) {
    theta[i,1] = rnorm(1, rho*theta[i-1,2], v)
    theta[i,2] = rnorm(1, rho*theta[i  ,1], v)
  }
  return(theta)
}

theta = gibbs_bivariate_normal(c(-3,3), n<-20, rho=rho<-0.9)
bivariate_normal_animation = function(x, rho, ask=interactive()) {
  # Create contour plot
  n.out = 101
  xx <- seq(-3, 3, length=n.out)
  grid <- expand.grid(x=xx, y=xx)
  Sigma = diag(rep(.1,2))+rho
  like <- matrix(apply(grid, 1, function(x) mvtnorm::dmvnorm(x,sigma=Sigma)),n.out,n.out)
  
  for (i in 2:nrow(x)) {
    jj = (2:i)[-(i-1)] # vector from 2:(i-1) and NULL if i=2
    for (j in 1:6) {
      plot.new()
      
      # All previous plotting
      contour(xx, xx, like, drawlabels=F, nlevels=10, xlim=c(-3,3), ylim=c(-3,3), 
              xlab=expression(theta[1]), ylab=expression(theta[2]))  
      segments(x[jj-1,1], x[jj-1,2], x[jj,1], x[jj-1,2], col="gray")
      segments(x[jj  ,1], x[jj-1,2], x[jj,1], x[jj  ,2], col="gray")
      points(x[(1:(i-1)),1], x[(1:(i-1)),2], col="red", pch=19)
      
      # New plotting
      if (j>1 & j<4) abline(h=x[i-1,2], lty=2)
      if (j>2) arrows(x[i-1,1], x[i-1,2], x[i,1], x[i-1,2], length=0.1)
      if (j>3 & j<6) abline(v=x[i,1], lty=2)
      if (j>4) arrows(x[i,1], x[i-1,2], x[i,1], x[i,2], length=0.1)
      if (j>5) points(x[i,1], x[i,2], col="red", pch=19)
      
      if (ask) readline("hit <enter>:")
    }
  }
  
  jj=2:nrow(x)
  contour(xx, xx, like, drawlabels=F, nlevels=10, xlim=c(-3,3), ylim=c(-3,3), 
          xlab=expression(theta[1]), ylab=expression(theta[2]))  
  segments(x[jj-1,1], x[jj-1,2], x[jj,1], x[jj-1,2], col="gray")
  segments(x[jj  ,1], x[jj-1,2], x[jj,1], x[jj  ,2], col="gray")
  points(x[,1], x[,2], col="red", pch=19)
}
bivariate_normal_animation(theta, rho = 0.9)

33.3 Additional resources

33.3.1 Galleries

Official:

33.3.2 Individual sites

Individuals:

33.3.3 Embed

You can always embed additional interactivity through the use of an iframe. To get this to work, you need to add the option data-external="1" to the iframe options.

For example, here is a google map.

Here is an embedded video of mine from YouTube discussing the Gibbs sampler demonstrated above.