It looks like you're new to SO; welcome to the community! You've gotten off to a great start by including the code you used and the issue you ran into. If you want great answers quickly, it's best to make your question reproducible. This includes sample data like the output from dput()
or reprex::reprex()
, libraries you are using. Check it out: making R reproducible questions.
Because I don't have the data you're working with, I've used mtcars
. You'll have to adjust the code to match the data you're working with.
I want to point out a few things that might be useful.
some ggplot2
layers aren't built into ggplotly
; one such layer is stat_density_2d_filled
(when you run ggplotly
here, you should have seen a warning in the console, if you enter warnings()
you would see a message that tells you why it didn't do what you expected and
Plotly has something similar which can be called with the argument `type = "histogram2dcontour"; however, I don't feel that this is a very accurate representation of density (no matter how I set the available parameters)
Plotly area charts can be used along with the data that was created when you made your ggplot2
plot to recreate this in Plotly; however, it's important to note that whenever you're using fill
in Plotly it gets overly helpful and makes extremely poor guesses as to what you're trying to accomplish...so baby steps for Plotly here (I do want to mention that in most aspects of plotting, Plotly is a phenomenal tool...)
Using ggplot2
to get density data
First, I've made a simplified ggplot2
density plot similar to the plot in your question.
library(tidyverse)
library(plotly)
data("mtcars")
ggplot(mtcars, aes(x = mpg, y = cyl)) + geom_point() +
stat_density_2d_filled(aes(fill = after_stat(level)))
Next, you'll collect the data from this plot. Currently, you have two layers: geom_point
and stat_density_2d_filled
. That means when we collect the data from this plot, we'll have two data frames in the order you've plotted them. So, in this case, the first will be the scatter plot, and the latter will be the density plot.
You could add as many layers as you want, just keep in mind that we will be using the density plot data.
BTW: last_plot()
is in Plotly and ggplot2
's libraries, so it's important to keep the package name so you've captured the right plot.
# using ggplotly here won't work; stat_density.. is not implemented
# workaround:
gg <- ggplot2::last_plot() %>% ggplot_build() # capture plot
ggd <- gg$data # capture list of data frames
# [[1]] data is the scatter plot; [[2]] data is the density plot
Create the contour plot
Next, create the contours using the data taken from ggplot2
.
# create contour/shaded area plot using ggplot2 data
plt <- plot_ly(type = "scatter", mode = "lines",
split = ~subgroup, # keeps shading correct
legendgroup = ~level, # consolidate legend
name = ~level, # legend name
fill = "tonext", # connect to next coordinate for "fill"
data = ggd[[2]], x = ~x, y = ~y,
color = ~level, colors = ~fill, # legend: level, literal colors: fill
line = list(dash = "3px", width = .5)) # matches ggplot2 a bit better
Modify the contour plot
The colors, legend, and scatter points will be taken care of next. I've written a function that goes through all of the data traces (a trace in Plotly is equivalent to a layer in ggplot2
). I've added a lot of comments to explain what's happening and why. If you really wanted to understand what this function does, look at plt
before and after this function is used.
# since area plots are pretty finicky in Plotly, and Plotly loves to have the same
# legend entry over, and over, and over... and over again, the following function
# consolidates the legend, sets the `fillcolor`,
# and makes the contour lines light gray (as in ggplot2)
fixer <- function(pltr) {
pltr <- plotly_build(pltr) # build plot to view/mod data
lgs <- unique(ggd[[2]]$level) %>% as.character() # fix legend: 1 each name
invisible(lapply(1:length(pltr$x$data), function(i) { # each trace
nm <- as.character(pltr$x$data[[i]]$name) # get name (level)
if(any(lgs %in% nm)) { # if new name, keep in legend
pltr$x$data[[i]]$showlegend <<- T
lgs <<- lgs[!lgs %in% nm] # rem name from list
} else {
pltr$x$data[[i]]$showlegend <<- F # rem from legend
} # collect proper fill color
col <- filter(ggd[[2]], level == nm) %>% select(fill) %>% unique() %>% as.character()
pltr$x$data[[i]]$fillcolor <<- col # update fill
pltr$x$data[[i]]$line$color <<- "lightgray" # to match ggplot
}))
pltr # return updated plot
}
plt1 <- plt %>% fixer() # fixer to mod plot BEFORE adding markers
Lastly, add the scatter points
It's time to create and add the markers...and that's it.
# add markers (so lines, fill, markers don't collide--- plotly tries to "help" --- UGH)
p <- plot_ly(type = "scatter", mode = "markers", color = I("black"),
data = ggd[[1]], x = ~x, y = ~y, showlegend = F) %>% plotly_build()
plt1$x$data <- append(plt1$x$data, p$x$data[1], after = 0) # combine areas/markers
plt1 # check it out!

Summary
Here's all that same code you've already seen, but altogether (easier copy + paste)
library(tidyverse)
library(plotly)
data("mtcars")
ggplot(mtcars, aes(x = mpg, y = cyl)) + geom_point() +
stat_density_2d_filled(aes(fill = after_stat(level)))
# using ggplotly here won't work; stat_density.. is not implemented
# workaround:
gg <- ggplot2::last_plot() %>% ggplot_build() # capture plot
ggd <- gg$data # capture list of data frames
# [[1]] data is the scatter plot; [[2]] data is the density plot
# create contour/shaded area plot using ggplot2 data
plt <- plot_ly(type = "scatter", mode = "lines",
split = ~subgroup, # keeps shading correct
legendgroup = ~level, # consolidate legend
name = ~level, # legend name
fill = "tonext", # connect to next coordinate for "fill"
data = ggd[[2]], x = ~x, y = ~y,
color = ~level, colors = ~fill, # legend: level, literal colors: fill
line = list(dash = "3px", width = .5)) # matches ggplot2 a bit better
# since area plots are pretty finicky in Plotly, and Plotly loves to have the same
# legend entry over, and over, and over... and over again, the following function
# consolidates the legend, sets the `fillcolor`,
# and makes the contour lines light gray (as in ggplot2)
fixer <- function(pltr) {
pltr <- plotly_build(pltr) # build plot to view/mod data
lgs <- unique(ggd[[2]]$level) %>% as.character() # fix legend: 1 each name
invisible(lapply(1:length(pltr$x$data), function(i) { # each trace
nm <- as.character(pltr$x$data[[i]]$name) # get name (level)
if(any(lgs %in% nm)) { # if new name, keep in legend
pltr$x$data[[i]]$showlegend <<- T
lgs <<- lgs[!lgs %in% nm] # rem name from list
} else {
pltr$x$data[[i]]$showlegend <<- F # rem from legend
} # collect proper fill color
col <- filter(ggd[[2]], level == nm) %>% select(fill) %>% unique() %>% as.character()
pltr$x$data[[i]]$fillcolor <<- col # update fill
pltr$x$data[[i]]$line$color <<- "lightgray" # to match ggplot
}))
pltr # return updated plot
}
plt1 <- plt %>% fixer() # instantiate fixer to mod plot BEFORE adding markers
# add markers (so lines, fill, markers don't collide--- plotly tries to "help" --- UGH)
p <- plot_ly(type = "scatter", mode = "markers", color = I("black"),
data = ggd[[1]], x = ~x, y = ~y, showlegend = F) %>% plotly_build()
plt1$x$data <- append(plt1$x$data, p$x$data[1], after = 0) # combine areas/markers
plt1 # check it out!
If you run into any problems or have any questions, let me know.