Skip to content

Commit 1be6096

Browse files
committed
Fix #2281: geom_blank no longer drops legend for other geoms
1 parent e970a83 commit 1be6096

File tree

4 files changed

+33
-26
lines changed

4 files changed

+33
-26
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ See the [plotly.js releases page](https://github.com/plotly/plotly.js/releases)
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.
3434
* Closed #2407, #2187: `ggplotly()` now translates `legend.position` theme element to plotly layout (supports "bottom", "top", "left", and numeric positions).
35+
* Closed #2281: `ggplotly()` no longer drops legends when `geom_blank()` is present in the plot.
3536

3637
# plotly 4.11.0
3738

R/ggplotly.R

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,6 @@ gg2list <- function(p, width = NULL, height = NULL,
411411
# of each non-positional scale for display in tooltips
412412
for (sc in npscales$scales) {
413413
data <- lapply(data, function(d) {
414-
# Only process aesthetics that actually exist in this layer's data
415414
present_aes <- intersect(sc$aesthetics, names(d))
416415
if (length(present_aes) > 0) {
417416
d[paste0(present_aes, "_plotlyDomain")] <- d[present_aes]
@@ -573,13 +572,15 @@ gg2list <- function(p, width = NULL, height = NULL,
573572
tr$hoverinfo <- tr$hoverinfo %||%"text"
574573
tr
575574
})
576-
# show only one legend entry per legendgroup
575+
# show only one legend entry per legendgroup (skip invisible traces for dedup)
577576
grps <- sapply(traces, "[[", "legendgroup")
577+
is_visible <- sapply(traces, function(tr) !isFALSE(tr$visible))
578+
grps_for_dedup <- ifelse(is_visible, grps, paste0(grps, "_invisible_", seq_along(grps)))
578579
traces <- Map(function(x, y) {
579580
if (!is.null(x[["frame"]])) return(x)
580581
x$showlegend <- isTRUE(x$showlegend) && y
581582
x
582-
}, traces, !duplicated(grps))
583+
}, traces, !duplicated(grps_for_dedup))
583584

584585
# ------------------------------------------------------------------------
585586
# axis/facet/margin conversion
@@ -985,7 +986,7 @@ gg2list <- function(p, width = NULL, height = NULL,
985986
font = text2font(theme$legend.text)
986987
)
987988

988-
# Translate legend.position from ggplot2 theme to plotly layout (fixes #2407, #2187)
989+
# Translate legend.position to plotly layout
989990
legend_pos <- theme$legend.position %||% theme[["legend.position"]]
990991
if (!is.null(legend_pos) && !identical(legend_pos, "none")) {
991992
if (is.character(legend_pos)) {
@@ -1000,7 +1001,6 @@ gg2list <- function(p, width = NULL, height = NULL,
10001001
x = -0.15, y = 0.5, xanchor = "right", yanchor = "middle"
10011002
)),
10021003
"inside" = {
1003-
# In ggplot2 >= 3.5.0, numeric position is stored in legend.position.inside
10041004
inside_pos <- theme$legend.position.inside %||% theme[["legend.position.inside"]]
10051005
if (is.numeric(inside_pos) && length(inside_pos) == 2) {
10061006
modifyList(gglayout$legend, list(
@@ -1010,10 +1010,9 @@ gg2list <- function(p, width = NULL, height = NULL,
10101010
gglayout$legend
10111011
}
10121012
},
1013-
gglayout$legend # "right" is default, no change needed
1013+
gglayout$legend
10141014
)
10151015
} else if (is.numeric(legend_pos) && length(legend_pos) == 2) {
1016-
# Handle numeric position like c(0.8, 0.2) for older ggplot2 versions
10171016
gglayout$legend <- modifyList(gglayout$legend, list(
10181017
x = legend_pos[1], y = legend_pos[2], xanchor = "left", yanchor = "bottom"
10191018
))

R/layers2traces.R

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,7 @@ layers2traces <- function(data, prestats_data, layout, p) {
5656
if (!aesName %in% names(x)) next
5757
# TODO: should we be getting the name from scale_*(name) first?
5858
varName <- y[[i]]
59-
# Skip auto-generated group aesthetic (where both aes name and var name are "group"),
60-
# but allow: (1) variables named "group" mapped to other aesthetics like colour,
61-
# and (2) other variables mapped to the "group" aesthetic
59+
# Skip auto-generated group aesthetic, but keep explicit group mappings
6260
if (identical(aesName, "group") && identical(varName, "group")) next
6361
# add a line break if hovertext already exists
6462
if ("hovertext" %in% names(x)) x$hovertext <- paste0(x$hovertext, br())
@@ -77,11 +75,7 @@ layers2traces <- function(data, prestats_data, layout, p) {
7775
x
7876
}, data, hoverTextAes)
7977

80-
# draw legends only for discrete scales
81-
# Register each aesthetic separately for proper legend matching (fixes #2467)
82-
# When a scale has multiple aesthetics (e.g., c("colour", "fill")), we need
83-
# individual entries so "colour_plotlyDomain" matches discreteScales[["colour"]]
84-
# Skip identity scales (guide = "none") as they don't produce legends or trace splitting
78+
# draw legends only for discrete scales (skip scales with guide = "none")
8579
discreteScales <- list()
8680
for (sc in p$scales$non_position_scales()$scales) {
8781
if (sc$is_discrete() && !identical(sc$guide, "none")) {
@@ -713,7 +707,7 @@ geom2trace <- function(data, params, p) {
713707

714708
#' @export
715709
geom2trace.GeomBlank <- function(data, params, p) {
716-
list(visible = FALSE)
710+
list(visible = FALSE, showlegend = FALSE)
717711
}
718712

719713
#' @export
@@ -877,11 +871,7 @@ geom2trace.GeomBoxplot <- function(data, params, p) {
877871
# marker styling must inherit from GeomPoint$default_aes
878872
# https://github.com/hadley/ggplot2/blob/ab42c2ca81458b0cf78e3ba47ed5db21f4d0fc30/NEWS#L73-L7
879873
point_defaults <- GeomPoint$use_defaults(NULL)
880-
881-
# Determine if outliers should be hidden
882-
# Hide outliers if: outliers = FALSE, or outlier.shape = NA (via outlier_gp$shape)
883-
hide_outliers <- isFALSE(params$outliers) ||
884-
isTRUE(is.na(params$outlier_gp$shape))
874+
hide_outliers <- isFALSE(params$outliers) || isTRUE(is.na(params$outlier_gp$shape))
885875

886876
compact(list(
887877
x = data[["x"]],
@@ -896,7 +886,6 @@ geom2trace.GeomBoxplot <- function(data, params, p) {
896886
aes2plotly(data, params, "fill"),
897887
aes2plotly(data, params, "alpha")
898888
),
899-
# Control whether outlier points are shown
900889
boxpoints = if (hide_outliers) FALSE,
901890
# markers/points
902891
marker = list(

tests/testthat/test-ggplot-blank.R

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,31 @@ test_that("geom_blank", {
33
skip_if_not_installed("ggplot2", "3.4.0")
44
qp <- expect_warning(qplot(), "deprecated")
55
l <- ggplotly(qp)$x
6-
6+
77
expect_length(l$data, 1)
88
expect_false(l$data[[1]]$visible)
9-
9+
1010
l <- ggplotly(ggplot())$x
11-
11+
1212
expect_length(l$data, 1)
1313
expect_false(l$data[[1]]$visible)
14-
14+
15+
})
16+
17+
test_that("geom_blank does not drop legend (#2281)", {
18+
# When geom_blank() is combined with other geoms, legend should still appear
19+
p <- ggplot(iris, aes(Sepal.Length, Sepal.Width, col = Species)) +
20+
geom_blank() +
21+
geom_point()
22+
23+
L <- plotly_build(ggplotly(p))$x
24+
25+
# geom_point should create 3 visible traces with legend
26+
visible_traces <- Filter(function(d) !isFALSE(d$visible) && d$type == "scatter", L$data)
27+
expect_equal(length(visible_traces), 3)
28+
expect_true(any(sapply(visible_traces, function(d) isTRUE(d$showlegend))))
29+
30+
# Trace names should be species names
31+
trace_names <- sapply(visible_traces, function(d) d$name)
32+
expect_true(all(c("setosa", "versicolor", "virginica") %in% trace_names))
1533
})

0 commit comments

Comments
 (0)