英文:
plotly drops grouped legend (by color, by symbol) when converted from ggplot
问题
I don't quite understand why the legend disappeared when I converted a plot made by ggplot
to plotly
using ggplotly
. The plotly help page did not have any information. I don't think their examples even worked properly on that page. Any help is greatly appreciated!
Sample data
library(scales)
packageVersion("ggplot2")
#> [1] '3.4.0'
library(plotly)
packageVersion("plotly")
#> [1] '4.10.1'
data <- data.frame(
stringsAsFactors = FALSE,
Level = c("Fast","Fast","Fast","Fast",
"Fast","Fast","Slow","Slow","Slow",
"Slow","Slow","Slow"),
Period = c("1Year","3Month","1Year","3Month",
"1Year","3Month","1Year","3Month",
"1Year","3Month","1Year","3Month"),
X = c(0.002,0.002,0.1,0.1,0.9,0.9,
0.002,0.002,0.1,0.1,0.9,0.9),
Y = c(1.38,1.29,1.61,1.61,1.74,0.98,
1.14,0.97,1.09,1.1,0.94,0.58)
)
ggplot2
plt <- ggplot(data = data,
aes(x = X,
y = Y,
shape = Period,
color = Level)) +
geom_point(alpha = 0.6, size = 3) +
labs(x = " ",
y = "Value") +
scale_y_continuous(labels = number_format(accuracy = 0.1)) +
guides(color = guide_legend(title = "Level", order = 1),
shape = guide_legend(title = "Period", order = 2)) +
theme(axis.text.x = element_text(angle = 90))
plt
Convert to plotly, legend disappeared
ggplotly(plt, height = 500) %>%
layout(xaxis = list(autorange = "reversed"))
Edit
There was an issue with guides()
. If I removed it, the legend in ggplotly
showed up
plt2 <- ggplot(data = data,
aes(x = X,
y = Y,
shape = Period,
color = Level)) +
geom_point(alpha = 0.6, size = 3) +
labs(x = " ",
y = "Value") +
scale_y_continuous(labels = number_format(accuracy = 0.1)) +
theme(axis.text.x = element_text(angle = 90))
plt2
ggplotly(plt2, height = 500) %>%
layout(
xaxis = list(autorange = "reversed"),
legend = list(
title = list(text = '(Period, Level)'))
)
英文:
I don't quite understand why the legend disappeared when I converted a plot made by ggplot
to plotly
using ggplotly
. The plotly help page did not have any information. I don't think their examples even worked properly on that page.
Any help is greatly appreciated!
Sample data
library(scales)
packageVersion("ggplot2")
#> [1] '3.4.0'
library(plotly)
packageVersion("plotly")
#> [1] '4.10.1'
data <- data.frame(
stringsAsFactors = FALSE,
Level = c("Fast","Fast","Fast","Fast",
"Fast","Fast","Slow","Slow","Slow",
"Slow","Slow","Slow"),
Period = c("1Year","3Month","1Year","3Month",
"1Year","3Month","1Year","3Month",
"1Year","3Month","1Year","3Month"),
X = c(0.002,0.002,0.1,0.1,0.9,0.9,
0.002,0.002,0.1,0.1,0.9,0.9),
Y = c(1.38,1.29,1.61,1.61,1.74,0.98,
1.14,0.97,1.09,1.1,0.94,0.58)
)
ggplot2
plt <- ggplot(data = data,
aes(x = X,
y = Y,
shape = Period,
color = Level)) +
geom_point(alpha = 0.6, size = 3) +
labs(x = " ",
y = "Value") +
scale_y_continuous(labels = number_format(accuracy = 0.1)) +
guides(color = guide_legend(title = "Level", order = 1),
shape = guide_legend(title = "Period", order = 2)) +
theme(axis.text.x = element_text(angle = 90))
plt
<!-- -->
Convert to plotly, legend disappeared
ggplotly(plt, height = 500) %>%
layout(xaxis = list(autorange = "reversed"))
<!-- -->
Edit
There was an issue with guides()
. If I removed it, the legend in ggplotly
showed up
plt2 <- ggplot(data = data,
aes(x = X,
y = Y,
shape = Period,
color = Level)) +
geom_point(alpha = 0.6, size = 3) +
labs(x = " ",
y = "Value") +
scale_y_continuous(labels = number_format(accuracy = 0.1)) +
theme(axis.text.x = element_text(angle = 90))
plt2
ggplotly(plt2, height = 500) %>%
layout(
xaxis = list(autorange = "reversed"),
legend = list(
title = list(text = '(Period, Level)'))
)
<!-- -->
答案1
得分: 6
以下是代码部分的翻译:
这是使用基本的R {plotly} 的一种解决方法,根据 @Tung 的要求修改图例:
```R
library(scales)
library(ggplot2)
library(plotly)
library(data.table)
DT <- data.frame(
stringsAsFactors = FALSE,
Level = c("Fast","Fast","Fast","Fast",
"Fast","Fast","Slow","Slow","Slow",
"Slow","Slow","Slow"),
Period = c("1Year","3Month","1Year","3Month",
"1Year","3Month","1Year","3Month",
"1Year","3Month","1Year","3Month"),
X = c(0.002,0.002,0.1,0.1,0.9,0.9,
0.002,0.002,0.1,0.1,0.9,0.9),
Y = c(1.38,1.29,1.61,1.61,1.74,0.98,
1.14,0.97,1.09,1.1,0.94,0.58)
)
setDT(DT)
LevelDT <- unique(DT, by = "Level")
PeriodDT <- unique(DT, by = "Period")
LevelDT[, Y := min(DT$Y)-1]
PeriodDT[, Y := min(DT$Y)-1]
plt2 <- ggplot(data = DT,
aes(x = X,
y = Y,
shape = Period,
color = Level)) +
geom_point(alpha = 0.6, size = 3) +
labs(x = " ",
y = "Value") +
scale_y_continuous(labels = number_format(accuracy = 0.1)) +
theme(axis.text.x = element_text(angle = 90))
plt2
markercolors <- hue_pal()(2)
ggplotly(plt2, height = 500) |
layout(
xaxis = list(autorange = "reversed"),
legend = list(
title = list(text = ''),
itemclick = FALSE,
itemdoubleclick = FALSE,
groupclick = FALSE
)
) |
add_trace(
data = LevelDT,
x = ~ X,
y = ~ Y,
inherit = FALSE,
type = "scatter",
mode = "markers",
marker = list(
color = markercolors,
size = 14,
opacity = 0.6,
symbol = "circle"
),
name = ~ Level,
legendgroup = "Level",
legendgrouptitle = list(text = "Level")
) |
add_trace(
data = PeriodDT,
x = ~ X,
y = ~ Y,
inherit = FALSE,
type = "scatter",
mode = "markers",
marker = list(
color = "darkgrey",
size = 14,
opacity = 0.6,
symbol = c("circle", "triangle-up")
),
name = ~Period,
legendgroup = "Period",
legendgrouptitle = list(text = "Period")
) |> style(showlegend = FALSE, traces = 1:4)
希望这对你有帮助。如果有任何其他问题,请随时告诉我。
英文:
After OPs Edit:
Here is a workaround using basic R {plotly} to modify the legend according to @Tung's requirements:
library(scales)
library(ggplot2)
library(plotly)
library(data.table)
DT <- data.frame(
stringsAsFactors = FALSE,
Level = c("Fast","Fast","Fast","Fast",
"Fast","Fast","Slow","Slow","Slow",
"Slow","Slow","Slow"),
Period = c("1Year","3Month","1Year","3Month",
"1Year","3Month","1Year","3Month",
"1Year","3Month","1Year","3Month"),
X = c(0.002,0.002,0.1,0.1,0.9,0.9,
0.002,0.002,0.1,0.1,0.9,0.9),
Y = c(1.38,1.29,1.61,1.61,1.74,0.98,
1.14,0.97,1.09,1.1,0.94,0.58)
)
setDT(DT)
LevelDT <- unique(DT, by = "Level")
PeriodDT <- unique(DT, by = "Period")
LevelDT[, Y := min(DT$Y)-1]
PeriodDT[, Y := min(DT$Y)-1]
plt2 <- ggplot(data = DT,
aes(x = X,
y = Y,
shape = Period,
color = Level)) +
geom_point(alpha = 0.6, size = 3) +
labs(x = " ",
y = "Value") +
scale_y_continuous(labels = number_format(accuracy = 0.1)) +
theme(axis.text.x = element_text(angle = 90))
plt2
markercolors <- hue_pal()(2)
ggplotly(plt2, height = 500) |>
layout(
xaxis = list(autorange = "reversed"),
legend = list(
title = list(text = ''),
itemclick = FALSE,
itemdoubleclick = FALSE,
groupclick = FALSE
)
) |>
add_trace(
data = LevelDT,
x = ~ X,
y = ~ Y,
inherit = FALSE,
type = "scatter",
mode = "markers",
marker = list(
color = markercolors,
size = 14,
opacity = 0.6,
symbol = "circle"
),
name = ~ Level,
legendgroup = "Level",
legendgrouptitle = list(text = "Level")
) |>
add_trace(
data = PeriodDT,
x = ~ X,
y = ~ Y,
inherit = FALSE,
type = "scatter",
mode = "markers",
marker = list(
color = "darkgrey",
size = 14,
opacity = 0.6,
symbol = c("circle", "triangle-up")
),
name = ~Period,
legendgroup = "Period",
legendgrouptitle = list(text = "Period")
) |> style(showlegend = FALSE, traces = 1:4)
PS: Here the related plotly.js GitHub issue can be found.
Original answer:
I'm not sure why they are set to FALSE
in the first place, but setting showlegend = TRUE
in layout()
and style()
(for the traces) brings back the legend:
library(scales)
library(ggplot2)
library(plotly)
data <- data.frame(
stringsAsFactors = FALSE,
Level = c("Fast","Fast","Fast","Fast",
"Fast","Fast","Slow","Slow","Slow",
"Slow","Slow","Slow"),
Period = c("1Year","3Month","1Year","3Month",
"1Year","3Month","1Year","3Month",
"1Year","3Month","1Year","3Month"),
X = c(0.002,0.002,0.1,0.1,0.9,0.9,
0.002,0.002,0.1,0.1,0.9,0.9),
Y = c(1.38,1.29,1.61,1.61,1.74,0.98,
1.14,0.97,1.09,1.1,0.94,0.58)
)
# ggplot2
plt <- ggplot(data = data,
aes(x = X,
y = Y,
shape = Period,
color = Level)) +
geom_point(alpha = 0.6, size = 3) +
labs(x = " ",
y = "Value") +
scale_y_continuous(labels = number_format(accuracy = 0.1)) +
guides(color = guide_legend(title = "Period", order = 1),
shape = guide_legend(title = "", order = 2)) +
theme(axis.text.x = element_text(angle = 90))
plt
# Convert to plotly, legend disappeared
fig <- ggplotly(plt, height = 500) %>%
layout(showlegend = TRUE, xaxis = list(autorange = "reversed")) %>%
style(showlegend = TRUE)
fig
答案2
得分: 3
以下是翻译好的部分:
这个答案适用于 plotly 4.10.1
版本。我定义了两个函数:
set_legend_names()
这个函数用于编辑由ggplotly()
创建的htmlwidget
的名称,在传递给plotly.js
之前。set_legend_symbols()
这个函数附加一些 JavaScript 代码到htmlwidget
对象,用于在plotly.js
绘制后更改图例符号。
plt2 |>
ggplotly(height = 500) |>
layout(xaxis = list(autorange = "reversed")) |>
set_legend_names() |>
set_legend_symbols()
函数定义:
set_legend_names()
set_legend_names <- function(p,
new_legend_names = c(
"Fast", "Slow", "One Year", "Three Month"
)) {
# 更新图例名称并放入一个组中
for (i in seq_along(p$x$data)) {
p$x$data[[i]]$name <- new_legend_names[i]
}
p$x$layout$legend$title <- ""
return(p)
}
set_legend_symbols()
set_legend_symbols <- function(p,
symbol_nums_change_color = c(3, 4),
new_color_string = "rgb(105, 105, 105)",
symbols_num_change_shape = 3,
symbols_nums_target_shape = 1) {
js_get_legend <- htmltools::HTML(
'let legend = document.querySelector(".scrollbox");
let symbols = legend.getElementsByClassName("legendsymbols");
const re = new RegExp("fill: rgb.+;", "i");\n
'
)
js_symbol_const <- paste0(
'const shape_re = new RegExp(\'d=".*?"\');\n',
"const correct_shape = symbols[",
symbols_nums_target_shape,
"].innerHTML.match(shape_re)[0];\n"
)
# 减去1以进行0索引的 JavaScript
change_symbol_color_code <- lapply(
symbol_nums_change_color - 1,
\(i)
paste0(
"symbols[", i, "].innerHTML = ",
"symbols[", i, "].innerHTML.replace(re,",
' "fill: ', new_color_string, ';");'
)
) |>
paste(collapse = "\n")
# 减去1以进行0索引的 JavaScript
change_symbols_shape_code <- lapply(
symbols_num_change_shape - 1,
\(i)
paste0(
"symbols[", i, "].innerHTML = symbols[",
symbols_nums_target_shape, "].innerHTML.replace(shape_re, correct_shape);"
)
) |>
paste(collapse = "\n")
all_js <- htmltools::HTML(
unlist(c(
js_get_legend,
js_symbol_const,
change_symbols_shape_code,
change_symbol_color_code
))
)
# 将其添加到绘图中
p <- htmlwidgets::prependContent(
p,
htmlwidgets::onStaticRenderComplete(all_js)
)
return(p)
}
我以前从未发布过第二个答案,但在 plotly 4.10.1
中似乎有明显的不同之处。我迫不及待地期待 plotly 4.10.2
的发布,以便发布第三个答案。
英文:
This answer is for plotly 4.10.1
. I have defined two functions:
set_legend_names()
This edits the names of thehtmlwidget
created byggplotly()
, before it is passed toplotly.js
.set_legend_symbols()
. This appends some js to thehtmlwidget
object to change the symbols afterplotly.js
has drawn them.
plt2 |>
ggplotly(height = 500) |>
layout(xaxis = list(autorange = "reversed")) |>
set_legend_names() |>
set_legend_symbols()
Function definitions:
1. set_legend_names()
set_legend_names <- function(p,
new_legend_names = c(
"Fast", "Slow", "One Year", "Three Month"
)) {
# Update legend names and put in one group
for (i in seq_along(p$x$data)) {
p$x$data[[i]]$name <- new_legend_names[i]
}
p$x$layout$legend$title <- ""
return(p)
}
2. set_legend_symbols()
set_legend_symbols <- function(p,
symbol_nums_change_color = c(3, 4),
new_color_string = "rgb(105, 105, 105)",
symbols_num_change_shape = 3,
symbols_nums_target_shape = 1) {
js_get_legend <- htmltools::HTML(
'let legend = document.querySelector(".scrollbox");
let symbols = legend.getElementsByClassName("legendsymbols");
const re = new RegExp("fill: rgb.+;", "i");\n
'
)
js_symbol_const <- paste0(
'const shape_re = new RegExp(\'d=".*?"\');\n',
"const correct_shape = symbols[",
symbols_nums_target_shape,
"].innerHTML.match(shape_re)[0];\n"
)
# subtract 1 for 0-indexed js
change_symbol_color_code <- lapply(
symbol_nums_change_color - 1,
\(i)
paste0(
"symbols[", i, "].innerHTML = ",
"symbols[", i, "].innerHTML.replace(re,",
' "fill: ', new_color_string, ';");'
)
) |>
paste(collapse = "\n")
# subtract 1 for 0-indexed js
change_symbols_shape_code <- lapply(
symbols_num_change_shape - 1,
\(i)
paste0(
"symbols[", i, "].innerHTML = symbols[",
symbols_nums_target_shape, "].innerHTML.replace(shape_re, correct_shape);"
)
) |>
paste(collapse = "\n")
all_js <- htmltools::HTML(
unlist(c(
js_get_legend,
js_symbol_const,
change_symbols_shape_code,
change_symbol_color_code
))
)
# Add it to the plot
p <- htmlwidgets::prependContent(
p,
htmlwidgets::onStaticRenderComplete(all_js)
)
return(p)
}
I've never posted a second answer before but it seems substantially different in plotly 4.10.1
. I eagerly anticipate the release of plotly 4.10.2
so I can post a third answer.
答案3
得分: 2
Plotly生成的图例与ggplot2
不同 - 可以使用R和一点JavaScript来修复
首先要做的是确保您拥有相对较新的软件包版本:
packageVersion("ggplot2") # 3.4.0
packageVersion("plotly") # 4.10.0
有了这些版本,就像@Quentin一样,我确实获得了一个图例,尽管它与ggplot2
生成的图例不同。
ggplotly(plt, height = 500) %>%
layout(xaxis = list(autorange = "reversed"))
复制ggplot2
图例的步骤:
- 更改图例文本。这可以通过在将其传递给
plotly.js
之前编辑R对象来完成。 - 删除
shape
指南中的颜色。这只能在绘图渲染后使用JavaScript完成。 - 将第三个圆形更改为三角形。这也需要在JavaScript中完成。
更改图例文本
要手动完成这一步,我们可以执行 p$x$data[[1]]$name <- "Fast"
,并为每个图层重复此操作。
幸运的是,您已手动指定了图例顺序,因此在传递给plotly
之前知道在哪里访问正确的图例名称,如果只完成这一步,它将创建一个看起来像这样的图例,即仍然是错误的(第一个三角形应该是一个圆形,两者都不应该有颜色)。
更改符号形状和颜色
我们无法在R中完成这个操作。我已经编写了一个R辅助函数,生成一些JavaScript代码来完成这个操作:
get_symbol_change_js <- function(symbol_nums,
new_color_string = "rgb(105, 105, 105)") {
js_get_legend <- htmltools::HTML(
'let legend = document.querySelector(".scrollbox");
let symbols = legend.getElementsByClassName("legendsymbols");
const re = new RegExp("fill: rgb.+;", "i");
'
)
change_symbol_color_code <- lapply(
symbol_nums,
\(i)
paste0(
"symbols[", i, "].innerHTML = ",
"symbols[", i, "].innerHTML.replace(re,",
' "', new_color_string, '";);'
)
) |>
paste(collapse = "\n")
# 要更改的形状
shape_change_num <- symbol_nums[1]
# 要替换的形状
shape_change_from <- shape_change_num - 1
change_symbols_shape_code <- paste0(
'const shape_re = new RegExp(\'d=".*?"\');\n',
"const correct_shape = symbols[", shape_change_from, "].innerHTML.match(shape_re)[0];\n",
"symbols[2].innerHTML = symbols[", shape_change_num, "].innerHTML.replace(shape_re, correct_shape);"
)
all_js <- htmltools::HTML(
unlist(c(
js_get_legend,
change_symbol_color_code,
change_symbols_shape_code
))
)
return(all_js)
}
我们可以将所有这些内容放在一起以生成所需的绘图:
draw_plotly_with_legend(plt)
最终的draw_plotly_with_legend()
函数
请注意,此函数调用了上面定义的 get_symbol_change_js()
。它还使用 htmlwidgets::prependContent()
在渲染之前附加我们的自定义HTML到小部件。
draw_plotly_with_legend <- function(gg = plt,
guide_types = c("colour", "shape")) {
# 期间、级别
legend_categories <- lapply(
guide_types, \(x) rlang::quo_get_expr(plt$mapping[[x]])
)
new_legend_names <- lapply(legend_categories, \(category) {
unique(data[[category]])
}) |> setNames(guide_types)
# 找出需要去除颜色的符号
symbols_to_remove_color <- new_legend_names[
names(new_legend_names) != "colour"
] |> unlist()
new_legend_names <- unlist(new_legend_names)
symbol_num_remove_color <- which(
new_legend_names %in% symbols_to_remove_color
)
# 创建绘图
p <- ggplotly(gg, height = 500) %>%
layout(xaxis = list(autorange = "reversed"))
# 显示图例
p$x$layout$showlegend <- TRUE
# 更新图例名称并放入一个组中
for (i in seq_along(p$x$data)) {
p$x$data[[i]]$name <- new_legend_names[i]
p$x$data[[1]]$legendgroup <- "Grouped legend"
}
# 获取更改图例颜色的js代码
# js是从0开始索引的
js_symbol_nums <- symbol_num_remove_color - 1
js_code <- get_symbol_change_js(js_symbol_nums)
# 将其添加到绘图中
p <- htmlwidgets::prependContent(
p,
htmlwidgets::onStaticRenderComplete(js_code)
)
return(p)
}
英文:
Plotly generates a different legend from ggplot2
- this can be fixed with R and and a little javascript
The first thing to do is ensure that you have a reasonably current version of the packages:
packageVersion("ggplot2") # 3.4.0
packageVersion("plotly") # 4.10.0
With these versions, like @Quentin, I do get a legend, although it is different to the one generated by ggplot2
.
ggplotly(plt, height = 500) %>%
layout(xaxis = list(autorange = "reversed"))
Steps to replicate the ggplot2
legend:
- Change the legend text. This can be done by editing the R object before it is passed to
plotly.js
. - Remove the color from the
shape
guide. This can only be done with javascript after the plot has rendered. - Change the third circle into a triangle. This also needs to be done in javascript.
Changing the legend text
To do this manually, we could do p$x$data[[1]]$name <- "Fast"
, and replicate for each layer.
Fortunately, you have manually specified the legend order, making it easy to know where to access the correct legend names before passing to plotly
. If we just do this step, it will create a legend which looks like this, i.e. still wrong (the first triangle should be a circle and neither should be have a color):
Changing the symbol shape and colors
We cannot do this in R. I have written an R helper function to generate some javascript to do this for us:
get_symbol_change_js <- function(symbol_nums,
new_color_string = "rgb(105, 105, 105)") {
js_get_legend <- htmltools::HTML(
'let legend = document.querySelector(".scrollbox");
let symbols = legend.getElementsByClassName("legendsymbols");
const re = new RegExp("fill: rgb.+;", "i");
'
)
change_symbol_color_code <- lapply(
symbol_nums,
\(i)
paste0(
"symbols[", i, "].innerHTML = ",
"symbols[", i, "].innerHTML.replace(re,",
' "fill: ', new_color_string, ';");'
)
) |>
paste(collapse = "\n")
# shape to change
shape_change_num <- symbol_nums[1]
# shape to replace with
shape_change_from <- shape_change_num - 1
change_symbols_shape_code <- paste0(
'const shape_re = new RegExp(\'d=".*?"\');\n',
"const correct_shape = symbols[", shape_change_from, "].innerHTML.match(shape_re)[0];\n",
"symbols[2].innerHTML = symbols[", shape_change_num, "].innerHTML.replace(shape_re, correct_shape);"
)
all_js <- htmltools::HTML(
unlist(c(
js_get_legend,
change_symbol_color_code,
change_symbols_shape_code
))
)
return(all_js)
}
We can put this all together to generate the plot as desired:
draw_plotly_with_legend(plt)
Final draw_plotly_with_legend()
function
Note this function calls get_symbol_change_js()
, as defined above. It also uses htmlwidgets::prependContent()
to attach our custom html to the widget before rendering.
draw_plotly_with_legend <- function(gg = plt,
guide_types = c("colour", "shape")) {
# Period, Level
legend_categories <- lapply(
guide_types, \(x) rlang::quo_get_expr(plt$mapping[[x]])
)
new_legend_names <- lapply(legend_categories, \(category) {
unique(data[[category]])
}) |> setNames(guide_types)
# Work out which symbols need to have color removed
symbols_to_remove_color <- new_legend_names[
names(new_legend_names) != "colour"
] |> unlist()
new_legend_names <- unlist(new_legend_names)
symbol_num_remove_color <- which(
new_legend_names %in% symbols_to_remove_color
)
# Create plot
p <- ggplotly(gg, height = 500) %>%
layout(xaxis = list(autorange = "reversed"))
# Show legend
p$x$layout$showlegend <- TRUE
# Update legend names and put in one group
for (i in seq_along(p$x$data)) {
p$x$data[[i]]$name <- new_legend_names[i]
p$x$data[[1]]$legendgroup <- "Grouped legend"
}
# Get the js code to change legend color
# js is 0 indexed
js_symbol_nums <- symbol_num_remove_color - 1
js_code <- get_symbol_change_js(js_symbol_nums)
# Add it to the plot
p <- htmlwidgets::prependContent(
p,
htmlwidgets::onStaticRenderComplete(js_code)
)
return(p)
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论