2.1 Interactive Graphics with plotly
2.1 Interactive Graphics with plotly
Learning objectives
By the end of this chapter, you can:
- distinguish explanation from exploration and match hover, zoom, and filtering to each purpose.
- convert an existing ggplot2 plot into an interactive widget with
ggplotly()and explain the conversion limits. - customize hover information into a compact table using a
textmapping and thetooltipargument. - arrange multiple interactive panels with
subplot()andlayout(). - persist a shareable web page with
htmlwidgets::saveWidget()and weigh the cost ofselfcontainedoutput. - decide when print, PDF, accessibility, or other delivery constraints rule out interaction.
Prerequisite check (≤5 minutes)
Continue when you can write a ggplot2 scatterplot with three mappings without help; otherwise revisit Chapter 1.7.
- Without looking anything up, write the skeleton of
ggplot(penguins, aes(...)) + geom_point(), mapping one variable each to x, y, and color. - Recall the last time you actually used hover or zoom to inspect a chart. What question were you trying to answer?
1. What is interaction for? Explore versus explain
A static chart is author-directed: you establish the conclusion and use color and annotation to guide attention toward it. An interactive chart is reader-directed: you hand over some control, allowing readers to hover, zoom, filter, and ask their own questions. Establish the purpose before coding:
- Explore: the reader, often you, needs to find patterns, inspect anomalies, or follow individual observations. Interaction is a tool for efficient investigation.
- Explain: the conclusion is already clear. Interaction supplies context, such as details on hover, while your narrative remains central.
Match the three core affordances to their purposes:
| Affordance | Question answered | Typical setting |
|---|---|---|
| Hover | “Which observation is this, and what is its value?” | Dense points requiring IDs and values |
| Zoom | “What is happening in the dense region or long tail?” | Overlapping points or urban hotspots |
| Filter / legend selection | “What if I look only at this group?” | Group comparisons and subset-specific stories |
Ask three questions: Do readers need to look up values? Are dense regions important? Do they need to choose subsets themselves? If all three answers are no, you can stop here and use a static chart.
Interactive graphics are a different form, rather than an upgraded version of static graphics: a static chart resembles an argument; an interactive chart resembles a tool. Forcing either into the other’s role serves neither well. Choose the form before the tool. Most reports need only one or two genuinely useful interactive charts; keep the rest static.
2. ggplotly(): A one-line entry point and its limits
library(ggplot2)
library(plotly)
p <- ggplot(palmerpenguins::penguins,
aes(x = bill_length_mm, y = bill_depth_mm, color = species)) +
geom_point(size = 2, alpha = 0.8)
ggplotly(p)You immediately gain hover values, wheel zoom, drag-to-pan, and legend selection as a built-in filter. The camera icon in the upper-right modebar also exports a PNG, providing an escape route for static delivery.
ggplotly() is a best-effort translation, not an exact copy. Theme details, some extension geoms, and complex guides may be lost or altered. Conversion itself slows with large datasets because each layer must be serialized to JSON. For tens of thousands of points, try toWebGL(ggplotly(p)) for WebGL rendering, or return to an aggregated static chart.
This book emphasizes reusing your ggplot2 work: first get the chart right with ggplot2 (Chapters 1.7–1.8), then apply ggplotly() when interaction helps. Plotly also offers native plot_ly() syntax for building interactive charts directly; see https://plotly-r.com.
3. Custom tooltips: A small table on hover
The default tooltip is a machine-oriented list of x / y / color. To customize it, combine the information you want with paste0() inside a text mapping, then ask ggplotly() to display only that mapping:
p <- ggplot(palmerpenguins::penguins,
aes(x = bill_length_mm, y = bill_depth_mm)) +
geom_point(aes(
color = species,
text = paste0(species, " · ", island, "\n",
"Bill length ", bill_length_mm, " mm / bill depth ",
bill_depth_mm, " mm")
), size = 2)
ggplotly(p, tooltip = "text")Quick reference for tooltip: "text" displays only your text; c("x", "text") combines fields; "none" disables the tooltip entirely, reducing noise from decorative layers.
text is a ggplot2 aesthetic that static rendering ignores. The same p can therefore produce both static and interactive charts—the benefit of getting the chart right before adding interaction.
Change the example so hover displays only “species · island,” with no numbers. Then change it to tooltip = c("x", "text"). Describe to a classmate which readers each version serves: identifying observations versus checking values.
4. subplot and layout: Arranging multiple panels
Convert each of two charts with ggplotly(), place them beside one another with subplot(), and use layout() for the overall title and other layout details:
p1 <- ggplot(palmerpenguins::penguins,
aes(x = bill_length_mm, y = bill_depth_mm)) +
geom_point(aes(color = species), size = 2)
p2 <- ggplot(palmerpenguins::penguins,
aes(x = species, y = flipper_length_mm, fill = species)) +
geom_boxplot(show.legend = FALSE)
subplot(ggplotly(p1), ggplotly(p2), nrows = 1,
titleX = TRUE, titleY = TRUE, margin = 0.04) |>
layout(title = "Penguin measurements: relationships and distributions")For comparisons across facets, start with static facet_wrap() (Chapter 1.7): its meaning is clear and its legends are consistent. The distinctive use for subplot is independent interaction within each panel, including separate zoom and hover. Also, subplot drops axis titles by default; restore them with titleX = TRUE, titleY = TRUE.
5. Saving and sharing: saveWidget and self-contained files
In an executable Quarto HTML document, ggplotly() output is embedded directly and needs no separate save step. For standalone sharing, such as an email attachment or an intranet page, use:
fig <- ggplotly(p)
htmlwidgets::saveWidget(fig, "penguins-scatter.html") # Self-contained file
htmlwidgets::saveWidget(fig, "penguins-scatter-lib.html",
selfcontained = FALSE) # HTML + dependency directory
file.size("penguins-scatter.html") / file.size("penguins-scatter-lib.html")| Mode | Output | Benefit | Cost |
|---|---|---|---|
selfcontained = TRUE (default) |
One .html file | One file to share | Base64-embedded dependencies often make it several times larger |
selfcontained = FALSE |
.html + _files/ directory |
Smaller HTML; dependencies can be shared | The whole directory must travel with the file |
With selfcontained = FALSE, sending the .html without its adjacent dependency directory can leave the recipient with a blank page. Use self-contained output for email; use directory-based output for web hosting or several widgets sharing dependencies.
6. When to avoid interaction: A decision table
| Delivery setting | Recommendation | Reason |
|---|---|---|
| Journal article / PDF report | Static | PDF does not execute JavaScript |
| Printed material | Static | Paper cannot respond to hover |
| Large-screen presentation | High-resolution static | The presentation schedule leaves little room for live exploration |
| Web page / dashboard (Chapters 2.3–2.4) | Interactive | Readers have a browser and time |
| Strong accessibility requirements | Static + alternative text | Canvas content is inaccessible to screen readers |
| More than 50,000 points | Aggregate first / WebGL | A browser is not a database |
Fallback rule: place a static thumbnail or data table beside important interactive graphics. The main conclusion must hold without requiring readers to hover.
Repeat §2 and §5: create the interactive penguins scatterplot, save a self-contained file with saveWidget(), report its size in MB using file.size()/1e6, and have a classmate actually open it. “It looks fine on my computer” is insufficient.
Change the §3 tooltip to multiline text containing “species · island · bill length/bill depth in mm,” with no default x/y clutter. Extend §4 to a 1×3 subplot, using a flipper_length_mm boxplot of your choice for the third panel. Write one sentence explaining which two panels should not share a y-axis scale.
Round 1 (AI prohibited): Choose a real static chart from your field, or any two variables from the class dataset nycflights13. First answer the §6 decision questions in writing: should it be interactive, for whom, and in which deliverable? Then implement an interactive version with a custom tooltip and legend filtering. Use toWebGL() if the point count warrants it, then saveWidget(). Round 2 (AI allowed): Give Posit Assistant the code and a screenshot. Ask only: “Which of these three affordances is mainly satisfying my own enthusiasm?” Record its answer and explain whether you agree.
Capstone
Task: “One delivery, two forms.” Return to your Unit 1 report, or choose three static charts you have made. Complete the §6 decision table for each, then upgrade the single strongest candidate into a web widget. Customize its tooltip, use toWebGL() if needed, save both selfcontained modes, and report their sizes. Deliver a one-page Quarto report containing the static chart, a link to the interactive HTML, and your decision rationale.
| Dimension | Meets expectations | Strong | Excellent |
|---|---|---|---|
| Decision quality | Clear recommendation for all three charts | Reasons cite affordances and delivery constraints | Rejects an attractive but unnecessary interactive option |
| Implementation | ggplotly runs | Custom tooltip is readable | WebGL / multiple panels serve a clear purpose |
| Engineering and size | Saved HTML opens | Compares file sizes for both modes | Chooses and explains a mode for the sharing context |
| Honest boundaries | Provides a static fallback | Explains print and accessibility limits | Supplies alternative text or a data table |
SOURCES
| Chapter section | Material | Use |
|---|---|---|
| Structure, examples, exercises, capstone, and rubric | This project | Original |
ggplotly(), tooltip customization, subplot(), toWebGL(), and widget saving |
plotly for R (Carson Sievert), https://plotly-r.com | Reference |
| penguins data | palmerpenguins R package; used only for practice | Data |
This chapter is published under CC-BY-SA 4.0.