6 - ggplot2, plotly, htmlwidgets, crosstalk

Author

Man

Published

September 23, 2026

Setup

Today we look at some ways to make our data slightly more interactive for the web without the full power of web development in R Shiny.

First we load in some libraries.

library(dplyr)
library(tidyr)
library(ggplot2)
library(reactable)
library(plotly)
library(crosstalk)

ggplot2

ggplot2 creates 2D graphs.

Directions

  1. State the data frame. Then add an “aesthetics” of which data frame columns to serve as x-, y-axis:
ggplot(df, 
    aes(x = col1, y = col2, color = ..., fill = ..., size = ..., shape = ...) +
    options below +
    more options below +
    etc. 
    )
  1. Add graph types, like:
    • geom_histogram()
    • geom_bar() and geom_col()
    • geom_dotplot()
    • geom_point()
    • geom_line()
    • geom_boxplot()
  2. Add etc.
    • Add labels labs(title = ..., x = ..., y = ..., color = ...)
    • Add theme: theme_bw(), theme_dark(), theme_minimal(), etc.

Example

Use the Billboard Songs database and the top positions of their songs, available at https://manctsui.github.io/datasets/charts.csv.

Plot the number of weeks song is on the board vs their peak positions.

(Idea: Keep only distinct songs.)

df <- read.csv("https://manctsui.github.io/datasets/charts.csv") |>
    distinct(song, artist, .keep_all = TRUE)

plot <- ggplot(df, aes(weeks.on.board, peak.rank)) +
    geom_point()

plot

Plotly

plotly creates 2D and 3D graphs with interactivity.

Wrap a ggplot object with ggplotly() to get a plotly graph:

library(ggplot2)
library(plotly)

x <- c(1:100)
random_y <- rnorm(100, mean = 0)
data <- data.frame(x, random_y)

fig <- ggplot(data, mapping = aes(x = x, y = random_y)) +
    geom_line()

ggplotly(fig)

Or directly create graphs in plotly:

fig <- plot_ly(data, x = ~x, y = ~random_y, type = 'scatter', mode = 'lines')

fig

Types to choose from:

  • scatter – accounts for dotplots, scatterplots, line graphs, etc.
  • scatter3d
  • bar
  • pie
  • choropleth map of geographic regions shaded by intensity of data values
  • Sankey diagrams, etc.

Example

Repeat the ggplot2 example purely in plotly:

plot2 <- plot_ly(df, x = ~weeks.on.board, y= ~peak.rank, type = "scatter", mode = 'markers') |>
    layout(
        title = "Billboard Songs: Total Weeks vs Peak Rank",
        xaxis = list(title = "Total Weeks on Board"),
        yaxis = list(title = "Peak Rank")
    )

plot2

Leaflet

library(leaflet)

locations_df <- data.frame(
  name = c("Eiffel Tower", "Louvre Museum", "Notre-Dame"),
  lat  = c(48.8584, 48.8606, 48.8530),
  lng  = c(2.2945, 2.3376, 2.3499)
)

# Plotting all 3 markers simultaneously
leaflet(data = locations_df) |>
  addTiles() |>
  addMarkers(
    lng = ~lng, 
    lat = ~lat, 
    popup = ~name # Adds clickable text popups
  )

HTML Widgets

plotly and leaflet are actually JavaScript libraries. The R package htmlwidgets allows developers write R bindings to access these JS libraries.

This means you can pick from many widgets to use in R without needing to use JS.

Crosstalk

Crosstalk does linked brushing: selecting data points in one widget automatically selects the same data points in a shared widget.

Directions:

  1. Pick a column df$col of a data frame df that uniquely identifies each row: this column serves as a key.

  2. Upgrade our data frame df to a SharedData object and store it as a variable

    sd <- SharedData$new(df, key = ~col)

    The ~ is used here because key expects a formula not a variable name as input.

  3. Use this SharedData object sd as you would use a data frame within ggplot2, plotly, DT, reactable, leaflet, etc.

Crosstalk also comes with the following inputs; some arguments are shown:

  • filter_slider(id, label, sharedData, column, min, max)
  • filter_select(id, label, sharedData, group)
  • filter_checkbox(id, label, sharedData, group)

Here:

  • id is the HTML id of the input, so you can reference this input object.
  • label is the name of this input that shows up.
  • sharedData is your upgraded data frame
  • group and column: the formula or data frame column that is shared between widgets.

Example

Display songs by Ed Sheeran, Adele, and Taylor Swift from the Billboard Songs database as well as the top positions of their songs as a table. Include a filter checkbox that toggles based on the selected artists to restrict our table.

specific_artists <- read.csv("https://manctsui.github.io/datasets/charts.csv") |>
    filter(artist %in% c("Ed Sheeran", "Adele", "Taylor Swift")) |>
    distinct(song, artist, peak.rank)

sd <- SharedData$new(specific_artists, key = ~song)

filter_checkbox("artist_checkbox", "Artist", sd, ~artist)
reactable(sd, defaultPageSize = 4)

Example, improved

We can make the above look nicer by arranging the widgets into columns with crosstalk’s bscols (Bootstrap columns) function. Read the documentations and an example of its use if you want to learn more.

bscols(
    filter_checkbox("artist_checkbox", "Artist", sd, ~artist),
    reactable(sd, defaultPageSize = 4),
    widths = c(4,8)
)

Caveats

  • Not all HTML widgets support crosstalk.
  • For full reactivity, use Shiny for R.

Reference