Skip to content

Commit 95bbdb5

Browse files
committed
more careful/correct legend creation and trace merging
1 parent b91cdc2 commit 95bbdb5

7 files changed

+112
-94
lines changed

R/ggplotly.R

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,6 @@ gg2list <- function(p, width = NULL, height = NULL, source = "A") {
411411

412412
# justification of legend boxes
413413
theme$legend.box.just <- theme$legend.box.just %||% c("center", "center")
414-
415414
# scales -> data for guides
416415
gdefs <- ggfun("guides_train")(scales, theme, p$guides, p$labels)
417416
if (length(gdefs) > 0) {
@@ -492,7 +491,7 @@ gg2list <- function(p, width = NULL, height = NULL, source = "A") {
492491
}
493492

494493
# try to merge marker/line traces that have the same values for these props
495-
props <- c("x", "y", "text", "type", "xaxis", "yaxis")
494+
props <- c("x", "y", "text", "type", "xaxis", "yaxis", "name")
496495
hashes <- vapply(traces, function(x) digest::digest(x[names(x) %in% props]), character(1))
497496
modes <- vapply(traces, function(x) x$mode %||% "", character(1))
498497
nhashes <- length(unique(hashes))
@@ -515,8 +514,10 @@ gg2list <- function(p, width = NULL, height = NULL, source = "A") {
515514
for (i in ax) {
516515
gglayout[[i]]$hoverformat <- ".2f"
517516
}
517+
# If a trace isn't named, it shouldn't have additional hoverinfo
518+
traces <- lapply(compact(traces), function(x) { x$name <- x$name %||% ""; x })
518519

519-
l <- list(data = compact(setNames(traces, NULL)), layout = compact(gglayout))
520+
l <- list(data = setNames(traces, NULL), layout = compact(gglayout))
520521
# ensure properties are boxed correctly
521522
l <- add_boxed(rm_asis(l))
522523
l$width <- width
@@ -739,8 +740,10 @@ gdef2trace <- function(gdef, theme, gglayout) {
739740
gdef$bar$value <- scales::rescale(gdef$bar$value, from = rng)
740741
gdef$key$.value <- scales::rescale(gdef$key$.value, from = rng)
741742
list(
742-
x = gglayout$xaxis$tickvals,
743-
y = gglayout$yaxis$tickvals,
743+
x = gglayout$xaxis$range,
744+
y = gglayout$yaxis$range,
745+
# esentially to prevent this getting merged at a later point
746+
name = gdef$hash,
744747
type = "scatter",
745748
mode = "markers",
746749
opacity = 0,

R/layers2traces.R

Lines changed: 93 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,21 @@ layers2traces <- function(data, prestats_data, layers, layout, scales, labels) {
3030
datz <- c(datz, d[j])
3131
paramz <- c(paramz, params[i])
3232
# When splitting layers into multiple traces, we need the domain/range of
33-
# the scale (for trace naming & legend generation)
33+
# the scale (for trace naming & legend generation).
34+
# if the splitting variables are constant in the data, we don't want to
35+
# split on them
36+
idx <- vapply(d[[j]], function(x) length(unique(x)) > 1, logical(1))
37+
# always split on PANEL, discrete scales, and other geom specific aes that
38+
# don't translate to a single trace
39+
split_by <- c("PANEL", names(discreteScales)[names(discreteScales) %in% names(idx)[idx]])
3440
psd <- prestats_data[[i]]
35-
idx <- names(psd) %in% c(names(discreteScales), "PANEL")
36-
key <- unique(psd[, idx, drop = FALSE])
41+
key <- unique(psd[names(psd) %in% split_by])
3742
# this order (should) determine the ordering of traces (within layer)
3843
key <- key[do.call(order, key), , drop = FALSE]
39-
nms <- names(key)
40-
idx <- nms %in% names(discreteScales)
41-
nms[idx] <- paste0(nms[idx], "_domain")
42-
key <- setNames(key, nms)
43-
for (k in setdiff(nms[idx], "PANEL")) {
44-
scaleName <- sub("_domain", "", k)
45-
key[[scaleName]] <- scales$get_scales(scaleName)$map(key[, k])
44+
split_vars <- setdiff(names(key), "PANEL")
45+
for (k in split_vars) {
46+
key[[paste0(k, "_domain")]] <- key[, k]
47+
key[[k]] <- scales$get_scales(k)$map(key[, k])
4648
}
4749
keyz <- c(keyz, list(key))
4850
}
@@ -52,62 +54,65 @@ layers2traces <- function(data, prestats_data, layers, layout, scales, labels) {
5254
trace.list <- list()
5355
for (i in seq_along(datz)) {
5456
d <- datz[[i]]
55-
# what aesthetics do we split on for this geom?
56-
split_by <- split_on(class(d)[1])
57-
# always split on PANEL
58-
idx <- names(d) %in% c(split_by, "PANEL")
59-
# if the variable is constant, don't split on it
60-
idx <- idx & vapply(d, function(x) length(unique(x)) > 1, logical(1))
6157
# create a factor to split the data on...
6258
# by matching the factor levels with the order of the domain (of _discrete_
6359
# scales), the trace ordering should be correct
6460
key <- keyz[[i]]
65-
idx2 <- names(key) %in% names(d[idx])
66-
fac <- if (sum(idx) >= 1) {
67-
if (sum(idx) == sum(idx2)) {
68-
factor(
69-
apply(d[idx], 1, paste, collapse = "."),
70-
levels = apply(key[names(d[idx])], 1, paste, collapse = ".")
71-
)
72-
} else {
73-
d[idx]
74-
}
75-
} else {
76-
1
61+
split_by <- names(key)[!grepl("_domain$", names(key))]
62+
fac <- factor(
63+
apply(d[split_by], 1, paste, collapse = "."),
64+
levels = apply(key[split_by], 1, paste, collapse = ".")
65+
)
66+
# if we split on a variable not in the key, we have no chance
67+
# of generating an appropriate legend
68+
splitContinuous <- length(setdiff(split_on(d), split_by)) > 0
69+
if (splitContinuous) {
70+
split_by <- c(split_by, split_on(d))
71+
splitDat <- d[names(d) %in% split_by]
72+
fac <- factor(
73+
apply(splitDat, 1, paste, collapse = "."),
74+
levels = apply(unique(splitDat), 1, paste, collapse = ".")
75+
)
7776
}
7877
dl <- split(d, fac, drop = TRUE)
7978
# list of traces for this layer
8079
trs <- Map(geom2trace, dl, paramz[i])
81-
# each trace is with respect to which axis?
82-
for (j in seq_along(trs)) {
83-
panel <- unique(dl[[j]]$PANEL)
84-
trs[[j]]$xaxis <- sub("axis", "", layout[panel, "xaxis"])
85-
trs[[j]]$yaxis <- sub("axis", "", layout[panel, "yaxis"])
86-
}
87-
# generate name/legendgroup/showlegend, if appropriate
88-
if (length(trs) > 1 && any(names(key) %in% names(discreteScales))) {
80+
# set name/legendgroup/showlegend, if appropriate
81+
legendVars <- setdiff(split_by, "PANEL")
82+
if (!splitContinuous && length(legendVars) > 0 && length(trs) > 1) {
8983
# labels is a list of legend titles, but since we're restricted to
9084
# one (merged) legend, I think it only makes since to prefix the variable
9185
# name in the legend entries
92-
lab <- labels[names(discreteScales)]
93-
idx <- names(key) %in% names(discreteScales)
94-
vals <- key[paste0(names(key[idx]), "_domain")]
86+
lab <- labels[legendVars]
87+
vals <- key[paste0(legendVars, "_domain")]
9588
valz <- Map(function(x, y) { paste0(x, ": ", y) }, lab, vals)
96-
entries <- Reduce(function(x, y) paste0(x, "<br>", y), valz)
97-
# also need to set `layout.legend.traceorder='reversed'` (YUCK!!!)
98-
if (inherits(d, "GeomBar") && paramz[[i]]$position == "identity") {
99-
trs <- rev(trs)
100-
}
89+
entries <- Reduce(function(x, y) {
90+
if (identical(x, y)) x else paste0(x, "<br>", y)
91+
}, valz)
10192
for (k in seq_along(trs)) {
10293
trs[[k]]$name <- entries[[k]]
10394
trs[[k]]$legendgroup <- entries[[k]]
104-
trs[[k]]$showlegend <- TRUE
95+
# depending on the geom (e.g. smooth) this may be FALSE already
96+
if (is.null(trs[[k]]$showlegend)) trs[[k]]$showlegend <- TRUE
10597
}
10698
} else {
10799
trs <- lapply(trs, function(x) { x$showlegend <- FALSE; x })
108100
}
101+
102+
# each trace is with respect to which axis?
103+
for (j in seq_along(trs)) {
104+
panel <- unique(dl[[j]]$PANEL)
105+
trs[[j]]$xaxis <- sub("axis", "", layout[panel, "xaxis"])
106+
trs[[j]]$yaxis <- sub("axis", "", layout[panel, "yaxis"])
107+
}
108+
# also need to set `layout.legend.traceorder='reversed'`
109+
if (inherits(d, "GeomBar") && paramz[[i]]$position == "identity") {
110+
trs <- rev(trs)
111+
}
112+
109113
trace.list <- c(trace.list, trs)
110114
}
115+
111116
trace.list
112117
}
113118

@@ -156,7 +161,7 @@ to_basic.GeomSmooth <- function(data, prestats_data, layout, params, ...) {
156161
dat <- prefix_class(data, "GeomPath")
157162
dat$alpha <- NULL
158163
if (!identical(params$se, FALSE)) {
159-
dat2 <- prefix_class(ribbon_dat(data), "GeomPolygon")
164+
dat2 <- prefix_class(ribbon_dat(data), c("GeomPolygon", "GeomSmooth"))
160165
dat2$colour <- NULL
161166
dat <- list(dat, dat2)
162167
}
@@ -282,10 +287,6 @@ to_basic.GeomVline <- function(data, prestats_data, layout, params, ...) {
282287

283288
#' @export
284289
to_basic.GeomJitter <- function(data, prestats_data, layout, params, ...) {
285-
if ("size" %in% names(data)) {
286-
params$sizemin <- min(prestats_data$globsizemin)
287-
params$sizemax <- max(prestats_data$globsizemax)
288-
}
289290
prefix_class(data, "GeomPoint")
290291
}
291292

@@ -295,9 +296,20 @@ to_basic.GeomErrorbar <- function(data, prestats_data, layout, params, ...) {
295296
# (plotly.js wants half, in pixels)
296297
data <- merge(data, layout, by = "PANEL", sort = FALSE)
297298
data$width <- (data$xmax - data$x) /(data$x_max - data$x_min)
299+
data$fill <- NULL
298300
prefix_class(data, "GeomErrorbar")
299301
}
300302

303+
#' @export
304+
to_basic.GeomErrorbarh <- function(data, prestats_data, layout, params, ...) {
305+
# height for ggplot2 means size of the entire bar, on the data scale
306+
# (plotly.js wants half, in pixels)
307+
data <- merge(data, layout, by = "PANEL", sort = FALSE)
308+
data$width <- (data$ymax - data$y) / (data$y_max - data$y_min)
309+
data$fill <- NULL
310+
prefix_class(data, "GeomErrorbarh")
311+
}
312+
301313
#' @export
302314
to_basic.GeomLinerange <- function(data, prestats_data, layout, params, ...) {
303315
data$width <- 0
@@ -313,15 +325,6 @@ to_basic.GeomPointrange <- function(data, prestats_data, layout, params, ...) {
313325
)
314326
}
315327

316-
#' @export
317-
to_basic.GeomErrorbarh <- function(data, prestats_data, layout, params, ...) {
318-
# height for ggplot2 means size of the entire bar, on the data scale
319-
# (plotly.js wants half, in pixels)
320-
data <- merge(data, layout, by = "PANEL", sort = FALSE)
321-
data$width <- (data$ymax - data$y) / (data$y_max - data$y_min)
322-
prefix_class(data, "GeomErrorbarh")
323-
}
324-
325328
#' @export
326329
to_basic.default <- function(data, prestats_data, layout, params, ...) {
327330
data
@@ -396,7 +399,9 @@ geom2trace.GeomPoint <- function(data, params) {
396399
)
397400
# fill is irrelevant for pch %in% c(1, 15:20)
398401
pch <- uniq(data$shape) %||% params$shape %||% GeomPoint$default_aes$shape
399-
L$marker$color[pch %in% c(1, 15:20)] <- L$marker$line$color[pch %in% c(1, 15:20)]
402+
if (any(pch %in% c(1, 15:20))) {
403+
L$marker$color <- L$marker$line$color
404+
}
400405
L
401406
}
402407

@@ -443,13 +448,12 @@ geom2trace.GeomPolygon <- function(data, params) {
443448
if ("level" %in% names(data)) {
444449
data$level <- paste("Level:", data$level)
445450
}
446-
list(
451+
L <- list(
447452
x = data$x,
448453
y = data$y,
449454
text = data$text %||% data$level,
450455
type = "scatter",
451456
mode = "lines",
452-
name = if (inherits(data, "GeomSmooth")) "standard error",
453457
line = list(
454458
# NOTE: line attributes must be constant on a polygon
455459
width = aes2plotly(data, params, "size"),
@@ -462,6 +466,11 @@ geom2trace.GeomPolygon <- function(data, params) {
462466
aes2plotly(data, params, "alpha")
463467
)
464468
)
469+
if (inherits(data, "GeomSmooth")) {
470+
L$name <- "standard error"
471+
L$showlegend <- FALSE
472+
}
473+
L
465474

466475
}
467476

@@ -554,7 +563,7 @@ geom2trace.default <- function(data, params) {
554563
" Please open an issue with your example code at\n",
555564
" https://github.com/ropensci/plotly/issues"
556565
)
557-
NULL
566+
list()
558567
}
559568

560569
# ---------------------------------------------------------------------------
@@ -605,24 +614,30 @@ group2NA <- function(data) {
605614
data
606615
}
607616

608-
609-
split_on <- function(geom = "GeomPoint") {
610-
# NOTE: Do we also want to split on size?
611-
# Legends based on sizes not implemented yet in Plotly
617+
# given a geom, should we split on any continuous variables?
618+
# this is necessary for some geoms, for example, polygons
619+
# since plotly.js can't draw two polygons with different fill in a single trace
620+
split_on <- function(dat) {
621+
geom <- class(dat)[1]
612622
lookup <- list(
613-
GeomPoint = c("colour", "fill", "shape"),
614-
GeomPath = c("linetype", "size", "colour", "shape"),
615-
GeomPolygon = c("colour", "fill", "linetype", "size"),
616-
GeomBar = c("colour", "fill"),
617-
GeomDensity = c("colour", "fill", "linetype"),
623+
GeomPath = c("fill", "colour", "size"),
624+
GeomPolygon = c("fill", "colour", "size"),
625+
GeomBar = "fill",
618626
GeomBoxplot = c("colour", "fill", "size"),
619-
GeomErrorbar = c("colour", "linetype"),
620-
GeomErrorbarh = c("colour", "linetype"),
621-
GeomArea = c("colour", "fill"),
622-
GeomStep = c("linetype", "size", "colour"),
623-
GeomText = c("colour")
627+
GeomErrorbar = "colour",
628+
GeomErrorbarh = "colour",
629+
GeomText = "colour"
624630
)
625-
lookup[[geom]]
631+
splits <- lookup[[geom]]
632+
# make sure the variable is in the data, and is non-constant
633+
splits <- splits[splits %in% names(dat)]
634+
# is there more than one unique value for this aes split in the data?
635+
for (i in splits) {
636+
if (length(unique(dat[, i])) < 2) {
637+
splits <- setdiff(splits, i)
638+
}
639+
}
640+
splits
626641
}
627642

628643
# make trace with errorbars

R/plotly.R

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ style <- function(p = last_plot(), ..., traces = 1, evaluate = FALSE) {
232232
#' @param l a ggplot object, or a plotly object, or a list.
233233
#' @export
234234
plotly_build <- function(l = last_plot()) {
235+
#if (inherits(l, "ggmatrix"))
235236
# ggplot objects don't need any special type of handling
236237
if (ggplot2::is.ggplot(l)) return(gg2list(l))
237238
l <- get_plot(l)

tests/testthat/test-cookbook-lines.R

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,8 @@ temp <- spf + geom_hline(aes(yintercept=10))
189189
test_that("geom_hline -> 2 more traces", {
190190
info <- expect_traces(temp, 4, "scatter-facet-hline")
191191
expect_true(info$layout$showlegend)
192-
has.name <- sapply(info$data, function(tr)is.character(tr$name))
193-
named.traces <- info$data[has.name]
194-
expect_equal(length(named.traces), 2)
192+
has.name <- sapply(info$data, function(tr) nchar(tr$name) > 0)
193+
expect_equal(sum(has.name), 2)
195194
})
196195

197196
df.vlines <- data.frame(cond = levels(df$cond), xval = c(10,11.5))

tests/testthat/test-ggplot-density2d.R

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ test_that("StatDensity2d with GeomPolygon translates to filled path(s)", {
3434
)
3535
# the other traces should be the colorbar and polygons
3636
notPoints <- L$data[!legends]
37-
polygons <- notPoints[grepl("^#", names(notPoints))]
38-
colorbar <- notPoints[!grepl("^#", names(notPoints))][[1]]
37+
polygons <- notPoints[-length(notPoints)]
38+
colorbar <- notPoints[[length(notPoints)]]
3939
expect_identical(
4040
unique(unlist(lapply(polygons, "[[", "type"))), "scatter"
4141
)

tests/testthat/test-ggplot-segment.R

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ test_that("with non-numeric data, we can have more than one segment", {
4040

4141
fig <- save_outputs(gg, "segment-multiple-non-numeric")
4242
# one trace is for the colorbar
43-
expect_equal(length(fig$data), 5)
44-
expect_equal(fig$data[[3]]$x[1], seg1$x)
45-
expect_equal(fig$data[[3]]$x[2], seg1$xend)
46-
expect_equal(fig$data[[4]]$x[1], seg2$x)
47-
expect_equal(fig$data[[4]]$x[2], seg2$xend)
43+
expect_equal(length(fig$data), 4)
44+
expect_equal(fig$data[[2]]$x[1], seg1$x)
45+
expect_equal(fig$data[[2]]$x[2], seg1$xend)
46+
expect_equal(fig$data[[3]]$x[1], seg2$x)
47+
expect_equal(fig$data[[3]]$x[2], seg2$xend)
4848
})

tests/testthat/test-ggplot-smooth.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ test_that("geom_smooth() respects colour aesthetic", {
5252
p7 <- qplot(carat, price, data = d) + geom_smooth(aes(fill = cut))
5353

5454
test_that("geom_smooth() respects fill aesthetic", {
55-
info <- expect_traces(p7, 7, "fill2")
55+
info <- expect_traces(p7, 11, "fill2")
5656
})
5757

5858
# ensure legend is drawn when needed

0 commit comments

Comments
 (0)