Interactive data visualization using Javascript packages

Author

An Bui

Published

March 16, 2023

Set up

# general use
library(here) # file organization
library(tidyverse) # manipulating
library(sf) # reading in spatial data, etc.
library(janitor) # cleaning variable names
library(lterdatasampler) # data source
library(randomcoloR) # random color generator

# Javascript package wrappers
library(leaflet) # interactive map
library(plotly) # interactive plots
library(ggiraph) # more interactive plots
library(echarts4r) # even more interactive plots

Interactive maps

leaflet is the go-to package for interactive maps. It’s not super for static maps, but for anyone looking to get an interactive map on their dashboard, this is a great option. In this example, we’re going to use some data from Niwot Ridge LTER to create a map of vegetation classes, snow surveys, pika locations, and landmarks at the site.

Here’s some cleaning code (we’re not going through this step by step):

# project extent

project_extent <- st_read(here::here("data", "nwt_project_extent", "shapefiles"), layer = "nwt_project_extent") %>% 
  st_transform(crs = 4326)
Reading layer `nwt_project_extent' from data source 
  `/Users/An/github/interactive-data-vis/data/nwt_project_extent/shapefiles' 
  using driver `ESRI Shapefile'
Simple feature collection with 1 feature and 1 field
Geometry type: POLYGON
Dimension:     XY
Bounding box:  xmin: 443729 ymin: 4429472 xmax: 456216.7 ymax: 4437388
Projected CRS: NAD83 / UTM zone 13N
# snow survey 

snow2018 <- st_read(here::here("data", "ss2018", "shapefiles"), layer = "ss2018") %>% 
  st_transform(crs = 4326) %>% 
  clean_names() %>% 
  mutate(comments = case_when(
    comments == "NaN" ~ "none",
    TRUE ~ comments
  )) %>% 
  mutate(marker_text = paste(
    "Depth:", snowdepth, "<br>",
    "Time:", sampletime, "<br>",
    "Recorders:", recorders, "<br>",
    "Comments:", comments, "<br>"
  )) 
Reading layer `ss2018' from data source 
  `/Users/An/github/interactive-data-vis/data/ss2018/shapefiles' 
  using driver `ESRI Shapefile'
Simple feature collection with 562 features and 13 fields
Geometry type: POINT
Dimension:     XY
Bounding box:  xmin: 444936.5 ymin: 4432975 xmax: 448047.3 ymax: 4434298
Projected CRS: NAD83 / UTM zone 13N
# vegetation classes 

veg <- st_read(here::here("data", "veg", "shapefiles"), layer = "veg") %>% 
  st_transform(crs = 4326) %>% 
  clean_names() %>% 
  mutate(marker_text = paste(
    "Type:", type, "<br>",
    "Area:", area, "<br>",
    "Perimeter:", perimeter, "<br>"
  )) 
Reading layer `veg' from data source 
  `/Users/An/github/interactive-data-vis/data/veg/shapefiles' 
  using driver `ESRI Shapefile'
Simple feature collection with 1233 features and 6 fields
Geometry type: POLYGON
Dimension:     XY
Bounding box:  xmin: 446324.4 ymin: 4432792 xmax: 453721.1 ymax: 4435984
Projected CRS: NAD83 / UTM zone 13N
# generating random colors for vegetation classes
veg_list <- veg %>% 
  pull(type) %>% 
  unique()

colors <- c(
  "#1c6e73", randomColor(count = 23, luminosity = "random"), "#e3e5e6"
)
veg_pal <- colorFactor(colors, domain = veg$type, ordered = TRUE)

# landmarks 

landmarks <- st_read(here::here("data", "nwt_annotation_pnt", "shapefiles"), layer = "nwt_annotation_pnt") %>% 
  st_transform(crs = 4326) %>% 
  clean_names() %>% 
  mutate(marker_text = paste(
    "Name:", name
  )) 
Reading layer `nwt_annotation_pnt' from data source 
  `/Users/An/github/interactive-data-vis/data/nwt_annotation_pnt/shapefiles' 
  using driver `ESRI Shapefile'
Simple feature collection with 40 features and 1 field
Geometry type: POINT
Dimension:     XY
Bounding box:  xmin: 444491 ymin: 4430127 xmax: 454307.8 ymax: 4436539
Projected CRS: NAD83 / UTM zone 13N
# pikas

pikas <- st_as_sf(x = nwt_pikas, coords = c("utm_easting", "utm_northing")) %>% 
  st_set_crs("+proj=utm +zone=13 +datum=NAD83 +units=m") %>% 
  st_transform("+proj=longlat +datum=WGS84") %>% 
  mutate(marker_text = paste(
    "Date:", date, "<br>",
    "Station:", station, "<br>",
    "Sex:", sex, "<br>"
  ))

And here’s a map:

map <- leaflet() %>% 
  # base maps
  addProviderTiles(providers$OpenTopoMap, group = "OpenTopoMap") %>%
  
  # map layers: project boundary and vegetation classes
  addPolygons(data = project_extent, color = "#488f32", group = "NWT project extent") %>%
  addPolygons(data = veg, group = "Vegetation", popup = ~marker_text, fillColor = ~veg_pal(type), stroke = FALSE, fillOpacity = 1) %>% 
  
  # markers
  addCircleMarkers(data = snow2018, group = "Snow surveys",
                   color = "lightblue", stroke = FALSE, fillOpacity = 1,
                   popup = ~marker_text,
                   popupOptions = popupOptions(closeOnClick = FALSE)) %>%
  addCircleMarkers(data = landmarks, group = "Landmarks", 
                   color = "yellow", stroke = FALSE, fillOpacity = 1,
                   popup = ~marker_text,  
                   popupOptions = popupOptions(closeOnClick = FALSE)) %>% 
  addCircleMarkers(data = pikas, group = "Pikas", 
                   color = "orange", stroke = FALSE, fillOpacity = 1, 
                   popup = ~marker_text, 
                   popupOptions = popupOptions(closeOnClick = FALSE)) %>%
  
  # layers control
  addLayersControl(
    baseGroups = c("OpenTopoMap"),
    overlayGroups = c("NWT project extent", "Vegetation", "Snow surveys", "Landmarks", "Pikas"),
    options = layersControlOptions(collapsed = TRUE)
  ) %>% 
  
  # legends
  addLegend(values = 1, group = "Snow surveys", position = "bottomleft", labels = "Snow surveys", colors = "lightblue", opacity = 1) %>%
  addLegend(values = 2, group = "Landmarks", position = "bottomleft", labels = "Landmarks", colors = "yellow", opacity = 1) %>% 
  addLegend(values = 3, group = "Pikas", position = "bottomleft", labels = "Pikas", colors = "orange", opacity = 1) %>% 
  
  # scale bar
  addScaleBar(position = "bottomright", options = scaleBarOptions(imperial = FALSE))

map 

Interactive plots

You can build interactivity into your plots using packages that are essentially wrappers for Javascript: you can get an interactive framework without having to learn a whole new language. We’re going to make the following plot interactive:

bg_col <- "#232324"
text_col <- "#f2f2f2"
  
weather <- arc_weather %>% 
  mutate(year = year(date),
         month = month(date),
         day = day(date)) %>% 
  mutate(season = case_when(
    month %in% c(12, 1, 2) ~ "winter",
    month %in% c(3, 4, 5) ~ "spring",
    month %in% c(6, 7, 8) ~ "summer",
    month %in% c(9, 10, 11) ~ "fall"
  ),
  season = fct_relevel(season, c("winter", "spring", "summer", "fall"))) %>% 
  mutate(julian = yday(date)) %>% 
  mutate(marker_text = paste(
    "Date: ", date, "<br>",
    "Mean air temp (C): ", mean_airtemp, "<br>",
    "Daily precipitation (mm): ", daily_precip, "<br>",
    "Mean windspeed (m/s): ", mean_windspeed, "<br>"
  ))

Static plot:

weather_static <- ggplot(data = weather, aes(x = julian, y = mean_airtemp, color = mean_airtemp, text = marker_text)) +
  geom_point(alpha = 0.8) +
  scale_color_gradient2(low = "#28ACE6", mid = "#FFFFFF", high = "#C13C31") +
  geom_hline(yintercept = 0, lty = 2, color = "#C13C31", linewidth = 2) +
  scale_x_continuous(expand = c(0, 0), limits = c(0, 366)) +
  scale_y_continuous(expand = c(0, 0), limits = c(-56, 25)) +
  labs(x = "Julian day", y = "degrees Celsius",
       title = "Toolik Field Station is below freezing for most of the year.",
       subtitle = "Each point represents mean air temperature on a given day of the year (Julian day) colored by temperature.") +
  theme_bw() +
  theme(text = element_text(family = "Garamond", color = text_col),
        panel.grid = element_blank(),
        legend.position = "none",
        panel.background = element_rect(fill = bg_col, color = bg_col),
        panel.border = element_blank(),
        plot.background = element_rect(fill = bg_col, color = bg_col),
        axis.text = element_text(color = text_col, size = 14),
        axis.line = element_blank(),
        axis.title = element_text(size = 14),
        axis.ticks = element_blank(),
        plot.title = element_text(size = 20, face = "bold"),
        legend.background = element_rect(fill = bg_col))
weather_static 

Option 1: Turn a ggplot object into an interactive graph with plotly

The easiest way to build in interactivity is to use plotly to get an interactive plot from a ggplot object.

There are other ways to use plotly too (check out the documentation), but the ggplotly() function is fairly powerful.

weather_ggplotly <- ggplotly(weather_static, tooltip = c("text")) %>% 
  layout(hoverlabel = list(
    font = list(
      family = "Garamond",
      size = 12
    )
  )) %>% 
  layout(title = list(
    text = paste0('Toolik Field Station is below freezing for most of the year.',
                  '<br>',
                  '<sup>',
                  'Each point represents mean air temperature on a given day of the year (Julian day) colored by temperature.',
                  '</sup>')
  ))

weather_ggplotly

Option 2: Use ggiraph’s unique geoms

ggiraph is another htmlwidgets package. There is a fairly well populated tutorial.

weather_ggiraph <- ggplot(data = weather, aes(x = julian, y = mean_airtemp, color = mean_airtemp, 
                                              tooltip = marker_text, data_id = marker_text)) +
  geom_point_interactive() +
  scale_color_gradient2(low = "#28ACE6", mid = "#FFFFFF", high = "#C13C31") +
  geom_hline(yintercept = 0, lty = 2, color = "#C13C31", linewidth = 2) +
  scale_x_continuous(expand = c(0, 0), limits = c(0, 366)) +
  scale_y_continuous(expand = c(0, 0), limits = c(-56, 25)) +
  labs(x = "Julian day", y = "degrees Celsius",
       title = "Toolik Field Station is below freezing for most of the year.",
       subtitle = "Each point represents mean air temperature on a given day of the year (Julian day) colored by temperature.") +
  theme_bw() +
  theme(text = element_text(family = "Garamond", color = text_col),
        panel.grid = element_blank(),
        legend.position = "none",
        panel.background = element_rect(fill = bg_col, color = bg_col),
        panel.border = element_blank(),
        plot.background = element_rect(fill = bg_col, color = bg_col),
        axis.text = element_text(color = text_col, size = 14),
        axis.line = element_blank(),
        axis.title = element_text(size = 14),
        axis.ticks = element_blank(),
        plot.title = element_text(size = 20, face = "bold"),
        legend.background = element_rect(fill = bg_col))

weather_ggiraph_interactive <- girafe(
  ggobj = weather_ggiraph,
  width = 8, height = 5,
  # bg = bg_col,
  options = list(
    opts_tooltip(
      opacity = 0.8, use_fill = TRUE,
      use_stroke = FALSE, 
      css = "padding:5pt;font-family: Garamond;font-size:1rem;color:white"),
    opts_hover_inv(css = "opacity:0.4"),
    opts_selection(
      type = "multiple",
      only_shiny = FALSE
    )
  )
)

weather_ggiraph_interactive

Option 3: echarts4r

This is the most complicated option - the documentation is fairly minimal. However, it’s cool. The guide is here.

airtemp_echarts4r <- weather %>% 
  group_by(year) %>% 
  e_charts(x = julian, timeline = TRUE) %>% 
  e_scatter(serie = mean_airtemp, symbol_size = 10) %>% 
  e_visual_map(min = -56, max = 21) %>% 
  e_tooltip(trigger = "item") %>% 
  e_axis_labels(
    x = "Julian day",
    y = "degrees Celsius"
  ) %>% 
  e_text_style(fontFamily = "Garamond") %>% 
  e_title(text = "Toolik Field Station is below freezing for most of the year.") %>% 
  e_legend(show = FALSE)
airtemp_echarts4r