Skip to content

Commit e970a83

Browse files
committed
Fix #2407, #2187: legend.position theme element now translated
1 parent b2f7d3d commit e970a83

File tree

3 files changed

+80
-2
lines changed

3 files changed

+80
-2
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ See the [plotly.js releases page](https://github.com/plotly/plotly.js/releases)
3131
* Closed #2466: `ggplotly()` no longer errors when `scale_*_manual()` has unused aesthetics (e.g., `aesthetics = c("colour", "fill")` when only colour is used).
3232
* Closed #2305: `ggplotly()` now respects `geom_boxplot(outlier.shape = NA)` to hide outlier points.
3333
* Closed #2467: `ggplotly()` now correctly shows legends and splits traces when scales have multiple aesthetics.
34+
* Closed #2407, #2187: `ggplotly()` now translates `legend.position` theme element to plotly layout (supports "bottom", "top", "left", and numeric positions).
3435

3536
# plotly 4.11.0
3637

R/ggplotly.R

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -979,12 +979,47 @@ gg2list <- function(p, width = NULL, height = NULL,
979979
bgcolor = toRGB(theme$legend.background$fill),
980980
bordercolor = toRGB(theme$legend.background$colour),
981981
borderwidth = unitConvert(
982-
theme$legend.background[[linewidth_or_size(theme$legend.background)]],
982+
theme$legend.background[[linewidth_or_size(theme$legend.background)]],
983983
"pixels", "width"
984984
),
985985
font = text2font(theme$legend.text)
986986
)
987-
987+
988+
# Translate legend.position from ggplot2 theme to plotly layout (fixes #2407, #2187)
989+
legend_pos <- theme$legend.position %||% theme[["legend.position"]]
990+
if (!is.null(legend_pos) && !identical(legend_pos, "none")) {
991+
if (is.character(legend_pos)) {
992+
gglayout$legend <- switch(legend_pos,
993+
"bottom" = modifyList(gglayout$legend, list(
994+
orientation = "h", x = 0.5, y = -0.15, xanchor = "center", yanchor = "top"
995+
)),
996+
"top" = modifyList(gglayout$legend, list(
997+
orientation = "h", x = 0.5, y = 1.02, xanchor = "center", yanchor = "bottom"
998+
)),
999+
"left" = modifyList(gglayout$legend, list(
1000+
x = -0.15, y = 0.5, xanchor = "right", yanchor = "middle"
1001+
)),
1002+
"inside" = {
1003+
# In ggplot2 >= 3.5.0, numeric position is stored in legend.position.inside
1004+
inside_pos <- theme$legend.position.inside %||% theme[["legend.position.inside"]]
1005+
if (is.numeric(inside_pos) && length(inside_pos) == 2) {
1006+
modifyList(gglayout$legend, list(
1007+
x = inside_pos[1], y = inside_pos[2], xanchor = "left", yanchor = "bottom"
1008+
))
1009+
} else {
1010+
gglayout$legend
1011+
}
1012+
},
1013+
gglayout$legend # "right" is default, no change needed
1014+
)
1015+
} else if (is.numeric(legend_pos) && length(legend_pos) == 2) {
1016+
# Handle numeric position like c(0.8, 0.2) for older ggplot2 versions
1017+
gglayout$legend <- modifyList(gglayout$legend, list(
1018+
x = legend_pos[1], y = legend_pos[2], xanchor = "left", yanchor = "bottom"
1019+
))
1020+
}
1021+
}
1022+
9881023
# if theme(legend.position = "none") is used, don't show a legend _or_ guide
9891024
if (npscales$n() == 0 || identical(theme$legend.position, "none")) {
9901025
gglayout$showlegend <- FALSE

tests/testthat/test-ggplot-theme.R

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,45 @@ test_that("element_blank panel.border does not create empty shapes (#2455, #2460
9090
expect_true(length(bw_shapes) > 0)
9191
expect_false(is.na(bw_shapes[[1]]$line$color))
9292
})
93+
94+
test_that("legend.position is translated correctly (#2407, #2187)", {
95+
# Test legend.position = "bottom"
96+
p_bottom <- ggplot(mtcars, aes(wt, mpg, color = factor(cyl))) +
97+
geom_point() +
98+
theme(legend.position = "bottom")
99+
L_bottom <- plotly_build(ggplotly(p_bottom))$x
100+
expect_equal(L_bottom$layout$legend$orientation, "h")
101+
expect_equal(L_bottom$layout$legend$xanchor, "center")
102+
expect_equal(L_bottom$layout$legend$y, -0.15)
103+
104+
# Test legend.position = "top"
105+
p_top <- ggplot(mtcars, aes(wt, mpg, color = factor(cyl))) +
106+
geom_point() +
107+
theme(legend.position = "top")
108+
L_top <- plotly_build(ggplotly(p_top))$x
109+
expect_equal(L_top$layout$legend$orientation, "h")
110+
expect_equal(L_top$layout$legend$y, 1.02)
111+
112+
# Test legend.position = "left"
113+
p_left <- ggplot(mtcars, aes(wt, mpg, color = factor(cyl))) +
114+
geom_point() +
115+
theme(legend.position = "left")
116+
L_left <- plotly_build(ggplotly(p_left))$x
117+
expect_equal(L_left$layout$legend$xanchor, "right")
118+
expect_equal(L_left$layout$legend$x, -0.15)
119+
120+
# Test legend.position = "none"
121+
p_none <- ggplot(mtcars, aes(wt, mpg, color = factor(cyl))) +
122+
geom_point() +
123+
theme(legend.position = "none")
124+
L_none <- plotly_build(ggplotly(p_none))$x
125+
expect_false(L_none$layout$showlegend)
126+
127+
# Test numeric legend.position
128+
p_custom <- ggplot(mtcars, aes(wt, mpg, color = factor(cyl))) +
129+
geom_point() +
130+
theme(legend.position = c(0.8, 0.2))
131+
L_custom <- plotly_build(ggplotly(p_custom))$x
132+
expect_equal(L_custom$layout$legend$x, 0.8)
133+
expect_equal(L_custom$layout$legend$y, 0.2)
134+
})

0 commit comments

Comments
 (0)