rguides

Interactive Visualizations with plotly

Interactive visualizations let your audience explore data rather than just view static images. The plotly package provides R programmers with a powerful way to build interactive charts that work in HTML documents, Shiny apps, and standalone HTML files without writing any JavaScript code.

Unlike static ggplot2 images, plotly charts respond to user input. Viewers can hover to see exact values, click legend items to filter categories, zoom into regions of interest, and pan around large datasets. This interactivity makes plotly ideal for exploratory data analysis and presenting findings to stakeholders who need to dig into the numbers themselves.

Installing plotly

Install plotly from CRAN or get the development version from GitHub:

# From CRAN
install.packages("plotly")

# Or from GitHub for latest features
# remotes::install_github("plotly/plotly.R")

Once installed, load plotly in your R session. You can use it standalone or alongside tidyverse for data manipulation before visualization. Since plotly integrates cleanly with dplyr pipelines, loading both packages together is a common and convenient pattern. The plotly package exports plot_ly() as its main workhorse function, along with ggplotly() for converting ggplot2 charts.

Load it alongside tidyverse if you prefer:

library(plotly)
library(tidyverse)

Converting ggplot2 to plotly

If you already know ggplot2, plotly makes it easy to convert your existing plots to interactive versions without rewriting everything. The ggplotly() function takes a ggplot object and returns an interactive plotly object. This is the fastest path to interactivity if your team already builds charts with ggplot2: you keep your existing code and layer on hover tooltips, zoom, and click-to-filter at the end:

# Create a ggplot first
p <- ggplot(mpg, aes(displ, hwy, color = class)) +
  geom_point() +
  labs(title = "Fuel Efficiency by Engine Displacement",
       x = "Engine Displacement (L)",
       y = "Highway MPG") +
  theme_minimal()

# Convert to interactive plotly
ggplotly(p)

This approach lets you build visualizations with familiar ggplot2 syntax, then add interactivity afterward. Hover over points to see values, click legend items to toggle categories, and drag to zoom. The conversion preserves most aesthetics including colors, shapes, and transparency.

Basic interactive charts

Plotly also works directly with its own syntax. Here is how to create common chart types:

Scatter plot

plot_ly(data = mtcars, x = ~wt, y = ~mpg, 
        color = ~factor(cyl), 
        type = "scatter", mode = "markers") %>%
  layout(title = "Car Weight vs MPG",
         xaxis = list(title = "Weight (1000 lbs)"),
         yaxis = list(title = "Miles per Gallon"))

Scatter plots are the default chart type for exploring relationships between two continuous variables. The color argument maps a categorical variable to point colors, producing one trace per group in the legend. For datasets with many overlapping points, consider setting alpha or switching to type = "scattergl" for WebGL-rendered performance with tens of thousands of points.

Bar chart

plot_ly(data = count(mpg, class), x = ~class, y = ~n, 
        type = "bar", 
        marker = list(color = "steelblue")) %>%
  layout(title = "Cars per Class",
         yaxis = list(title = "Count"))

Bar charts summarize counts or aggregated values per category. Unlike ggplot2’s geom_bar(), plotly requires you to pre-compute the aggregation; here dplyr::count() calculates the count of cars in each class before passing the summarized data to plot_ly(). The marker list controls bar appearance, including color, opacity, and border styling.

Histogram

plot_ly(data = iris, x = ~Sepal.Length, type = "histogram") %>%
  layout(title = "Sepal Length Distribution")

Histograms show the distribution of a single continuous variable by binning values into intervals. Plotly automatically chooses bin widths, but you can override them with nbinsx for finer control over the number of bars. Unlike bar charts which display pre-aggregated counts, histograms accept raw data and compute the binning internally. For overlaying multiple distributions, add separate histogram traces with opacity set below 1 so overlapping bars remain visible.

3D visualizations

Plotly supports 3D scatter plots and surfaces, which are useful for exploring multivariate data that would be hard to see in two dimensions:

plot_ly(data = iris, x = ~Sepal.Length, y = ~Sepal.Width, 
        z = ~Petal.Length, color = ~Species) %>%
  add_markers() %>%
  layout(scene = list(xaxis = title("Sepal Length"),
                      yaxis = title("Sepal Width"),
                      zaxis = title("Petal Length")))

3D plots can be rotated by clicking and dragging, letting you find angles that reveal patterns in your data. Use them sparingly; they can be harder to interpret than 2D views. When you do need a third dimension, type = "scatter3d" with mode = "markers" works well for visualizing clusters, while type = "surface" fits a continuous surface through three-dimensional points for modeling relationships like response surfaces in experimental design.

Creating subplots

Combine multiple plots into one figure using subplot() when you need to compare different views side by side:

p1 <- plot_ly(data = mtcars, x = ~wt, y = ~mpg, type = "scatter", mode = "markers")
p2 <- plot_ly(data = mtcars, x = ~wt, y = ~disp, type = "scatter", mode = "markers")
p3 <- plot_ly(data = mtcars, x = ~wt, y = ~hp, type = "scatter", mode = "markers")

subplot(p1, p2, p3, nrows = 1, shareX = TRUE, titleX = FALSE)

Set shareX = TRUE to synchronize zooming across all subplots, which is helpful when comparing variables against the same scale. The widths and heights parameters let you assign unequal space to each panel, so a main plot can occupy twice the width of its companion panels. When building dashboards, subplots give viewers a multi-faceted view of related data without requiring them to switch between separate charts.

Animations

Create animated visualizations that cycle through categories to show how values change over time or across groups:

plot_ly(data = mtcars, x = ~wt, y = ~mpg, 
        frame = ~factor(cyl),
        type = "scatter", mode = "markers") %>%
  animation_opts(frame = 1000, transition = 800)

The frame argument specifies which variable to animate through. Use this to build narratives around change over time or categorical transitions. The animation_opts() function controls playback speed with frame (milliseconds per frame) and transition (milliseconds for smooth transitions between frames). For time-series data, combine animation with a slider by adding animation_slider() to let viewers scrub through the timeline at their own pace.

Customizing layout

The layout() function controls titles, axes, legends, and styling. Beyond basic axis labels, layout gives you fine-grained control over every non-data visual element. You can hide gridlines with showgrid = FALSE, change the plot background color with plot_bgcolor, and set the overall figure background with paper_bgcolor. For presentations, adjusting margin parameters prevents axis labels from being clipped.

plot_ly(data = mtcars, x = ~wt, y = ~mpg, type = "scatter", mode = "markers") %>%
  layout(
    title = list(text = "Weight vs MPG", font = list(size = 20)),
    xaxis = list(title = "Weight", showgrid = FALSE),
    yaxis = list(title = "MPG", showgrid = TRUE),
    plot_bgcolor = "#f0f0f0",
    paper_bgcolor = "white"
  )

The font argument inside title accepts a named list, letting you set family, size, and color independently for the title text. For multi-line titles, use the HTML <br> tag inside the text string to break across lines. Axis formatting goes beyond simple labels. Use tickformat for numeric axes (d3-format syntax like ".2f" for two decimal places) and date format strings for time axes. The tickangle property rotates axis labels to prevent overlap when you have many categories on a categorical axis.

Sharing your visualizations

Interactive plotly charts render as HTML widgets. Save them as standalone HTML files:

# Save to HTML file
htmlwidgets::saveWidget(plot_ly(data = mtcars, x = ~wt, y = ~mpg, 
                                type = "scatter", mode = "markers"),
                        "my-plot.html")

Use selfcontained = TRUE (the default) to bundle all JavaScript dependencies into the file so it works offline without a web server. For sharing via email or hosting on a static file server, self-contained HTML files are ideal because the recipient only needs a browser. When you embed plotly charts in R Markdown documents or Quarto reports, they render automatically as interactive widgets without any extra configuration. In Shiny apps, pair plotlyOutput() with renderPlotly() to display charts, and use event_data("plotly_click") to capture user interactions for drill-down dashboards where clicking a point updates other panels.

Plotly bridges the gap between exploratory analysis and polished presentation. Start with ggplotly() to convert existing charts, then gradually adopt native plot_ly() syntax when you need finer control over interactivity, animations, or 3D views. The package scales from quick data exploration to production Shiny dashboards without changing your toolchain.

See also