Skip to content

Commit fdc5eb3

Browse files
committed
Merge pull request #368 from ropensci/fix/build
Fix/build
2 parents b41a8db + 8d111f2 commit fdc5eb3

File tree

8 files changed

+83
-64
lines changed

8 files changed

+83
-64
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: 2.0.19
3+
Version: 2.1.0
44
Authors@R: c(person("Carson", "Sievert", role = c("aut", "cre"),
55
email = "[email protected]"),
66
person("Chris", "Parmer", role = c("aut", "cph"),

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
2.1.0 -- 29 Dec 2015
2+
3+
plot_ly() now defaults to inherit=FALSE and plotly_build() is now idempotent. Fixes #280 and #277. See #368 for details.
4+
15
2.0.19 -- 23 Dec 2015
26

37
Added as.widget() function for conveniency in converting plotly object to htmlwidget objects. See #294.

R/plotly.R

Lines changed: 28 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
#'
6666
plot_ly <- function(data = data.frame(), ..., type = "scatter",
6767
group, color, colors, symbol, symbols, size,
68-
width = NULL, height = NULL, inherit = TRUE,
68+
width = NULL, height = NULL, inherit = FALSE,
6969
evaluate = FALSE) {
7070
# "native" plotly arguments
7171
argz <- substitute(list(...))
@@ -170,7 +170,7 @@ layout <- function(p = last_plot(), ...,
170170
enclos = parent.frame()
171171
)
172172
p <- last_plot(p)
173-
p$layout <- c(p$layout, list(layout))
173+
p$layout <- c(p$layout, list(layout = layout))
174174
if (evaluate) p <- plotly_build(p)
175175
hash_plot(data, p)
176176
}
@@ -265,9 +265,6 @@ plotly_build <- function(l = last_plot()) {
265265
# ggplot objects don't need any special type of handling
266266
if (is.ggplot(l)) return(gg2list(l))
267267
l <- get_plot(l)
268-
# plots without NSE don't need it either
269-
nmz <- c(lapply(l$data, names), lapply(l$layout, names), lapply(l$style, names))
270-
if (!all(c("args", "env") %in% unlist(nmz))) return(structure(l, class = unique("plotly", class(l))))
271268
# assume unnamed list elements are data/traces
272269
nms <- names(l)
273270
idx <- nms %in% ""
@@ -276,14 +273,22 @@ plotly_build <- function(l = last_plot()) {
276273
} else if (any(idx)) {
277274
c(data = c(l$data, l[idx]), l[!idx])
278275
} else l
279-
dats <- list()
276+
# carry over properties, if necessary (but don't carry over evaluation envir)
277+
if (length(l$data) > 1 && isTRUE(l$data[[1]]$inherit)) {
278+
d <- l$data[[1]]
279+
d <- d[!names(d) %in% c("env", "enclos")]
280+
for (i in seq.int(2, length(l$data))) {
281+
l$data[[i]] <- modifyList(l$data[[i]], d)
282+
}
283+
}
284+
# 'x' is the same as 'l', but with arguments evaluated
285+
# this is ugly, but I think it is necessary, since we don't know how many
286+
# traces we have until we evaluate args and call traceify() (or similar)
287+
x <- list()
280288
for (i in seq_along(l$data)) {
281289
d <- l$data[[i]]
282-
# if appropriate, evaluate trace arguments in a suitable environment
283-
idx <- names(d) %in% c("args", "env", "enclos")
284-
if (sum(idx) == 3) {
285-
dat <- c(d[!idx], eval(d$args, as.list(d$env, all.names = TRUE), d$enclos))
286-
dat[c("args", "env", "enclos")] <- NULL
290+
if (should_eval(d)) {
291+
dat <- do_eval(d)
287292
# start processing specially named arguments
288293
s <- dat[["size"]]
289294
if (!is.null(s)) {
@@ -309,49 +314,30 @@ plotly_build <- function(l = last_plot()) {
309314
has_group <- !is.null(dat[["group"]])
310315
if (has_color) {
311316
title <- as.list(d$args)[["color"]] %||% as.list(d$args)[["z"]] %||% ""
312-
dats <- c(dats, colorize(dat, title))
317+
x$data <- c(x$data, colorize(dat, title))
313318
}
314319
# TODO: add a legend title (is this only possible via annotations?!?)
315-
if (has_symbol) dats <- c(dats, symbolize(dat))
316-
if (has_group) dats <- c(dats, traceify(dat, "group"))
317-
if (!has_color && !has_symbol && !has_group) dats <- c(dats, list(dat))
320+
if (has_symbol) x$data <- c(x$data, symbolize(dat))
321+
if (has_group) x$data <- c(x$data, traceify(dat, "group"))
322+
if (!has_color && !has_symbol && !has_group) x$data <- c(x$data, list(dat))
318323
} else {
319-
dats <- c(dats, list(d))
320-
}
321-
}
322-
x <- list(data = dats)
323-
# carry over properties/data from first trace (if appropriate)
324-
if (length(x$data) > 1 && isTRUE(l$data[[1]]$inherit)) {
325-
for (i in seq.int(2, length(x$data))) {
326-
x$data[[i]] <- modifyList(x$data[[1]], x$data[[i]])
324+
x$data <- c(x$data, list(d))
327325
}
328326
}
329-
# layout() tacks on an unnamed list element to potentially pre-existing
330-
# layout(s). Note that ggplotly() will return a named list
331-
# of length n >= 1 (so we need to carefully merge them ).
327+
# it's possible have nested layouts (e.g., plot_ly() %>% layout() %>% layout())
332328
nms <- names(l$layout)
333-
if (!is.null(nms) && any(idx <- nms %in% "")) {
334-
# TODO: does this always preserve the correct order to layouts?
335-
# (important since we use modifyList at a later point)
336-
l$layout <- c(list(l$layout[!idx]), l$layout[idx])
337-
}
329+
idx <- nms %in% "layout"
330+
l$layout <- c(list(l$layout[!idx]), setNames(l$layout[idx], NULL))
338331
for (i in seq_along(l$layout)) {
339-
layout <- l$layout[[i]]
340-
idx <- names(layout) %in% c("args", "env", "enclos")
341-
x$layout[[i]] <- if (sum(idx) == 3) {
342-
c(layout[!idx], eval(layout$args, as.list(layout$env, all.names = TRUE), layout$enclos))
343-
} else {
344-
layout
345-
}
332+
x$layout[[i]] <- perform_eval(l$layout[[i]])
346333
}
347334
x$layout <- Reduce(modifyList, x$layout)
348335
# if style is not null, use it to modify existing traces
349336
if (!is.null(l$style)) {
350337
for (i in seq_along(l$style)) {
351-
sty <- l$style[[i]]
352-
idx <- names(sty) %in% c("args", "env", "enclos")
353-
new_sty <- if (sum(idx) == 3) c(sty[!idx], eval(sty$args, as.list(sty$env, all.names = TRUE), sty$enclos)) else sty
354-
for (k in sty$traces) x$data[[k]] <- modifyList(x$data[[k]], new_sty)
338+
sty <- perform_eval(l$style[[i]])
339+
for (k in l$style[[i]]$traces)
340+
x$data[[k]] <- modifyList(x$data[[k]], sty)
355341
}
356342
}
357343
# add appropriate axis title (if they don't already exist)
@@ -365,26 +351,6 @@ plotly_build <- function(l = last_plot()) {
365351
if (!is.null(a) && !is.null(names(a))) {
366352
x$layout$annotations <- list(x$layout$annotations)
367353
}
368-
# search for keyword args in traces and place them at the top level
369-
kwargs <- lapply(x$data, function(z) z[get_kwargs()])
370-
# 'top-level' keywords args take precedence
371-
kwargs <- Reduce(modifyList, c(kwargs, list(x[get_kwargs()])))
372-
# empty keyword arguments can cause problems
373-
kwargs <- kwargs[sapply(kwargs, length) > 0]
374-
# try our damndest to assign a sensible filename
375-
if (is.null(kwargs$filename)) {
376-
kwargs$filename <-
377-
as.character(kwargs$layout$title) %||%
378-
paste(
379-
c(kwargs$layout$xaxis$title,
380-
kwargs$layout$yaxis$title,
381-
kwargs$layout$zaxis$title),
382-
collapse = " vs. "
383-
) %||%
384-
"plot from api"
385-
}
386-
# tack on keyword arguments
387-
x <- c(x, kwargs)
388354
# traces shouldn't have any names
389355
x$data <- setNames(x$data, NULL)
390356
# add plotly class mainly for printing method

R/plotly_POST.R

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,14 @@
2626

2727
plotly_POST <- function(x, filename, fileopt = "new", world_readable = TRUE) {
2828
x <- plotly_build(x)
29-
if (!missing(filename)) x$filename <- filename
29+
x$filename <- if (!missing(filename)) {
30+
filename
31+
} else {
32+
# try our damndest to assign a sensible filename
33+
x$filename %||% as.character(x$layout$title) %||%
34+
paste(c(x$layout$xaxis$title, x$layout$yaxis$title, x$layout$zaxis$title),
35+
collapse = " vs. ") %||% "plot from api"
36+
}
3037
if (!is.null(x$fileopt))
3138
warning("fileopt was specified in the wrong place. Please specify in plotly_POST()")
3239
x$fileopt <- fileopt

R/process.R

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ process.figure <- function(resp) {
2828
fig <- con$payload$figure
2929
fig$url <- sub("apigetfile/", "~", resp$url)
3030
fig <- add_boxed(fig)
31+
fig$data[[1]]$inherit <- FALSE
3132
# any reasonable way to return a data frame?
3233
hash_plot(data.frame(), fig)
3334
}

R/utils.R

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,25 @@ plotly_headers <- function(type = "main") {
190190
httr::add_headers(.headers = h)
191191
}
192192

193+
194+
perform_eval <- function(x) {
195+
if (should_eval(x)) do_eval(x) else x
196+
}
197+
198+
# env/enclos are special properties specific to the R API
199+
# if they appear _and_ are environments, then evaluate arguments
200+
# (sometimes figures return these properties but evaluation doesn't make sense)
201+
should_eval <- function(x) {
202+
any(vapply(x[c("env", "enclos")], is.environment, logical(1)))
203+
}
204+
205+
# perform evaluation of arguments, keeping other list elements
206+
do_eval <- function(x) {
207+
y <- c(x, eval(x$args, as.list(x$env, all.names = TRUE), x$enclos))
208+
y[c("args", "env", "enclos")] <- NULL
209+
y
210+
}
211+
193212
# try to write environment variables to an .Rprofile
194213
cat_profile <- function(key, value, path = "~") {
195214
r_profile <- file.path(normalizePath(path, mustWork = TRUE),

tests/testthat/test-plotly-getfigure.R

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,12 @@ test_that("retrieving a public figure ... works.", {
2828
p <- plotly_build(fig)
2929
expect_equivalent(p$data[[1]]$x, c("1", "2", "3"))
3030
})
31+
32+
test_that("can add traces to a subplot figure", {
33+
skip_on_cran()
34+
fig <- get_figure('chelsea_lyn', 6366)
35+
p <- add_trace(fig, x=c(1, 2, 3), y=c(4, 2, 4))
36+
l <- plotly_build(p)
37+
expect_equivalent(length(fig$data) + 1, length(l$data))
38+
})
39+

tests/testthat/test-plotly.R

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,16 @@ test_that("axis titles get attached to scene object for 3D plots", {
9898
expect_identical(scene$yaxis$title, "Petal.Width")
9999
expect_identical(scene$zaxis$title, "Sepal.Width")
100100
})
101+
102+
test_that("inheriting properties works as expected", {
103+
library(dplyr)
104+
p <- iris %>%
105+
count(Species) %>%
106+
plot_ly(x = Species, y = n, opacity = 0.5, type = "bar", inherit = TRUE) %>%
107+
layout(barmode = "overlay", showlegend = FALSE)
108+
s <- count(iris[sample(nrow(iris), 10), ], Species)
109+
p2 <- add_trace(p, data = s)
110+
l <- plotly_build(p2)
111+
expect_equal(l$data[[2]]$opacity, 0.5)
112+
expect_true(all(l$data[[1]]$y > l$data[[2]]$y))
113+
})

0 commit comments

Comments
 (0)