Skip to content

Commit 563d120

Browse files
committed
sensible default tooltips, implement geom_violin, legend titles no longer appear in entries
1 parent bde1158 commit 563d120

15 files changed

+270
-195
lines changed

R/ggplotly.R

Lines changed: 135 additions & 71 deletions
Large diffs are not rendered by default.

R/layers2traces.R

Lines changed: 71 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -17,86 +17,61 @@ layers2traces <- function(data, prestats_data, layers, layout, scales, labels) {
1717
# This may involve preprocessing the data, for example:
1818
# 1. geom_line() is really geom_path() with data sorted by x
1919
# 2. geom_smooth() is really geom_path() + geom_ribbon()
20-
#
21-
# This has to be done in a loop, since some layers are really two layers,
22-
# (and we need to replicate the data/params in those cases)
2320
datz <- list()
2421
paramz <- list()
25-
keyz <- list()
2622
for (i in seq_along(data)) {
23+
# This has to be done in a loop, since some layers are really two layers,
24+
# (and we need to replicate the data/params in those cases)
2725
d <- to_basic(data[[i]], prestats_data[[i]], layout, params[[i]])
2826
if (is.data.frame(d)) d <- list(d)
2927
for (j in seq_along(d)) {
3028
datz <- c(datz, d[j])
31-
paramz <- c(paramz, params[i])
32-
# When splitting layers into multiple traces, we need the domain/range of
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]])
40-
psd <- prestats_data[[i]]
41-
key <- unique(psd[names(psd) %in% split_by])
42-
# this order (should) determine the ordering of traces (within layer)
43-
key <- key[do.call(order, key), , drop = FALSE]
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])
48-
}
49-
keyz <- c(keyz, list(key))
29+
paramz <- c(paramz, params[j])
5030
}
5131
}
5232

5333
# now to the actual layer -> trace conversion
5434
trace.list <- list()
5535
for (i in seq_along(datz)) {
5636
d <- datz[[i]]
57-
# create a factor to split the data on...
58-
# by matching the factor levels with the order of the domain (of _discrete_
59-
# scales), the trace ordering should be correct
60-
key <- keyz[[i]]
61-
split_by <- names(key)[!grepl("_domain$", names(key))]
37+
# always split on discrete scales, and other geom specific aesthetics that
38+
# can't translate to a single trace
39+
split_by <- c(split_on(d), names(discreteScales))
40+
# always split on PANEL and domain values (for trace ordering)
41+
split_by <- c("PANEL", paste0(split_by, "_plotlyDomain"))
42+
# split "this layers" data into a list of data frames
43+
idx <- names(d) %in% split_by
44+
# ensure the factor level orders (which determies traces order)
45+
# matches the order of the domain values
46+
lvls <- unique(d[idx])
47+
lvls <- lvls[do.call(order, lvls), , drop = FALSE]
6248
fac <- factor(
63-
apply(d[split_by], 1, paste, collapse = "."),
64-
levels = apply(key[split_by], 1, paste, collapse = ".")
49+
apply(d[idx], 1, paste, collapse = "@%&"),
50+
levels = apply(lvls, 1, paste, collapse = "@%&")
6551
)
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-
)
76-
}
7752
dl <- split(d, fac, drop = TRUE)
7853
# list of traces for this layer
7954
trs <- Map(geom2trace, dl, paramz[i])
80-
# set name/legendgroup/showlegend, if appropriate
81-
legendVars <- setdiff(split_by, "PANEL")
82-
if (!splitContinuous && length(legendVars) > 0 && length(trs) > 1) {
83-
# labels is a list of legend titles, but since we're restricted to
84-
# one (merged) legend, I think it only makes since to prefix the variable
85-
# name in the legend entries
86-
lab <- labels[legendVars]
87-
vals <- key[paste0(legendVars, "_domain")]
88-
valz <- Map(function(x, y) {
89-
if (nchar(x) > 0) paste0(x, ": ", y) else y
90-
}, lab, vals)
91-
entries <- Reduce(function(x, y) {
92-
if (identical(x, y)) x else paste0(x, "<br>", y)
93-
}, valz)
94-
for (k in seq_along(trs)) {
95-
trs[[k]]$name <- entries[[k]]
96-
trs[[k]]$legendgroup <- entries[[k]]
97-
# depending on the geom (e.g. smooth) this may be FALSE already
98-
if (is.null(trs[[k]]$showlegend)) trs[[k]]$showlegend <- TRUE
55+
# are we splitting by a discrete scale on this layer?
56+
# if so, set name/legendgroup/showlegend
57+
isDiscrete <- names(d) %in% paste0(names(discreteScales), "_plotlyDomain")
58+
if (length(trs) > 1 && sum(isDiscrete) >= 1) {
59+
nms <- names(trs)
60+
# ignore "non-discrete" scales that we've split on
61+
for (w in seq_len(sum(names(d) %in% c("PANEL", split_on(d))))) {
62+
nms <- sub("^[^@%&]@%&", "", nms)
9963
}
64+
nms <- strsplit(nms, "@%&")
65+
nms <- vapply(nms, function(x) {
66+
if (length(x) > 1) paste0("(", paste0(x, collapse = ","), ")") else x
67+
}, character(1))
68+
trs <- Map(function(x, y) {
69+
x$name <- y
70+
x$legendgroup <- y
71+
# depending on the geom (e.g. smooth) this may be FALSE already
72+
x$showlegend <- x$showlegend %||% TRUE
73+
x
74+
}, trs, nms)
10075
} else {
10176
trs <- lapply(trs, function(x) { x$showlegend <- FALSE; x })
10277
}
@@ -111,10 +86,8 @@ layers2traces <- function(data, prestats_data, layers, layout, scales, labels) {
11186
if (inherits(d, "GeomBar") && paramz[[i]]$position == "identity") {
11287
trs <- rev(trs)
11388
}
114-
11589
trace.list <- c(trace.list, trs)
11690
}
117-
11891
trace.list
11992
}
12093

@@ -138,24 +111,29 @@ to_basic <- function(data, prestats_data, layout, params, ...) {
138111

139112
#' @export
140113
to_basic.GeomViolin <- function(data, prestats_data, layout, params, ...) {
141-
# TODO: it should be possible to implement this via GeomPolygon
142-
# just need preprocess the data, then:
143-
# replace_class(data, "GeomPolygon", "GeomViolin")
144-
warning(
145-
"plotly.js does not yet support violin plots. \n",
146-
"Converting to boxplot instead.",
147-
call. = FALSE
114+
n <- nrow(data)
115+
revData <- data[order(data$y, decreasing = TRUE), ]
116+
idx <- !names(data) %in% c("x", "xmin", "xmax")
117+
data <- rbind(
118+
cbind(x = data$x - data$violinwidth / 2, data[, idx]),
119+
cbind(x = revData$x + revData$violinwidth / 2, revData[, idx])
148120
)
149-
to_basic.GeomBoxplot(data, prestats_data)
121+
if (!is.null(data$hovertext)) data$hovertext <- paste0(data$hovertext, "<br>")
122+
data$hovertext <- paste0(data$hovertext, "density: ", round(data$density, 3))
123+
prefix_class(data, c("GeomPolygon", "GeomViolin"))
150124
}
151125

152126
#' @export
153127
to_basic.GeomBoxplot <- function(data, prestats_data, layout, params, ...) {
154-
# 'trained' aesthetics that we're interested in mapping from data to prestats
155-
aez <- c("fill", "colour", "size", "alpha", "linetype", "shape", "x")
156-
dat <- data[names(data) %in% c(aez, "group")]
157-
pre <- prestats_data[!names(prestats_data) %in% aez]
158-
prefix_class(merge(pre, dat, by = "group", sort = FALSE), "GeomBoxplot")
128+
aez <- names(GeomBoxplot$default_aes)
129+
for (i in aez) {
130+
prestats_data[[i]] <- NULL
131+
}
132+
vars <- c("PANEL", "group", aez, grep("_plotlyDomain$", names(data), value = T))
133+
prefix_class(
134+
merge(prestats_data, data[vars], by = c("PANEL", "group"), sort = FALSE),
135+
"GeomBoxplot"
136+
)
159137
}
160138

161139
#' @export
@@ -224,7 +202,6 @@ to_basic.GeomRect <- function(data, prestats_data, layout, params, ...) {
224202

225203
#' @export
226204
to_basic.GeomRaster <- function(data, prestats_data, layout, params, ...) {
227-
# TODO: what if nrow(data) != nrow(prestats_data)?
228205
data$z <- prestats_data$fill
229206
if (is.discrete(prestats_data$fill)) {
230207
data <- prefix_class(data, "GeomRect")
@@ -259,8 +236,10 @@ to_basic.GeomDensity2d <- function(data, prestats_data, layout, params, ...) {
259236

260237
#' @export
261238
to_basic.GeomAbline <- function(data, prestats_data, layout, params, ...) {
262-
data <- unique(data[c("PANEL", "intercept", "slope", "group")])
263-
data$group <- seq_len(nrow(data))
239+
# ugh, we can't trust the group here
240+
data$group <- interaction(
241+
data[!grepl("group", names(data)) & !vapply(data, anyNA, logical(1))]
242+
)
264243
lay <- tidyr::gather_(layout, "variable", "x", c("x_min", "x_max"))
265244
data <- merge(lay[c("PANEL", "x")], data, by = "PANEL")
266245
data$y <- with(data, intercept + slope * x)
@@ -269,8 +248,10 @@ to_basic.GeomAbline <- function(data, prestats_data, layout, params, ...) {
269248

270249
#' @export
271250
to_basic.GeomHline <- function(data, prestats_data, layout, params, ...) {
272-
data <- unique(data[c("PANEL", "yintercept", "group")])
273-
data$group <- seq_len(nrow(data))
251+
# ugh, we can't trust the group here
252+
data$group <- interaction(
253+
data[!grepl("group", names(data)) & !vapply(data, anyNA, logical(1))]
254+
)
274255
lay <- tidyr::gather_(layout, "variable", "x", c("x_min", "x_max"))
275256
data <- merge(lay[c("PANEL", "x")], data, by = "PANEL")
276257
data$y <- data$yintercept
@@ -279,8 +260,10 @@ to_basic.GeomHline <- function(data, prestats_data, layout, params, ...) {
279260

280261
#' @export
281262
to_basic.GeomVline <- function(data, prestats_data, layout, params, ...) {
282-
data <- unique(data[c("PANEL", "xintercept", "group")])
283-
data$group <- seq_len(nrow(data))
263+
# ugh, we can't trust the group here
264+
data$group <- interaction(
265+
data[!grepl("group", names(data)) & !vapply(data, anyNA, logical(1))]
266+
)
284267
lay <- tidyr::gather_(layout, "variable", "y", c("y_min", "y_max"))
285268
data <- merge(lay[c("PANEL", "y")], data, by = "PANEL")
286269
data$x <- data$xintercept
@@ -357,7 +340,8 @@ geom2trace.GeomPath <- function(data, params) {
357340
L <- list(
358341
x = data$x,
359342
y = data$y,
360-
text = data$text,
343+
text = data$hovertext,
344+
hoverinfo = "text",
361345
type = "scatter",
362346
mode = "lines",
363347
name = if (inherits(data, "GeomSmooth")) "fitted values",
@@ -378,13 +362,10 @@ geom2trace.GeomPath <- function(data, params) {
378362
#' @export
379363
geom2trace.GeomPoint <- function(data, params) {
380364
shape <- aes2plotly(data, params, "shape")
381-
if (length(unique(data$size)) > 1 && is.null(data$text)) {
382-
data$text <- paste("size:", data$size)
383-
}
384365
L <- list(
385366
x = data$x,
386367
y = data$y,
387-
text = data$text,
368+
text = data$hovertext,
388369
key = data$key,
389370
type = "scatter",
390371
mode = "markers",
@@ -416,7 +397,7 @@ geom2trace.GeomBar <- function(data, params) {
416397
list(
417398
x = data$x,
418399
y = data$y,
419-
text = data$text,
400+
text = data$hovertext,
420401
type = "bar",
421402
marker = list(
422403
autocolorscale = FALSE,
@@ -442,7 +423,7 @@ geom2trace.GeomPolygon <- function(data, params) {
442423
L <- list(
443424
x = data$x,
444425
y = data$y,
445-
text = data$text %||% data$level,
426+
text = data$hovertext %||% data$level,
446427
type = "scatter",
447428
mode = "lines",
448429
line = list(
@@ -471,6 +452,7 @@ geom2trace.GeomBoxplot <- function(data, params) {
471452
x = data$x,
472453
y = data$y,
473454
type = "box",
455+
hoverinfo = "y",
474456
fillcolor = toRGB(
475457
aes2plotly(data, params, "fill"),
476458
aes2plotly(data, params, "alpha")

R/utils.R

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ is.plotly <- function(x) inherits(x, "plotly")
1212
x %||% y
1313
}
1414

15+
strextract <- function(str, pattern) {
16+
regmatches(str, regexpr(pattern, str))
17+
}
18+
1519
compact <- function(x) {
1620
Filter(Negate(is.null), x)
1721
}

man/bbox.Rd

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/gg2list.Rd

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/ggplotly.Rd

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat.R

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ if (report_diffs || build_table) {
1313
Rserve::Rserve(args = "--vanilla --RS-enable-remote")
1414
conn <- RSconnect()
1515
# master version should _always_ depend on the CRAN version of ggplot2
16-
RSeval(conn, "library(methods); options(repos = c(CRAN = 'https://cran.rstudio.com/'))")
17-
RSeval(conn, "install.packages('ggplot2')")
1816
RSeval(conn, "devtools::install_github('ropensci/plotly')")
1917
RSeval(conn, "library(plotly)")
2018
if (report_diffs) {

tests/testthat/test-cookbook-lines.R

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ bp.err <-
9898
linetype = "dashed")
9999

100100
test_that("The error bars get plotted over one another", {
101-
info <- expect_traces(bp.err, 3, "bar-dodge-color-error")
101+
info <- expect_traces(bp.err, 4, "bar-dodge-color-error")
102102
})
103103

104104
df <- read.table(header = TRUE, text = "
@@ -117,11 +117,7 @@ bp.err4 <- bp +
117117
linetype = "dashed", position = position_dodge())
118118

119119
test_that("4 error bars", {
120-
info <- expect_traces(bp.err4, 3, "bar-dodge-color-err4")
121-
tr <- info$data[[3]]
122-
expect_equal(length(tr$y), 4)
123-
expect_equal(length(unique(tr$y)), 4)
124-
expect_equal(length(tr$x), 4)
120+
info <- expect_traces(bp.err4, 4, "bar-dodge-color-err4")
125121
})
126122

127123
df <- read.table(header = T, text = "

tests/testthat/test-ggplot-abline.R

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,5 @@ test_that("abline aesthetics", {
4343
L <- expect_traces(p, 1, "multiple-abline")
4444
expect_identical(range(L$layout$xaxis$tickvals), c(-5, 5))
4545
expect_identical(range(L$layout$yaxis$tickvals), c(-5, 5))
46-
expect_identical(L$data[[1]]$y[1:2], df$m[1] * L$data[[1]]$x[1:2] + df$b[1])
4746
})
4847

tests/testthat/test-ggplot-boxplot.R

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,6 @@ test_that("geom_boxplot gives a boxplot", {
2525
expect_identical(L$data[[1]]$type, "box")
2626
})
2727

28-
test_that("geom_violin is equated to geom_boxplot for now", {
29-
gg <- ggplot(mtcars, aes(factor(cyl), mpg)) + geom_violin()
30-
31-
L <- save_outputs(gg, "violin")
32-
33-
# right nb. traces
34-
expect_equal(length(L$data), 1)
35-
# right type for 1st trace
36-
expect_identical(L$data[[1]]$type, "box")
37-
})
38-
3928
test_that("you can make a boxplot for a distribution of datetimes", {
4029
dist <- c(10, 20, 33, 40, 11, 12, 11)
4130
dist <- as.POSIXct(paste0("2014-09-19 10:00:", dist))

0 commit comments

Comments
 (0)