Plotly abandoned R in 2025. Start 2026 thinking if it really is your best choice.

Plotly isn’t magic, it’s powered by {htmlwidgets} which unlocks a vast universe of interactive dataviz with over 133 packages! Consider if you should move beyond Plotly.
R
htmlwidgets
Author

Charlotte Jane Hadley

Published

January 5, 2026

*They also abandoned their MATLAB, Julia and F# libraries.

We have a note for you if plotly is mission critical for you

It’s true. In September 2025 the Plotly company announced they are abandoning their R* documentation and packages. Plotly didn’t need to do this.

They made the announcement one day after announcing their “Rise of Vibe Analytics” event. It is clear that Plotly is not a data visualisation company with a broad commitment to Open Source. They’re a vibe analytics and enterprise BI outfit.

We want you to spend 15minutes at the start of 2026 thinking about if Plotly really is your best (and only) choice in R, Quarto and Shiny.

{plotly} isn’t magic, it’s a {htmlwidgets} package

Do you know how {plotly} is able to create interactive dataviz in R, Quarto and Shiny? It’s 100% down to the magic of {htmlwidgets} which enables such rich interactive dataviz with R.

You might not believe the diversity of what’s possible - check out these examples:

The {leaflet} package is the best choice for creating interactive maps.

Code
library("sf")
library("leaflet")
# From https://leafletjs.com/examples/choropleth/us-states.js
states <- sf::read_sf("https://rstudio.github.io/leaflet/json/us-states.geojson")

bins <- c(0, 10, 20, 50, 100, 200, 500, 1000, Inf)
pal <- colorBin("YlOrRd", domain = states$density, bins = bins)

labels <- sprintf(
  "<strong>%s</strong><br/>%g people / mi<sup>2</sup>",
  states$name, states$density
) %>% lapply(htmltools::HTML)

leaflet(states) %>%
  setView(-96, 37.8, 4) %>%
  addProviderTiles("MapBox", options = providerTileOptions(
    id = "mapbox.light",
    accessToken = Sys.getenv('MAPBOX_ACCESS_TOKEN'))) %>%
  addPolygons(
    fillColor = ~pal(density),
    weight = 2,
    opacity = 1,
    color = "white",
    dashArray = "3",
    fillOpacity = 0.7,
    highlightOptions = highlightOptions(
      weight = 5,
      color = "#666",
      dashArray = "",
      fillOpacity = 0.7,
      bringToFront = TRUE),
    label = labels,
    labelOptions = labelOptions(
      style = list("font-weight" = "normal", padding = "3px 8px"),
      textsize = "15px",
      direction = "auto")) %>%
  addLegend(pal = pal, values = ~density, opacity = 0.7, title = NULL,
    position = "bottomright")

We wrote an end-to-end Mapping with R course on LinkedIn Learning that gets deep into using {leaflet} for interactive mapping.

Our favourite choice (from several) is the {visNetwork} package for interactive network visualisation.

Code
library(visNetwork)
nodes <- data.frame(id = 1:6, title = paste("node", 1:6), 
                    shape = c("dot", "square"),
                    size = 10:15, color = c("blue", "red"))
edges <- data.frame(from = 1:5, to = c(5, 4, 6, 3, 3))
visNetwork(nodes, edges) %>%
  visOptions(highlightNearest = TRUE, nodesIdSelection = TRUE)

Move your cursor to get the best out of this chart. It’s built with {upsetjs} which implements the UpSet Chart invented in 2012 DOI:10.1109/TVCG.2014.2346248

Code
library("upsetjs")
starwars_films <- starwars |> 
  filter(species == "Droid") |> 
  select(name, films) |> 
  unnest(films) |> 
  pivot_wider(names_from = name,
              values_from = name) |> 
  select(-films) |> 
  mutate(dplyr::across(dplyr::everything(), ~ dplyr::if_else(is.na(.x), 0, 1)))

upsetjs() %>% fromDataFrame(starwars_films) |> 
  generateDistinctIntersections() |> 
  upsetjs::interactiveChart() |>
  upsetjs::chartLabels(combination.name = "Times appearing together", set.name = "Movies with droid")

This could well be quite exciting for you! There’s suddenly a whole universe of interactive dataviz available to you.

So how does {htmlwidgets} work?

The {htmlwidgets} package is a framework that R developers can use to build another R package that wraps a JavaScript library. That’s how it works.

  • {plotly} wraps the Plotly JavaScript library.

  • {leaflet} wraps the Leaflet JavaScript library.

  • and so on… except for some {htmlwidgets} packages that purposefully wrap small parts of a larger JavaScript framework like {timevis} for interactive timelines.

It’s quite fun and not too difficult to build your own {htmlwidgets} package. We gave a demo of this in our Shiny in Production 2025 talk

Plotly might not be the best tool for you (anyway)

You might be using {plotly} in government or charity use. Accessibility should be something that is front and centre in mind when building your dataviz. Plotly have an interesting approach to accessibility. They’ve sought community sponsorship to add basic accessibility features.

It can also be quite difficult using a {plotly} chart on a mobile device. It’s very common to see {plotly} charts in Shiny apps that work fine on desktop but are squashed, difficult to read and not great to jab at with your finger. The Plotly touch points are very small.

We’re delighted to make some alternative recommendations in a moment. But the number one thing we hear people say is “{plotly} makes {ggplot2} charts interactive!”

Plotly automagical {ggplot2} conversion

You know, it is very neat that you can use ggplotly() to automagically create an interactive chart from a {ggplot2} chart. Carson Sievert who built the {plotly} package has made something that often feels very magical.

Code
library("tidyverse")
library("plotly")
library("fivethirtyeight")
library("scales")

gg_for_plotly <- bechdel |> 
  ggplot() +
  aes(x = budget_2013,
      y = domgross_2013) +
  geom_point(aes(fill = clean_test, 
                 text = paste0(title, "\nProfitability: ", label_percent(accuracy = 1)(domgross_2013 / budget_2013)), 
                 alpha = if_else(domgross_2013 >= budget_2013, "profitable", "not profitable")),
             pch = 21,
             stroke = 0.2) +
  geom_function(fun = function(x) x, linetype = 3) +
  scale_alpha_manual(values = c("profitable" = 1, "not profitable" = 0.4), guide = guide_none()) +
  scale_x_continuous(limits = c(0, 0.3E9)) +
  scale_y_continuous(limits = c(0, 0.3E9)) +
  facet_wrap(~ clean_test) +
  theme(legend.position = "none")

ggplotly(gg_for_plotly)

Ah. But those tooltips aren’t great.

  • There’s too much info in the movie dots

  • The break-even lines shouldn’t have tooltips

To tidy up the tooltips I used this StackOverflow answer to figure out which traces should have the hoverinfo set to none:

n_clean_tests <- length(unique(bechdel$clean_test))

ggplotly(gg_for_plotly, tooltip = c("text")) |> 
  style(hoverinfo = "none", traces = {n_clean_tests * 3 + 1}:{n_clean_tests * 3 + 5})

That really doesn’t feel very much like the grammar of graphics behind {ggplot2}.

So, let us introduce you to an alternative choice, {ggiraph}:

Code
library("ggiraph")

gg_for_ggiraph <- bechdel |> 
  ggplot() +
  aes(x = budget_2013,
      y = domgross_2013) +
  geom_point_interactive(aes(fill = clean_test, 
                             tooltip = paste0(title, "<br/>Profitability: ", label_percent(accuracy = 1)(domgross_2013 / budget_2013)), 
                             alpha = if_else(domgross_2013 >= budget_2013, "profitable", "not profitable"),
                             data_id = imdb),
                         pch = 21,
                         stroke = 0.2) +
  geom_function(fun = function(x) x, linetype = 3) +
  scale_alpha_manual(values = c("profitable" = 1, "not profitable" = 0.4), guide = guide_none()) +
  scale_x_continuous(limits = c(0, 0.3E9)) +
  scale_y_continuous(limits = c(0, 0.3E9)) +
  facet_wrap(~ clean_test) +
  theme(legend.position = "none")

girafe(ggobj = gg_for_ggiraph)


We love {ggiraph}. It’s absolutely incredible and criminally under represented.

Cara Thompson built this beautiful interactive timeline with {ggiraph}. Hover over the data points - look at those lovely tooltips!

Code
set.seed(202501)

theme_piccia <- function() {
  theme_minimal(base_size = 12) +
    theme(
      text = element_text(family = "Noah", colour = "#12051C"),
      plot.title = ggtext::element_textbox_simple(
        face = "bold",
        size = rel(1.5),
        margin = margin(12, 0, 12, 0, "pt")
      ),
      axis.text = element_text(family = "Noah", colour = "#372D40"),
      panel.grid = element_line(colour = "#FFFFFF"),
      panel.grid.major.x = element_blank(),
      panel.grid.minor = element_blank(),
      axis.title = element_blank(),
      plot.background = element_rect(colour = "#F7F4F6", fill = "#F7F4F6"),
      # Give everything a bit more space to breathe
      plot.margin = margin(rep(12, 4))
    )
}


piccia_palette <- c(
  purple = "#5c1c8d",
  lilac = "#be77bf",
  dark_pink = "#cd0d72",
  light_pink = "#f76bcc",
  orange = "#b4612b",
  emerald = "#3ca465",
  forest_green = "#1c4d3b",
  blue_grey = "#55517e"
)

many_trees <- Orange |>
  dplyr::mutate(Tree = as.character(Tree)) |>
  rbind(dplyr::tibble(
    Tree = c(rep("6", 7), rep("7", 7), rep("8", 7)),
    age = rep(unique(Orange$age), 3),
    circumference = c(
      sort(sample(25:250, 7)),
      sort(sample(25:250, 7)),
      sort(sample(25:250, 7))
    )
  ))

interactive_lines <- many_trees |>
  dplyr::group_by(Tree) |>
  dplyr::mutate(
    total_growth = max(circumference) - min(circumference),
    orchard = dplyr::case_when(
      Tree %in% c("8", "7", "6", "4") ~ "Greenleaf Citrus Farm",
      TRUE ~ "Purple Horizon Orchards"
    ),
    plantation_year = dplyr::case_when(
      Tree %in% c("6", "3") ~ 2021,
      Tree %in% c("4", "2") ~ 2022,
      Tree %in% c("7", "5") ~ 2023,
      TRUE ~ 2024
    ),
    tooltip_text = paste0(
      "<b>Tree #",
      Tree,
      "</b> planted in ",
      plantation_year,
      "<br>in ",
      orchard,
      "<br><br><b>Age</b> ",
      format(age, big.mark = ","),
      " days | <b>Circumference</b> ",
      circumference,
      "mm"
    )
  ) |>
  ggplot(aes(x = age, y = circumference, colour = orchard, group = Tree)) +
  geom_line(linewidth = 2.5, alpha = 0.5, colour = "grey") +
  geom_line(aes(alpha = plantation_year), linewidth = 2.5) +
  ggiraph::geom_point_interactive(
    aes(tooltip = tooltip_text, data_id = Tree, alpha = plantation_year),
    shape = 21,
    size = 2.5,
    fill = "#FFFFFF",
    stroke = 2
  ) +
  scale_alpha_continuous(range = c(0.05, 1), transform = "identity") +
  labs(
    title = "Charting the increase in tree circumference by age, by orchard and by year"
  ) +
  scale_x_continuous(labels = function(x) paste(x, "days")) +
  scale_y_continuous(labels = function(x) paste(x, "mm"), limits = c(0, 250)) +
  scale_colour_manual(
    values = c(
      "Greenleaf Citrus Farm" = piccia_palette[["forest_green"]],
      "Purple Horizon Orchards" = piccia_palette[["purple"]]
    )
  ) +
  ggtext::geom_textbox(
    data = head(many_trees, 1),
    aes(
      x = 1000,
      y = 225,
      label = paste0(
        "In <span style='color: ",
        piccia_palette[["forest_green"]],
        "'>**Greenleaf Citrus Farm**</span> the most recently planted tree (the darkest line) saw the most consitent growth..."
      )
    ),
    family = "Noah",
    size = 4.5,
    box.colour = NA,
    fill = NA,
    colour = "#372D40",
    width = unit(14, "lines")
  ) +
  ggtext::geom_textbox(
    data = head(many_trees, 1),
    aes(
      x = 1400,
      y = 85,
      label = paste0(
        "... while in <span style='color: ",
        piccia_palette[["purple"]],
        "'>**Purple Horizon Orchard**</span> we were back to square one."
      )
    ),
    family = "Noah",
    size = 4.5,
    box.colour = NA,
    fill = NA,
    colour = "#372D40"
  ) +
  theme_piccia() +
  theme(legend.position = "none", legend.title = element_blank())

ggiraph::girafe(
  ggobj = interactive_lines,
  options = list(
    ggiraph::opts_tooltip(
      opacity = 0.92,
      css = "background-color:#372D40;font-size:0.9em;color:#f9f9f7;padding:0.9em;letter-spacing:0.03em;border-radius:10px;font-family:Noah;max-width:350px;"
    ),
    ggiraph::opts_hover(css = "r:4pt;")
  )
)


From our perspective there are a few reasons to favour {ggiraph} over {plotly}:

  • The ggplotly() function does complicated magic to convert ggplot2 charts to plotly charts. It can often go very wrong and you spend more time trying to fix that than making the chart.

  • {ggiraph} honours your ggplot2 theme choices whereas you’ll need to fight against Plotly’s styles with ggplotly()

  • {ggiraph} generates SVG which gives you lots of flexbility, including opening up accessibility options

  • {plotly} has extremely limited tooltip options. You can fully customise them, even including other {htmlwidgets} within the tooltip

Check out Kyle Cuilla’s incredibly {ggiraph} choropleth that includes tables within tooltips

Recommendations for other {htmlwidgets}

We’re running a free workshop on looking beyond plotly for interactive dataviz with R. In the workshop we’ll discuss everything from recommended packages, shiny compatability and expanding your view of what’s possible with dataviz in R. We’d love you to bring questions and during the workshop we’ll do as much live coding as possible.

But let’s summarise our recommendations:

Code
library("highcharter")
data(favorite_bars)
data(favorite_pies)

highchart() |> 
  # Data
  hc_add_series(
    favorite_pies, 
    "column",
    hcaes(
      x = pie,
      y = percent
      ),
    name = "Pie"
    ) |>
  hc_add_series(
    favorite_bars,
    "pie",
    hcaes(
      name = bar,
      y = percent
      ),
    name = "Bars"
    ) |>
  # Options for each type of series
  hc_plotOptions(
    series = list(
      showInLegend = FALSE,
      pointFormat = "{point.y}%",
      colorByPoint = TRUE
      ),
    pie = list(
      center = c('30%', '10%'),
      size = 120,
      dataLabels = list(enabled = FALSE)
      )
    ) |>
  # Axis
  hc_yAxis(
    title = list(text = "percentage of tastiness"),
    labels = list(format = "{value}%"), 
    max = 100
  ) |> 
  hc_xAxis(
    categories = favorite_pies$pie
    ) |>
  # Titles, subtitle, caption and credits
  hc_title(
    text = "How I Met Your Mother: Pie Chart Bar Graph"
  ) |> 
  hc_subtitle(
    text = "This is a bar graph describing my favorite pies
    including a pie chart describing my favorite bars"
  ) |>
  hc_credits(
    enabled = TRUE, text = "Source: HIMYM",
    href = "https://www.youtube.com/watch?v=f_J8QU1m0Ng",
    style = list(fontSize = "12px")
  ) 

But plotly is mission critical for us!

We get you. The Plotly company have abandoned R. The {plotly} package itself is maintained by the wonderful Carson Sievert and his employer Posit have spoken about transitioning docs from the Plotly company to elsewhere.

That’s great. And really the 10,000+ daily downloads of {plotly} from CRAN demonstrate how great a job Carson has done with the package. However, the Plotly company dropping official support is a bad sign. We also feel sad that folks don’t talk about how it’s {htmlwidgets} that makes {plotly} work and there are 133+ registered {htmlwidgets} empowering all sorts of interactive dataviz!

Hopefully you now feel a little bit more knowledgeable (and excited?) about interactive dataviz with R and might want to look at how you could grow beyond Plotly with our workshop in mid January