Interactivity in charts

Notes
Modified

May 22, 2026

NoteLearning objectives
  • Introduce forms of interactivity for data visualizations
  • Identify core interactivity techniques
  • Critique interactivity in data visualizations
  • Implement interactive charts using {ggiraph}

When to go interactive

The foundational principle of interactive data visualization comes from Ben Shneiderman’s 1996 paper “The eyes have it”:

Overview first, zoom and filter, then details on demand.

Static charts present the most important patterns to the audience. Interactivity goes further — it allows readers to dig into the information, explore on their own terms, and surface stories that the designer did not anticipate. The two forms of communication serve different purposes: static charts communicate a predetermined message; interactive charts invite participation.

Forms of interactivity

Storytelling form

Interactive data experiences take two narrative forms:

  • Linear: The reader progresses through content in a fixed order. Scrollytelling (where effects are triggered by scrolling) is the most common linear form.
  • Nonlinear: The reader controls the order of exploration. Dashboards and interactive applications are the primary nonlinear forms.

Interaction techniques

Common interaction techniques include:

  • Scroll and pan — navigate along a single axis
  • Zoom — adjust the level of detail
  • Open and close — expand or collapse sections of content
  • Sort and rearrange — change the ordering of elements
  • Search and filter — show only data meeting specific criteria

Real interactive graphics often combine several of these techniques. For example, the New York Times “You Draw It” series asks readers to predict a trend before revealing the actual data — combining user drawing (open/close) with reveal (zoom and filter). The Pixar Cry Chart uses hover and filter to let readers explore emotional moments across Pixar films.

Approaches to interactive data communication

There are four general approaches to interactive data communication, ordered roughly by complexity:

  1. Interactive charts — Single charts with hover, click, or filter behavior. The easiest to build and the most common form. Tools: {ggiraph}, {plotly}.

  2. Scrollytelling — Longform stories where audio, video, and animation effects are triggered by scrolling. Maintains a linear narrative while making the experience feel dynamic. Common in data journalism.

  3. Dashboards — Nonlinear layouts that present related variables and allow client-side filtering and exploration. The reader controls which slices of the data to view, but all data is loaded upfront. Tools: {flexdashboard}, Quarto dashboards.

  4. Interactive web applications — Nonlinear format with server-side computation: the server re-runs R code in response to reader input, so the full dataset need not be loaded into the browser. More powerful but more complex. Tool: Shiny.

The rest of this note focuses on interactive charts, which have the lowest barrier to entry and can be embedded in any HTML document.

Interactive charts with {ggiraph}

{ggiraph} makes {ggplot2} graphics interactive by replacing standard geoms with _interactive variants. It keeps the familiar {ggplot2} structure while adding hover labels and click behavior. The output is an {htmlwidgets} widget — it renders correctly in any HTML document, including Quarto notes and RMarkdown reports.

Basic example

The pattern is:

  1. Replace a geom (e.g., geom_point()) with its interactive version (geom_point_interactive())
  2. Add tooltip and/or data_id aesthetics
  3. Wrap the plot in girafe() instead of printing it directly
gapminder_2007 <- filter(gapminder, year == 2007)

my_plot <- ggplot(
  data = gapminder_2007,
  mapping = aes(x = gdpPercap, y = lifeExp, color = continent)
) +
  geom_point_interactive(

    mapping = aes(tooltip = country, data_id = country)
  ) +
  scale_x_log10() +
  theme_minimal()

girafe(ggobj = my_plot)
1
geom_point_interactive() replaces geom_point() and adds interactivity support.
2
tooltip = country displays the country name on hover; data_id = country links hover highlighting to the same identifier.
3
girafe() renders the {ggplot2} object as an interactive SVG widget.

Hover over any point to see the country name. Points with the same data_id are highlighted together — useful when the same entity appears in multiple places.

Custom hover text

The tooltip aesthetic accepts HTML strings, which enables rich multi-line tooltips with formatted values:

my_plot <- ggplot(
  data = gapminder_2007,
  mapping = aes(x = gdpPercap, y = lifeExp, color = continent)
) +
  geom_point_interactive(
    mapping = aes(
      tooltip = str_glue(
        "{country}<br>Life expectancy: {round(lifeExp, 1)}"
      ),
      data_id = country
    )
  ) +
  scale_x_log10() +
  theme_minimal()

girafe(ggobj = my_plot) |>
  girafe_options(

    opts_tooltip(
      use_fill = TRUE,
      css = "background-color: #f8f9fa; color: #111000;"
    )
  )
1
str_glue() builds the tooltip string by interpolating data values. <br> is HTML for a line break.
2
opts_tooltip() controls tooltip appearance — background color, text color, padding, and border.

Works with many geoms

Most standard geoms have an interactive variant. For example, replace geom_histogram() with geom_histogram_interactive() to display bin counts on hover:

bm_hist <- ggplot(
  data = penguins,
  mapping = aes(x = body_mass)
) +
  geom_histogram_interactive(
    mapping = aes(
      tooltip = after_stat(count),
      data_id = after_stat(x)
    ),
    color = "white",
    binwidth = 100
  )

girafe(ggobj = bm_hist)
1
after_stat(count) accesses the computed bin count as the tooltip value.
2
after_stat(x) uses the bin center as the unique identifier for hover behavior.

Three interactivity aesthetics

All _interactive geoms support three aesthetics:

  • tooltip — HTML string displayed when the mouse hovers over the element
  • data_id — a unique identifier that links elements: when one element is hovered, all elements with the same data_id are highlighted together
  • onclick — a JavaScript expression executed when the element is clicked (less commonly used in R workflows)

A complete example: partisan baby names

The partisan names dataset contains the SSA-reported popularity of US baby names by year and state, augmented with the 2024 presidential election result for each state. The variable part_diff measures the partisan gap: the percentage-point difference in how common a name is in states won by Trump vs. states won by Harris.

partisan_names <- read_csv("data/partisan-names.csv") |>
  mutate(year = factor(year))
glimpse(partisan_names)
Rows: 200
Columns: 7
$ outcome   <chr> "Trump", "Trump", "Trump", "Trump", "Trump", "Trump", "Trump", "Trump"…
$ sex       <chr> "M", "M", "M", "F", "F", "F", "F", "M", "M", "F", "M", "M", "F", "F", …
$ year      <fct> 1983, 1983, 1983, 1983, 1983, 1983, 1983, 1983, 1983, 1983, 1983, 1983…
$ name      <chr> "Kendrick", "Trey", "Rodrick", "Ashlea", "Tosha", "Misti", "Latoria", …
$ Trump     <dbl> 0.9419954, 0.9318182, 0.9268293, 0.9166667, 0.8898305, 0.8863636, 0.88…
$ Harris    <dbl> 0.05800464, 0.06818182, 0.07317073, 0.08333333, 0.11016949, 0.11363636…
$ part_diff <dbl> 0.8839907, 0.8636364, 0.8536585, 0.8333333, 0.7796610, 0.7727273, 0.76…

Building the static chart

A beeswarm plot (via geom_quasirandom() from {ggbeeswarm}) distributes points to avoid overplotting while preserving the per-year grouping:

static_plot <- ggplot(
  data = partisan_names,
  mapping = aes(
    x = part_diff,
    y = fct_rev(year),
    color = outcome
  )
) +
  geom_quasirandom() +
  scale_x_continuous(labels = label_percent(style_positive = "plus")) +
  scale_color_manual(values = c(dem, rep), guide = "none") +
  labs(
    title = "The most popular names have gotten more polarized",
    x = "Partisan gap",
    y = NULL,
    caption = '"Blue" and "red" state designations are based on the 2024 presidential election results.\nOnly names assigned at least 100 times in each year were included.\n\nSource: Social Security Administration'
  )
static_plot

The distribution is clear — names are more polarized in recent years — but individual points are unidentifiable. Interactivity solves this.

Making it interactive

Replace geom_quasirandom() with geom_quasirandom_interactive() and add tooltip and data_id:

interactive_plot <- ggplot(
  data = partisan_names,
  mapping = aes(
    x = part_diff,
    y = fct_rev(year),
    color = outcome
  )
) +
  geom_quasirandom_interactive(
    mapping = aes(tooltip = name, data_id = name)
  ) +
  scale_x_continuous(labels = label_percent(style_positive = "plus")) +
  scale_color_manual(values = c(dem, rep), guide = "none") +
  labs(
    title = "The most popular names have gotten more polarized",
    x = "Partisan gap",
    y = NULL,
    caption = '"Blue" and "red" state designations are based on the 2024 presidential election results.\nOnly names assigned at least 100 times in each year were included.\n\nSource: Social Security Administration'
  )
girafe(ggobj = interactive_plot)
1
data_id = name means that hovering over a name highlights that same name across all years — you can track “Emma” or “James” up and down the chart.

Adding a rich tooltip

A richer tooltip that includes the year, direction, and formatted percentage makes the hover experience more informative. Build the tooltip string as a new column with mutate():

partisan_names <- partisan_names |>
  mutate(
    outcome_short = if_else(outcome == "Trump", "R", "D"),
    abs_part_diff = abs(part_diff),
    pct_label = label_percent(accuracy = 1, style_positive = "plus")(
      abs_part_diff
    ),
    fancy_label = str_glue("{year}: {name}<br>{pct_label} {outcome_short}")
  )

partisan_names |>
  select(year, name, part_diff, outcome_short, fancy_label)
1
Shorten “Trump”/“Harris” to “R”/“D” for compact display.
2
str_glue() combines the year, name, formatted percentage, and party abbreviation into a single HTML string.
# A tibble: 200 × 5
   year  name     part_diff outcome_short fancy_label             
   <fct> <chr>        <dbl> <chr>         <glue>                  
 1 1983  Kendrick     0.884 R             1983: Kendrick<br>+88% R
 2 1983  Trey         0.864 R             1983: Trey<br>+86% R    
 3 1983  Rodrick      0.854 R             1983: Rodrick<br>+85% R 
 4 1983  Ashlea       0.833 R             1983: Ashlea<br>+83% R  
 5 1983  Tosha        0.780 R             1983: Tosha<br>+78% R   
 6 1983  Misti        0.773 R             1983: Misti<br>+77% R   
 7 1983  Latoria      0.767 R             1983: Latoria<br>+77% R 
 8 1983  Jackie       0.753 R             1983: Jackie<br>+75% R  
 9 1983  Demarcus     0.740 R             1983: Demarcus<br>+74% R
10 1983  Angelia      0.737 R             1983: Angelia<br>+74% R 
# ℹ 190 more rows
fancy_plot <- ggplot(
  data = partisan_names,
  mapping = aes(
    x = part_diff,
    y = fct_rev(year),
    color = outcome
  )
) +
  geom_quasirandom_interactive(
    mapping = aes(tooltip = fancy_label, data_id = name)
  ) +
  scale_x_continuous(labels = label_percent(style_positive = "plus")) +
  scale_color_manual(values = c(dem, rep), guide = "none") +
  labs(
    title = "The most popular names have gotten more polarized",
    x = "Partisan gap",
    y = NULL
  )

girafe(ggobj = fancy_plot) |>
  girafe_options(
    opts_tooltip(
      css = "background-color: #ffffff; color: #111111; padding: 6px; border: 1px solid #999;"
    )
  )

Other interactive chart packages

{ggiraph} is not the only option. Other R packages for interactive charts include:

  • {plotly} — builds on the Plotly.js library; ggplotly() converts a {ggplot2} object to interactive with minimal code
  • {highcharter} — wraps Highcharts.js; rich animations and chart types
  • {rgl} — 3D interactive graphics rendered in WebGL
  • {dygraph} — specialized for time series with synchronized panning
  • {leaflet} — interactive maps via Leaflet.js
  • {mapgl} — vector tile maps via MapBox/MapLibre GL JS

For more examples see R Graph Gallery: interactive charts.

Summary

  • Interactivity shifts the reader from passive viewer to active explorer — use it when your audience benefits from being able to investigate details on demand
  • The core principle: overview first, zoom and filter, then details on demand
  • {ggiraph} converts {ggplot2} plots to interactive SVG widgets by replacing standard geoms with _interactive variants and wrapping in girafe()
  • The three key aesthetics are tooltip (hover text, accepts HTML), data_id (links related elements for coordinated highlighting), and onclick (JavaScript behavior on click)
  • Use str_glue() with HTML tags to build rich, multi-line tooltip strings from data values

Acknowledgements

Material derived in part from Advanced Data Visualization.