Skip to content

Commit 271b271

Browse files
committed
Merge pull request #487 from ropensci/fix/hoverinfo
Better hoverinfo
2 parents bde1158 + bab8da4 commit 271b271

18 files changed

+328
-213
lines changed

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Package: plotly
22
Title: Create Interactive Web Graphics via Plotly's JavaScript Graphing Library
3-
Version: 3.1.0
3+
Version: 3.2.0
44
Authors@R: c(person("Carson", "Sievert", role = c("aut", "cre"),
55
email = "cpsievert1@gmail.com"),
66
person("Chris", "Parmer", role = c("aut", "cph"),

NEWS

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
3.2.0 -- 8 Mar 2015
2+
3+
CHANGES:
4+
5+
* Legend titles no longer appear in legend entries.
6+
* Tooltips now reflect aesthetic mappings. This makes it easier to decode
7+
data values from a given visual marking.
8+
9+
NEW FEATURES:
10+
11+
* geom_violin() is now supported.
12+
* ggplotly() gains a mapping argument to control the set of aesthetics to appears in the tooltip as well as their order.
13+
114
3.1.0 -- 8 Mar 2015
215

316
CHANGES:

R/ggplotly.R

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

R/layers2traces.R

Lines changed: 89 additions & 106 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,20 +202,16 @@ 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)?
228-
data$z <- prestats_data$fill
229-
if (is.discrete(prestats_data$fill)) {
230-
data <- prefix_class(data, "GeomRect")
231-
to_basic(data, prestats_data, layout, params)
232-
} else {
233-
prefix_class(data, "GeomTile")
234-
}
205+
data <- prefix_class(data, "GeomTile")
206+
to_basic(data, prestats_data, layout, params)
235207
}
236208

237209
#' @export
238210
to_basic.GeomTile <- function(data, prestats_data, layout, params, ...) {
239-
data$z <- prestats_data$fill
240-
if (is.discrete(prestats_data$fill)) {
211+
# geom2trace.GeomTile is a heatmap, which requires continuous fill and
212+
# a complete grid
213+
g <- expand.grid(unique(data$x), unique(data$y))
214+
if (nrow(g) != nrow(data) || is.discrete(prestats_data$fill)) {
241215
data <- prefix_class(data, "GeomRect")
242216
to_basic(data, prestats_data, layout, params)
243217
} else {
@@ -259,8 +233,10 @@ to_basic.GeomDensity2d <- function(data, prestats_data, layout, params, ...) {
259233

260234
#' @export
261235
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))
236+
# ugh, we can't trust the group here
237+
data$group <- interaction(
238+
data[!grepl("group", names(data)) & !vapply(data, anyNA, logical(1))]
239+
)
264240
lay <- tidyr::gather_(layout, "variable", "x", c("x_min", "x_max"))
265241
data <- merge(lay[c("PANEL", "x")], data, by = "PANEL")
266242
data$y <- with(data, intercept + slope * x)
@@ -269,8 +245,10 @@ to_basic.GeomAbline <- function(data, prestats_data, layout, params, ...) {
269245

270246
#' @export
271247
to_basic.GeomHline <- function(data, prestats_data, layout, params, ...) {
272-
data <- unique(data[c("PANEL", "yintercept", "group")])
273-
data$group <- seq_len(nrow(data))
248+
# ugh, we can't trust the group here
249+
data$group <- interaction(
250+
data[!grepl("group", names(data)) & !vapply(data, anyNA, logical(1))]
251+
)
274252
lay <- tidyr::gather_(layout, "variable", "x", c("x_min", "x_max"))
275253
data <- merge(lay[c("PANEL", "x")], data, by = "PANEL")
276254
data$y <- data$yintercept
@@ -279,8 +257,10 @@ to_basic.GeomHline <- function(data, prestats_data, layout, params, ...) {
279257

280258
#' @export
281259
to_basic.GeomVline <- function(data, prestats_data, layout, params, ...) {
282-
data <- unique(data[c("PANEL", "xintercept", "group")])
283-
data$group <- seq_len(nrow(data))
260+
# ugh, we can't trust the group here
261+
data$group <- interaction(
262+
data[!grepl("group", names(data)) & !vapply(data, anyNA, logical(1))]
263+
)
284264
lay <- tidyr::gather_(layout, "variable", "y", c("y_min", "y_max"))
285265
data <- merge(lay[c("PANEL", "y")], data, by = "PANEL")
286266
data$x <- data$xintercept
@@ -357,7 +337,8 @@ geom2trace.GeomPath <- function(data, params) {
357337
L <- list(
358338
x = data$x,
359339
y = data$y,
360-
text = data$text,
340+
text = data$hovertext,
341+
hoverinfo = "text",
361342
type = "scatter",
362343
mode = "lines",
363344
name = if (inherits(data, "GeomSmooth")) "fitted values",
@@ -378,13 +359,10 @@ geom2trace.GeomPath <- function(data, params) {
378359
#' @export
379360
geom2trace.GeomPoint <- function(data, params) {
380361
shape <- aes2plotly(data, params, "shape")
381-
if (length(unique(data$size)) > 1 && is.null(data$text)) {
382-
data$text <- paste("size:", data$size)
383-
}
384362
L <- list(
385363
x = data$x,
386364
y = data$y,
387-
text = data$text,
365+
text = data$hovertext,
388366
key = data$key,
389367
type = "scatter",
390368
mode = "markers",
@@ -416,7 +394,7 @@ geom2trace.GeomBar <- function(data, params) {
416394
list(
417395
x = data$x,
418396
y = data$y,
419-
text = data$text,
397+
text = data$hovertext,
420398
type = "bar",
421399
marker = list(
422400
autocolorscale = FALSE,
@@ -435,14 +413,10 @@ geom2trace.GeomBar <- function(data, params) {
435413
#' @export
436414
geom2trace.GeomPolygon <- function(data, params) {
437415
data <- group2NA(data)
438-
# TODO: do this for more density-like measures??
439-
if ("level" %in% names(data)) {
440-
data$level <- paste("Level:", data$level)
441-
}
442416
L <- list(
443417
x = data$x,
444418
y = data$y,
445-
text = data$text %||% data$level,
419+
text = data$hovertext,
446420
type = "scatter",
447421
mode = "lines",
448422
line = list(
@@ -471,6 +445,7 @@ geom2trace.GeomBoxplot <- function(data, params) {
471445
x = data$x,
472446
y = data$y,
473447
type = "box",
448+
hoverinfo = "y",
474449
fillcolor = toRGB(
475450
aes2plotly(data, params, "fill"),
476451
aes2plotly(data, params, "alpha")
@@ -519,16 +494,24 @@ geom2trace.GeomTile <- function(data, params) {
519494
data <- data[order(data$x, order(data$y, decreasing = T)), ]
520495
x <- sort(unique(data$x))
521496
y <- sort(unique(data$y))
497+
fill <- data$fill_plotlyDomain
522498
colorscale <- cbind(
523499
c(0, 1),
524-
data[c(which.min(data$z), which.max(data$z)), "fill"]
500+
data[c(which.min(fill), which.max(fill)), "fill"]
525501
)
526502
list(
527503
x = x,
528504
y = y,
529-
text = matrix(data$z, nrow = length(y), ncol = length(x)),
530-
hoverinfo = "text",
531-
z = matrix(scales::rescale(data$z), nrow = length(y), ncol = length(x)),
505+
z = matrix(
506+
scales::rescale(fill),
507+
nrow = length(y),
508+
ncol = length(x)
509+
),
510+
text = matrix(
511+
data$hovertext,
512+
nrow = length(y),
513+
ncol = length(x)
514+
),
532515
colorscale = colorscale,
533516
type = "heatmap",
534517
showscale = FALSE,

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.

0 commit comments

Comments
 (0)