Plotly won't make a discrete legend for you, but you can still make it happen.
First, I assigned both the ggplot and ggplotly to objects.
plt <- ggplotly(
ggplot(data=mtcars,
aes(x=mpg, y=cyl, color=qsec)) +
geom_point()
)
g <- ggplot(data=mtcars,
aes(x=mpg, y=cyl, color=qsec)) +
geom_point()
Next, use the data behind the ggplot object, combined with mtcars, to get a color by qsec data frame, so that you know what colors go with what values.
colByVal <- cbind(ggplot_build(g)$data[[1]], mtcars) %>%
as.data.frame() %>%
select(colour, qsec) %>% arrange(qsec) %>%
group_by(colour) %>%
summarise(qsec = median(qsec)) %>% as.data.frame()
I figured that four or five values would be ideal. I just used summary to pick them. However, that's not necessary. Obviously, you can choose however many values you would like. These are the values I'll show in the legend.
parts <- summary(colByVal$qsec)
# drop the mean or median (the same color probably)
parts <- parts[-4]
Next, use DescTools::Closest to find the qsec values closest to the summary values.
vals <- lapply(parts, function(k) {
DescTools::Closest(colByVal$qsec, k)[1]
}) %>% unlist(use.names = F)
Use these qsec values and the data frame with value by color to get the colors associated with these values.
cols <- colByVal %>%
filter(qsec %in% vals) %>% select(colour) %>%
unlist(use.names = F)
Using the colors and values (legend labels), use shapes and annotations (circles and text) to rebuild the legend. There is only one other element that needs to change between each legend item, the y position of the legend entry.
ys <- seq(from = .7, by = .07, length.out = length(cols))
There are two functions: shapes and annotations. Using lapply, walk through the values, colors, and y values through these functions to create the shapes and annotations.
# create shapes
shp <- function(y, cr) { # y0, and fillcolor
list(type = "circle",
xref = "paper", x0 = 1.1, x1 = 1.125,
yref = "paper", y0 = y, y1 = y + .025,
fillcolor = cr, yanchor = "center",
line = list(color = cr))
}
# create labels
ano <- function(ya, lab) { # y and label
list(x = 1.13, y = ya + .035, text = lab,
xref = "paper", yref = "paper",
xanchor = "left", yanchor = 'top',
showarrow = F)
}
# the shapes list
shps <- lapply(1:length(cols),
function(j) {
shp(ys[j], cols[j])
})
# the labels list
labs <- lapply(1:length(cols),
function(i) {
ano(ys[i], as.character(vals[i]))
})
When you use ggplotly, for some reason it ends an empty shape to the ggplotly object. This interferes with the ability to call for shapes in layout (which is the proper method). You have to force the issue with shapes. Additionally, the legend bar needs to go away. Once you drop the legend bar, Plotly will adjust the plot margins. The legend created with shapes and annotations will be hidden if you don't add the margins back.
# ggplot > ggplotly adds an empty shape; this conflicts with calling it in
# layout(); we'll replace 'shapes' first
plt$x$layout$shapes <- shps
plt %>% hide_colorbar() %>%
layout(annotations = labs, showlegend = F,
margin = list(t = 30, r = 100, l = 50, b = 30, pad = 3))

All of that code in one chunk:
library(tidyverse)
library(plotly)
# original plot
plt <- ggplotly(
ggplot(data=mtcars,
aes(x=mpg, y=cyl, color=qsec)) +
geom_point()
)
g <- ggplot(data=mtcars,
aes(x=mpg, y=cyl, color=qsec)) +
geom_point()
# color by qsec values frame
colByVal <- cbind(ggplot_build(g)$data[[1]], mtcars) %>%
as.data.frame() %>%
select(colour, qsec) %>% arrange(qsec) %>%
group_by(colour) %>%
summarise(qsec = median(qsec)) %>% as.data.frame()
parts <- summary(colByVal$qsec)
# drop the mean or median (the same color probably)
parts <- parts[-4]
vals <- lapply(parts, function(k) {
DescTools::Closest(colByVal$qsec, k)[1]
}) %>% unlist(use.names = F)
cols <- colByVal %>%
filter(qsec %in% vals) %>% select(colour) %>%
unlist(use.names = F)
ys <- seq(from = .7, by = .07, length.out = length(cols))
# create shapes
shp <- function(y, cr) { # y0, and fillcolor
list(type = "circle",
xref = "paper", x0 = 1.1, x1 = 1.125,
yref = "paper", y0 = y, y1 = y + .025,
fillcolor = cr, yanchor = "center",
line = list(color = cr))
}
# create labels
ano <- function(ya, lab) { # y and label
list(x = 1.13, y = ya + .035, text = lab,
xref = "paper", yref = "paper",
xanchor = "left", yanchor = 'top',
showarrow = F)
}
# the shapes list
shps <- lapply(1:length(cols),
function(j) {
shp(ys[j], cols[j])
})
# the labels list
labs <- lapply(1:length(cols),
function(i) {
ano(ys[i], as.character(vals[i]))
})
# ggplot > ggplotly adds an empty shape; this conflicts with calling it in
# layout(); we'll replace 'shapes' first
plt$x$layout$shapes <- shps
plt %>% hide_colorbar() %>%
layout(annotations = labs, showlegend = F,
margin = list(t = 30, r = 100, l = 50, b = 30, pad = 3))