Interactive visualization in R

RaukR 2026 • Data Science With R

Roy Francis

18-Aug-2026

Why interactive visualizations?

  • Explore through zooming, filtering, brushing, hover details, and controls.
  • Keep the first view focused while making record-level detail available on demand.
  • Choose interaction to answer a question, not as decoration.

Design principles

  • Shape and summarize data before it reaches the browser.
  • Make every interaction earn its place.
  • Preserve a static fallback for reports and accessibility.
  • Aggregate, filter, or use transparency when many marks overlap.

Squirrels data

Data preparation

squirrels <- read_csv(
  "https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2019/2019-10-29/nyc_squirrels.csv",
  show_col_types = FALSE
)
squirrels <- squirrels |>
  mutate(
    observation_date = as.Date(as.character(date), format = "%m%d%Y"),
    fur_color = factor(
      coalesce(primary_fur_color, "Unknown"),
      levels = c("Gray", "Cinnamon", "Black", "Unknown")
    ),
    age_group = case_when(
      age %in% c("Adult", "Juvenile") ~ age,
      TRUE ~ "Unknown"
    ),
    above_ground_sighter_measurement = as.integer(
      if_else(above_ground_sighter_measurement == "FALSE", "0", above_ground_sighter_measurement)
    ),
    location = coalesce(location, "Unknown"),
    shift = factor(shift, levels = c("AM", "PM"))
  )

fur_colors <- c(
  Gray = "#7A7D80",
  Cinnamon = "#B65A24",
  Black = "#2B2B2B",
  Unknown = "#BDBDBD"
)

squirrel_points <- squirrels |>
  filter(!is.na(long), !is.na(lat)) |>
  mutate(
    point_id = row_number(),
    hover = paste0(
      "<b>", unique_squirrel_id, "</b><br>",
      "Fur: ", fur_color, "<br>",
      "Age: ", age_group, "<br>",
      "Shift: ", shift, "<br>",
      "Location: ", location
    )
  )

fur_counts <- squirrels |>
  count(fur_color, sort = TRUE)

squirrel_preview <- squirrels |>
  select(unique_squirrel_id, shift, age_group, fur_color, location, long, lat) |>
  slice_head(n = 15)

behaviour_names <- c(
  "running", "chasing", "climbing", "eating", "foraging", "kuks",
  "quaas", "moans", "tail_flags", "tail_twitches", "approaches",
  "indifferent", "runs_from"
)

R interactive ecosystem

  • Tables: DT, reactable, gt
  • Charts: plotly, highcharter, dygraphs, ggiraph, echarts4r
  • Maps: leaflet, mapgl, mapview, mapdeck, tmap
  • Networks: visNetwork, networkD3, DiagrammeR
  • Linked data: crosstalk

Tables: DT

library(DT)
datatable(
  squirrel_preview,
  filter = "top",
  options = list(
    pageLength = 5, 
    scrollX = TRUE
  ),
  rownames = FALSE
)

DT wraps DataTables.js with filtering, sorting, pagination, and export extensions.

Tables: reactable

library(reactable)
reactable(
  squirrel_preview,
  filterable = TRUE,
  searchable = TRUE,
  striped = TRUE,
  highlight = TRUE,
  defaultPageSize = 3
)

reactable provides React-based tables with strong R-side formatting options.

Tables: gt

library(gt)
squirrel_preview |>
  select(
    unique_squirrel_id, 
    shift, 
    age_group, 
    fur_color, 
    location) |>
  gt() |>
  cols_label(
    unique_squirrel_id = "Squirrel ID",
    age_group = "Age",
    fur_color = "Fur color"
  ) |>
  opt_interactive()

gt is primarily for publication-ready tables, with a concise interactive mode.

ggiraph

ggiraph adds browser interactions to familiar ggplot2 geoms.

library(ggiraph)

ggiraph_plot <- ggplot(
  squirrel_points,
  aes(long, lat, color = fur_color)
) +
  geom_point_interactive(
    aes(tooltip = hover),
    alpha = 0.65,
    size = 1.8
  ) +
  scale_color_manual(values = fur_colors) +
  labs(x = "Longitude", y = "Latitude", color = "Fur color") +
  theme_minimal()

girafe(ggobj = ggiraph_plot)

Plotly

  • R interface to Plotly.js
  • plot_ly() builds native traces and layouts
  • Strong for custom hover content, subplot composition, linked selection, and maps
  • ggplotly() provides a quick bridge from ggplot2

Plotly: bar traces and hover text

plot_ly(
  fur_counts,
  x = ~fur_color, 
  y = ~n,
  type = "bar", 
  text = ~paste0(fur_color, ": ", n),
  hoverinfo = "text",
  marker = list(
    color = unname(fur_colors[as.character(fur_counts$fur_color)])
  ),
  width = 360, 
  height = 430
) |>
  layout(
    title = "Primary fur color counts",
    xaxis = list(title = "Fur color"),
    yaxis = list(title = "Observations")
  )

Plotly: grouped and stacked bars

location_age <- squirrels |>
  filter(
    age_group != "Unknown", 
    location != "Unknown"
  ) |>
  count(location, age_group)

plot_ly(
  location_age,
  x = ~location, 
  y = ~n,
  color = ~age_group, 
  type = "bar"
) |>
  layout(
    title = "Location by age group",
    barmode = "group"
  )

Use barmode = "stack" for composition and "group" for direct comparison.

Plotly: date axes and range sliders

daily_counts <- squirrels |>
  count(observation_date, shift) |>
  filter(
    !is.na(observation_date), 
    !is.na(shift)
  )

plot_ly(daily_counts,
  x = ~observation_date,
  y = ~n,
  color = ~shift
) |>
  add_lines() |>
  add_markers() |>
  layout(xaxis = list(
    rangeslider = list(visible = TRUE)
  ))

A range slider exposes a detailed time window without rebuilding the chart.

Plotly: heatmaps

behaviour_by_shift <- squirrels |>
  select(shift, all_of(behaviour_names)) |>
  filter(!is.na(shift)) |>
  pivot_longer(
    -shift, 
    names_to = "behaviour", 
    values_to = "observed"
  ) |>
  group_by(shift, behaviour) |>
  summarise(
    proportion = mean(observed), 
    .groups = "drop"
  ) |>
  pivot_wider(
    names_from = shift, 
    values_from = proportion
  )

plot_ly(
  x = c("AM", "PM"), 
  y = behaviour_by_shift$behaviour,
  z = as.matrix(select(behaviour_by_shift, AM, PM)),
  type = "heatmap", 
  colors = "YlGnBu"
)

Plotly: maps and hover templates

plot_ly(
  squirrel_points,
  type = "scattermapbox",
  lon = ~long, lat = ~lat,
  color = ~fur_color,
  colors = unname(fur_colors),
  text = ~hover, hoverinfo = "text",
  marker = list(size = 7, opacity = 0.65)
) |>
  layout(mapbox = list(
    style = "open-street-map",
    center = list(
      lon = -73.9654, 
      lat = 40.7829
    ), 
    zoom = 12.5
  ))

Plotly: linked selections

linked_points <- squirrel_points |>
  mutate(point_id = row_number()) |>
  highlight_key(~point_id)

coordinates <- plot_ly(linked_points,
  x = ~long, y = ~lat, color = ~fur_color,
  type = "scatter", mode = "markers",
  colors = unname(fur_colors)
)
height <- plot_ly(linked_points,
  x = ~hectare_squirrel_number,
  y = ~above_ground_sighter_measurement,
  color = ~fur_color, type = "scatter", 
  mode = "markers",
  showlegend = FALSE,
  colors = unname(fur_colors)
)

highlight(subplot(coordinates, height),
  on = "plotly_selected", dynamic = TRUE)

The ggplot2 bridge

static_plot <- ggplot(
  squirrel_points,
  aes(long, lat, color = fur_color)
) +
  geom_point(alpha = 0.6) +
  labs(
    x = "Longitude", 
    y = "Latitude", 
    color = "Fur color"
  ) +
  scale_color_manual(values = fur_colors) +
  theme_minimal()

ggplotly(static_plot)

Use ggplotly() for quick hover and zoom. Use native Plotly traces when interaction needs precise control.

Highcharter

  • R wrapper around the Highcharts JavaScript ecosystem
  • hchart() is concise for familiar data shapes
  • highchart() plus hc_add_series() gives explicit series control
  • Exporting, themes, and configuration are first-class features

Highcharter: quick charts with hchart()

fur_counts |>
  hchart("column", 
    hcaes(
      x = fur_color, 
      y = n
    )
  ) |>
  hc_title(
    text = "Primary fur color counts"
  ) |>
  hc_yAxis(
    title = list(text = "Observations")
  ) |>
  hc_plotOptions(
    column = list(
      dataLabels = list(enabled = TRUE)
    )
  )

hchart() turns a data frame and mapped columns into a chart quickly.

Highcharter: stacking, tooltips, and exporting

daily_counts |>
  mutate(
    timestamp = datetime_to_timestamp(observation_date)
  ) |>
  hchart("area", 
    hcaes(
      x = timestamp, 
      y = n, 
      group = shift)
  ) |>
  hc_xAxis(type = "datetime") |>
  hc_plotOptions(
    area = list(stacking = "normal")
  ) |>
  hc_tooltip(shared = TRUE) |>
  hc_exporting(enabled = TRUE)

These options layer chart behaviour independently from the series definition.

Highcharter: coordinate scatterplots

highchart() |>
  hc_add_series(
    squirrel_points,
    type = "scatter",
    hcaes(
      x = long,
      y = lat,
      name = unique_squirrel_id, 
      group = fur_color
    ),
    marker = list(radius = 4)
  ) |>
  hc_xAxis(title = list(text = "Longitude")) |>
  hc_yAxis(title = list(text = "Latitude")) |>
  hc_mapNavigation(
    enabled = TRUE
  )

A coordinate scatterplot is a lightweight geographic fallback when a basemap is unnecessary.

Summary

  • plot_ly() builds traces; layout() controls presentation; highlight() links views
  • hchart() is concise; highchart() plus hc_add_series() is explicit and extensible
  • Both support hover, zoom, and export
  • Plotly has a ggplot2 bridge
  • Highcharts is not free for commercial use

Resources

Thank you!

Questions?

2026 • SciLifeLabNBISRaukR

Session

R version 4.5.3 (2026-03-11)
Platform: x86_64-conda-linux-gnu
Running under: Ubuntu 26.04 LTS

Matrix products: default
BLAS/LAPACK: /home/roy/miniforge3/envs/r-4.5/lib/libopenblasp-r0.3.33.so;  LAPACK version 3.12.0

locale:
 [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
 [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
 [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
 [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
 [9] LC_ADDRESS=C               LC_TELEPHONE=C            
[11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       

time zone: Europe/Stockholm
tzcode source: system (glibc)

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
 [1] gt_1.3.0          reactable_0.4.5   DT_0.34.0         highcharter_0.9.5
 [5] plotly_4.12.0     ggiraph_0.9.6     ggplot2_4.0.3     readr_2.2.0      
 [9] tidyr_1.3.2       dplyr_1.2.1      

loaded via a namespace (and not attached):
 [1] gtable_0.3.6            bslib_0.11.0            xfun_0.59              
 [4] htmlwidgets_1.6.4       rlist_0.4.6.2           lattice_0.22-9         
 [7] tzdb_0.5.0              crosstalk_1.2.2         vctrs_0.7.3            
[10] tools_4.5.3             generics_0.1.4          curl_7.1.0             
[13] tibble_3.3.1            xts_0.14.2              pkgconfig_2.0.3        
[16] data.table_1.18.4       RColorBrewer_1.1-3      S7_0.2.2               
[19] assertthat_0.2.1        lifecycle_1.0.5         compiler_4.5.3         
[22] farver_2.1.2            stringr_1.6.0           httpuv_1.6.17          
[25] fontquiver_0.2.1        fontLiberation_0.1.0    sass_0.4.10            
[28] htmltools_0.5.9         yaml_2.3.12             lazyeval_0.2.3         
[31] later_1.4.8             jquerylib_0.1.4         pillar_1.11.1          
[34] MASS_7.3-65             cachem_1.1.0            mime_0.13              
[37] fontBitstreamVera_0.1.1 tidyselect_1.2.1        digest_0.6.39          
[40] stringi_1.8.7           purrr_1.2.2             labeling_0.4.3         
[43] fastmap_1.2.0           grid_4.5.3              cli_3.6.6              
[46] magrittr_2.0.5          broom_1.0.13            reactR_0.6.1           
[49] withr_3.0.3             promises_1.5.0          gdtools_0.5.1          
[52] scales_1.4.0            backports_1.5.1         lubridate_1.9.5        
[55] timechange_0.4.0        TTR_0.24.4              rmarkdown_2.31         
[58] httr_1.4.8              quantmod_0.4.29         otel_0.2.0             
[61] zoo_1.8-15              hms_1.1.4               shiny_1.14.0           
[64] evaluate_1.0.5          knitr_1.51              viridisLite_0.4.3      
[67] rlang_1.3.0             Rcpp_1.1.1-1.1          xtable_1.8-8           
[70] glue_1.8.1              xml2_1.6.0              jsonlite_2.0.0         
[73] R6_2.6.1                systemfonts_1.3.2       fs_2.1.0