EDS 240: Lecture 9.1

Interactive Data Visualization


Week 9 | March 4th, 2026

Interactivity lets readers become explorers — not just observers

Why add interactivity to data visualizations?


Interactive charts can provide more information to the viewer, without cluttering your visualization — viewers can:

  • hover to surface exact values
  • zoom into regions of interest
  • filter by toggling groups on/off
  • animate through time or categories
  • explore at their own pace

Interactive charts are especially powerful for:

  • Datasets with many overlapping points
  • Time series data with many time steps
  • Visualizations embedded in HTML reports


Interactivity is not always the right choice


Good use cases:

  • Exploratory dashboards where interactivity is expected
  • Large datasets where labelling every point is impossible
  • Presentations in HTML/web format
  • Animated trends across many time steps
  • Scenarios where readers might want to know specific values

Be cautious when:

  • The chart will be printed or embedded in a PDF
  • A single static annotation tells the story more clearly
  • Interactivity adds complexity without insight
  • The audience isn’t expected to interact (e.g., a quick social post)

The goal is always communication — use interactivity when it genuinely helps the reader understand the data, not just because you can.

The htmlwidgets framework brings interactive JavaScript libraries to R

htmlwidgets — a shared framework for interactive R objects


The {htmlwidgets} package provides a standard interface for bundling interactive JavaScript visualizations as R objects.


Packages built on htmlwidgets:

Package What it does
{plotly} Interactive charts (scatter, bar, line, bubble, …)
{leaflet} Interactive maps with zoom and click
{DT} Searchable, sortable tables
{dygraphs} Interactive time series


All of these can be saved with saveWidget() and embedded in Quarto / R Markdown HTML output — no server or Shiny needed.

ggplotly() vs. plot_ly()


ggplotly()

  • Start with a ggplot2 chart you already have
  • Wrap it: ggplotly(my_plot)
  • Inherits ggplot2 aesthetics and layers
  • Less control over hover content

plot_ly()

  • Build charts directly in plotly’s own grammar
  • Uses formula notation: ~variable
  • Full access to Plotly’s feature set
  • More control over tooltips, layout, animation


We’ll go over both approaches today! In general, start with ggplotly() if you already have a ggplot2 workflow — switch to plot_ly() when you need animation or fine-grained control.

ggplotly() — one function turns any ggplot interactive

ggplotly() — wrapping an existing ggplot


# Step 1: build a normal ggplot
ggplot_static <- storms_clean |>
  ggplot(aes(
    x     = pressure,
    y     = wind,
    color = status,
    text  = label
  )) +
  geom_jitter(alpha = 0.35, size = 0.9, width = 1.5, height = 1.5) +
  scale_color_brewer(palette = "Paired") +
  labs(
    title  = "Wind Speed vs. Pressure by Storm Status",
    x      = "Pressure (mb)",
    y      = "Wind Speed (knots)",
    color  = "Status"
  ) +
  theme_minimal(base_size = 13)

ggplot_static
# Step 2: one line makes it interactive
ggplotly(ggplot_static, tooltip = "text")

Customizing tooltip content


The tooltip argument accepts a vector of aesthetic names from your aes() call:

# Map a formatted label to the 'text' aesthetic
storms_clean <- storms_clean |>
  mutate(label = paste0(
    "<b>", name, " (", year, ")</b><br>",
    "Status: ", status, "<br>",
    "Wind: ", round(wind, 1), " kt<br>",
    "Pressure: ", round(pressure, 1), " mb"
  ))

ggplot_static <- storms_clean |>
  ggplot(aes(
    x     = pressure,
    y     = wind,
    color = status,
    text  = label
  )) ...


ggplotly(p, tooltip = "text")   # only show our custom label

Repeat the units your data is measured in


Writing alt text for interactive plots


  • Interactive plots still need alt text, but it is important to note that screen readers cannot convey hover tooltips, animations, or interactivity

  • Including that your chart is interactive depends on how you use interactivity

  • Keep the same alt text formula discussed previously :Chart type of type of data where reason for including chart. If suitable, include interactive context by adding a sentence like this to the end of your alt text:

The chart is interactive, and each data point can be hovered over to reveal…..

  • The simplest way to include alt text for interactive plots is still via the fig-alt chunk option

ggplotly() — limitations to be aware of


ggplotly() is convenient but not a perfect conversion. Some things that don’t translate well:

  • Custom fonts loaded via {showtext}
  • Many theme() styling details (font families, some margins)
  • geom_sf() and spatial geometries
  • Faceted plots (can work, but often imperfectly)
  • Complex custom annotations


When ggplotly() doesn’t give you what you need, use plot_ly() instead

plot_ly() — native plotly for full control and animation

The plot_ly() grammar


plot_ly() has its own grammar, distinct from ggplot2:

ggplot2 grammar:

ggplot(data, aes(x = var1,
                  y = var2,
                  color = var3)) +
  geom_point()
  • Uses bare variable names
  • Layers added with +
  • aes() for aesthetic mappings

plot_ly() grammar:

plot_ly(data,
        x     = ~var1,
        y     = ~var2,
        color = ~var3,
        type  = "scatter",
        mode  = "markers")
  • Uses formula notation ~variable
  • Layers added with |> and add_*()
  • Type & mode set the geometry

Controlling hover content with text and hoverinfo


storms_clean |>
  plot_ly(
    x             = ~decade,
    y             = ~n,
    color         = ~status,
    colors        = RColorBrewer::brewer.pal(8, "Paired"),
    type          = "bar",
    text          = ~paste0("<b>", status, "</b><br>n = ", n),
    textposition  = "none",
    hoverinfo     = "text"  ) |>
  layout(
    barmode = "stack",
    title   = "Atlantic Storm Counts by Decade and Status",
    xaxis   = list(title = "Decade"),
    yaxis = list(title = ""),
    legend  = list(title = list(text = "<b>Status</b>"))
  )

hoverinfo controls what appears in the tooltip — options include "text", "x", "y", "x+y", "text+x+y", "none", and more. Combining with + shows multiple pieces.

Animation — the frame argument


A powerful feature of native plot_ly() is animation. Adding frame = ~variable creates one slide per unique value and automatically generates a Play/Pause button.

land <- "#2d3436"
water <- "#1a1a2e"
plot_ly(storms, 
        lat = ~lat,
        lon = ~long,
        frame = ~year,
        type = "scattergeo", 
        mode = "markers", # other options is lines or text
        color = ~as.factor(category), # so its not on a continuous scale
        colors = RColorBrewer::brewer.pal(5, "BuPu"),
        marker = list(size = 6, opacity = 0.7),
        text = ~paste0("<b>", name, "</b><br>Category: ", category),
        hoverinfo = "text") |>
  layout(
    title = "Atlantic Storm Positions by Year",
    geo = list(
      scope        = "north america", # default is world
      showland     = TRUE,  landcolor  = land, 
      showocean    = TRUE,  oceancolor = water,
      showcoastlines = TRUE, coastlinecolor = "#636e72",
      showlakes    = TRUE,  lakecolor  = water,
      bgcolor      = water,
      projection   = list(type = "mercator"),
      lonaxis      = list(range = c(-110, -10)),
      lataxis      = list(range = c(5, 60))
    ),
    paper_bgcolor = water, # rectangular box outside of plot
    font = list(color = "white")
  ) |>
  # each frame for 1000 milliseconds ( 1 second)
  animation_opts(frame = 1000)

Hit Play → the chart steps through each year. Use the slider to jump to a specific year. Click any point to isolate it across frames.

Sharing widgets — saveWidget()


Any htmlwidget can be saved as a standalone HTML file that anyone can open in a browser — no R installation required.


library(htmlwidgets)

# Assign your widget to an object, then save it
my_chart <- plot_ly(...)

saveWidget(my_chart,
           file  = "my_chart.html")

Embedding widgets in Quarto — it’s automatic


In a .qmd (or .rmd), you do not need saveWidget(). Simply print the widget object inside a code chunk.


my_chart <- plot_ly(storms, x = ~wind, y = ~pressure,
                    type = "scatter", mode = "markers")

my_chart   # printing embeds it in the rendered HTML output

Take a Break

05:00