-
Notifications
You must be signed in to change notification settings - Fork 633
/
Copy pathplotly.R
484 lines (472 loc) · 19.5 KB
/
plotly.R
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
#' Initiate a plotly visualization
#'
#' Transform data into a plotly visualization.
#'
#' There are a number of "visual properties" that aren't included in the officical
#' Reference section (see below).
#'
#' @param data A data frame (optional).
#' @param ... These arguments are documented at \url{https://plot.ly/r/reference/}
#' Note that acceptable arguments depend on the value of \code{type}.
#' @param type A character string describing the type of trace.
#' @param group Either a variable name or a vector to use for grouping. If used,
#' a different trace will be created for each unique value.
#' @param color Either a variable name or a vector to use for color mapping.
#' @param colors Either a colorbrewer2.org palette name (e.g. "YlOrRd" or "Blues"),
#' or a vector of colors to interpolate in hexadecimal "#RRGGBB" format,
#' or a color interpolation function like \code{colorRamp()}.
#' @param symbol Either a variable name or a (discrete) vector to use for symbol encoding.
#' @param symbols A character vector of symbol types. Possible values:
#' 'dot', 'cross', 'diamond', 'square', 'triangle-down', 'triangle-left', 'triangle-right', 'triangle-up'
#' @param size A variable name or numeric vector to encode the size of markers.
#' @param width Width in pixels (optional, defaults to automatic sizing).
#' @param height Height in pixels (optional, defaults to automatic sizing).
#' @param inherit logical. Should future traces inherit properties from this initial trace?
#' @param evaluate logical. Evaluate arguments when this function is called?
#' @seealso \code{\link{layout}()}, \code{\link{add_trace}()}, \code{\link{style}()}
#' @author Carson Sievert
#' @export
#' @examples
#' \dontrun{
#' data(economics, package = "ggplot2")
#' # basic time-series plot
#' p <- plot_ly(economics, x = date, y = uempmed, type = "scatter",
#' showlegend = FALSE)
#' # add a loess smoother
#' p2 <- add_trace(p, y = fitted(loess(uempmed ~ as.numeric(date))))
#' # add a title
#' p3 <- layout(p2, title = "Median duration of unemployment (in weeks)")
#' # change the font
#' layout(p3, font = list(family = "Courier New, monospace"))
#'
#' # using the color argument
#' plot_ly(economics, x = date, y = unemploy / pop, color = pop, mode = "markers")
#' plot_ly(economics, x = date, y = unemploy / pop, color = pop,
#' colors = terrain.colors(5), mode = "markers")
#'
#' # function to extract the decade of a given date
#' decade <- function(x) {
#' factor(floor(as.numeric(format(x, "%Y")) / 10) * 10)
#' }
#' plot_ly(economics, x = unemploy / pop, color = decade(date), type = "box")
#'
#' # plotly loves pipelines
#' economics %>%
#' transform(rate = unemploy / pop) %>%
#' plot_ly(x = date, y = rate) %>%
#' loess(rate ~ as.numeric(date), data = .) %>%
#' broom::augment() %>%
#' add_trace(y = .fitted)
#'
#' # sometimes, a data frame isn't fit for the use case...
#' # for 3D surface plots, a numeric matrix is more natural
#' plot_ly(z = volcano, type = "surface")
#' }
#'
plot_ly <- function(data = data.frame(), ..., type = "scatter",
group, color, colors, symbol, symbols, size,
width = NULL, height = NULL, inherit = FALSE,
evaluate = FALSE) {
# "native" plotly arguments
argz <- substitute(list(...))
# old arguments to this function that are no longer supported
if (!is.null(argz$filename))
warning("Ignoring filename. Use plotly_POST() if you want to post figures to plotly.")
if (!is.null(argz$fileopt))
warning("Ignoring fileopt. Use plotly_POST() if you want to post figures to plotly.")
if (!is.null(argz$world_readable))
warning("Ignoring world_readable. Use plotly_POST() if you want to post figures to plotly.")
# tack on "special" arguments
if (!missing(group)) argz$group <- substitute(group)
if (!missing(color)) argz$color <- substitute(color)
if (!missing(colors)) argz$colors <- substitute(colors)
if (!missing(symbol)) argz$symbol <- substitute(symbol)
if (!missing(symbols)) argz$symbols <- substitute(symbols)
if (!missing(size)) argz$size <- substitute(size)
# trace information
tr <- list(
type = type,
args = argz,
env = list2env(data), # environment in which to evaluate arguments
enclos = parent.frame(), # if objects aren't found in env, look here
inherit = inherit
)
# plotly objects should always have a _list_ of trace(s)
p <- list(
data = list(tr),
layout = NULL,
url = NULL,
width = width,
height = height,
base_url = get_domain()
)
if (evaluate) p <- plotly_build(p)
hash_plot(data, p)
}
#' Add a trace to a plotly visualization
#'
#' @param p A plotly object.
#' @param ... These arguments are documented in the references section below.
#' Note that acceptable arguments depend on the trace type.
#' @param group Either a variable name or a vector to use for grouping. If used,
#' a different trace will be created for each unique value.
#' @param color Either a variable name or a vector to use for color mapping.
#' @param colors Either a colorbrewer2.org palette name (e.g. "YlOrRd" or "Blues"),
#' or a vector of colors to interpolate in hexadecimal "#RRGGBB" format,
#' or a color interpolation function like \code{colorRamp}.
#' @param symbol Either a variable name or a (discrete) vector to use for symbol encoding.
#' @param symbols A character vector of symbol types. Possible values:
#' 'dot', 'cross', 'diamond', 'square', 'triangle-down', 'triangle-left', 'triangle-right', 'triangle-up'
#' @param size A variable name or numeric vector to encode the size of markers.
#' @param data A data frame to associate with this trace (optional). If not
#' provided, arguments are evaluated using the data frame in \code{\link{plot_ly}()}.
#' @param evaluate logical. Evaluate arguments when this function is called?
#' @seealso \code{\link{plot_ly}()}
#' @references \url{https://plot.ly/r/reference/}
#' @author Carson Sievert
#' @export
add_trace <- function(p = last_plot(), ...,
group, color, colors, symbol, symbols, size,
data = NULL, evaluate = FALSE) {
# "native" plotly arguments
argz <- substitute(list(...))
# tack on "special" arguments
if (!missing(group)) argz$group <- substitute(group)
if (!missing(color)) argz$color <- substitute(color)
if (!missing(colors)) argz$colors <- substitute(colors)
if (!missing(symbol)) argz$symbol <- substitute(symbol)
if (!missing(symbols)) argz$symbols <- substitute(symbols)
if (!missing(size)) argz$size <- substitute(size)
data <- data %||% if (is.data.frame(p)) p else list()
tr <- list(
args = argz,
env = list2env(data),
enclos = parent.frame()
)
p <- last_plot(p)
p$data <- c(p$data, list(tr))
if (evaluate) p <- plotly_build(p)
hash_plot(data, p)
}
#' Add and/or modify layout of a plotly
#'
#' @param p A plotly object.
#' @param ... Arguments to the layout object. For documentation,
#' see \url{https://plot.ly/r/reference/#Layout_and_layout_style_objects}
#' @param data A data frame to associate with this layout (optional). If not
#' provided, arguments are evaluated using the data frame in \code{\link{plot_ly}()}.
#' @param evaluate logical. Evaluate arguments when this function is called?
#' @author Carson Sievert
#' @export
layout <- function(p = last_plot(), ...,
data = NULL, evaluate = FALSE) {
data <- data %||% if (is.data.frame(p)) p else list()
layout <- list(
args = substitute(list(...)),
env = list2env(data),
enclos = parent.frame()
)
p <- last_plot(p)
p$layout <- c(p$layout, list(layout = layout))
if (evaluate) p <- plotly_build(p)
hash_plot(data, p)
}
#' Set the default configuration for plotly
#'
#' @param p a plotly object
#' @param staticPlot for export or image generation
#' @param workspace we're in the workspace, so need toolbar etc (TODO describe functionality instead)?
#' @param editable edit titles, move annotations, etc
#' @param autosizable respect layout.autosize=true and infer its container size?
#' @param fillFrame if we DO autosize, do we fill the container or the screen?
#' @param scrollZoom mousewheel or two-finger scroll zooms the plot
#' @param doubleClick double click interaction (false, 'reset', 'autosize' or 'reset+autosize')
#' @param showTips see some hints about interactivity
#' @param showLink link to open this plot in plotly
#' @param sendData if we show a link, does it contain data or just link to a plotly file?
#' @param linkText text appearing in the sendData link
#' @param displayModeBar display the modebar (T, F, or 'hover')
#' @param displaylogo add the plotly logo on the end of the modebar
#' @param plot3dPixelRatio increase the pixel ratio for 3D plot images
#' @param mathjax If \code{FALSE}, don't attempt to include mathjax.
#' If \code{'cdn'}, mathjax will be loaded externally (meaning an internet
#' connection is needed to view the graph). If \code{'local'}, a local copy
#' of mathjax will be included. For this to work, you'll need a local copy of
#' plotly.js and set the environment variable plotly_jsdir to its location on
#' your machine.
#' @author Carson Sievert
#' @export
# TODO: use htmlwidgets::JS() to specify setBackground function?
# https://github.com/ropensci/plotly/issues/284#issue-108153160
config <- function(p = last_plot(), staticPlot = F, workspace = F, editable = F,
autosizable = F, fillFrame = F, scrollZoom = F,
doubleClick = 'reset+autosize', showTips = F, showLink = T,
sendData = T, linkText = 'Edit chart', displayModeBar = 'hover',
displaylogo = T, plot3dPixelRatio = 2, mathjax = FALSE) {
conf <- list(
staticPlot = staticPlot,
workspace = workspace,
editable = editable,
autosizable = autosizable,
fillFrame = fillFrame,
scrollZoom = scrollZoom,
doubleClick = doubleClick,
showTips = showTips,
showLink = showLink,
sendData = sendData,
linkText = linkText,
displayModeBar = displayModeBar,
displaylogo = displaylogo,
plot3dPixelRatio = plot3dPixelRatio,
mathjax = mathjax
)
p <- last_plot(p)
p$config <- c(p$config, conf)
hash_plot(if (is.data.frame(p)) p else list(), p)
}
#' Modify trace(s)
#'
#' Modify trace(s) of an existing plotly visualization. Useful when used in
#' conjunction with \code{\link{get_figure}()}.
#'
#' @param p A plotly visualization.
#' @param ... Visual properties.
#' @param traces numeric vector. Which traces should be modified?
#' @param evaluate logical. Evaluate arguments when this function is called?
#' @seealso \code{\link{get_figure}()}
#' @author Carson Sievert
#' @export
style <- function(p = last_plot(), ..., traces = 1, evaluate = FALSE) {
idx <- traces >= length(p$data)
if (any(idx)) warning("You've referenced non-existent traces", call. = FALSE)
style <- list(
args = substitute(list(...)),
# not optimal....
env = p$data[[max(traces)]]$env,
enclos = parent.frame(),
traces = traces
)
p$style <- c(p$style, list(style))
if (evaluate) p <- plotly_build(p)
hash_plot(data, p)
}
#' Build a plotly object before viewing it
#'
#' For convenience and efficiency purposes, plotly objects are subject to lazy
#' evaluation. That is, the actual content behind a plotly object is not
#' created until it is absolutely necessary. In some instances, you may want
#' to perform this evaluation yourself, and work directly with the resulting
#' list.
#'
#' @param l a ggplot object, or a plotly object, or a list.
#' @importFrom viridis viridis
#' @export
plotly_build <- function(l = last_plot()) {
# ggplot objects don't need any special type of handling
if (is.ggplot(l)) return(gg2list(l))
l <- get_plot(l)
# assume unnamed list elements are data/traces
nms <- names(l)
idx <- nms %in% ""
l <- if (is.null(nms)) {
list(data = l)
} else if (any(idx)) {
c(data = c(l$data, l[idx]), l[!idx])
} else l
# carry over properties, if necessary (but don't carry over evaluation envir)
if (length(l$data) > 1 && isTRUE(l$data[[1]]$inherit)) {
d <- l$data[[1]]
d <- d[!names(d) %in% c("env", "enclos")]
for (i in seq.int(2, length(l$data))) {
l$data[[i]] <- modifyList(l$data[[i]], d)
}
}
# 'x' is the same as 'l', but with arguments evaluated
# this is ugly, but I think it is necessary, since we don't know how many
# traces we have until we evaluate args and call traceify() (or similar)
x <- list()
for (i in seq_along(l$data)) {
d <- l$data[[i]]
if (should_eval(d)) {
dat <- do_eval(d)
# start processing specially named arguments
s <- dat[["size"]]
if (!is.null(s)) {
if (!is.numeric(s)) warning("size should be numeric", call. = FALSE)
# if autosizing is used, guess that the plot is 300 by 600
auto <- dat[["layout"]][["autosize"]] %||% TRUE
hw <- if (auto) c(300, 600)
else c(dat[["layout"]][["height"]], dat[["layout"]][["width"]])
# ensure that markers cover 30% of the plot area
m <- list(
size = 0.3 * prod(hw) * (s/sum(s)),
sizemode = "area"
)
# the marker object is the only type of object which respects size
dat[["marker"]] <- modifyList(dat[["marker"]] %||% list(), m)
# either add some appropriate hover text
txt <- paste0(as.list(d$args)[["size"]], " (size): ", s)
dat[["text"]] <- if (is.null(dat[["text"]])) txt else paste0(dat[["text"]], "<br>", txt)
}
has_color <- !is.null(dat[["color"]]) ||
isTRUE(!is.null(dat[["z"]]) && !dat[["type"]] %in% "scatter3d")
has_symbol <- !is.null(dat[["symbol"]])
has_group <- !is.null(dat[["group"]])
if (has_color) {
title <- as.list(d$args)[["color"]] %||% as.list(d$args)[["z"]] %||% ""
x$data <- c(x$data, colorize(dat, title))
}
# TODO: add a legend title (is this only possible via annotations?!?)
if (has_symbol) x$data <- c(x$data, symbolize(dat))
if (has_group) x$data <- c(x$data, traceify(dat, "group"))
if (!has_color && !has_symbol && !has_group) x$data <- c(x$data, list(dat))
} else {
x$data <- c(x$data, list(d))
}
}
# it's possible have nested layouts (e.g., plot_ly() %>% layout() %>% layout())
nms <- names(l$layout)
idx <- nms %in% "layout"
l$layout <- c(list(l$layout[!idx]), setNames(l$layout[idx], NULL))
for (i in seq_along(l$layout)) {
x$layout[[i]] <- perform_eval(l$layout[[i]])
}
x$layout <- Reduce(modifyList, x$layout)
# if style is not null, use it to modify existing traces
if (!is.null(l$style)) {
for (i in seq_along(l$style)) {
sty <- perform_eval(l$style[[i]])
for (k in l$style[[i]]$traces)
x$data[[k]] <- modifyList(x$data[[k]], sty)
}
}
# add appropriate axis title (if they don't already exist)
x <- axis_titles(x, l)
# tack on other keyword arguments, if necessary
idx <- !names(l) %in% c("data", "layout")
if (any(idx)) x <- c(x, l[idx])
x <- add_boxed(x)
# ugh, annotations _must_ be an _array_ of object(s)...
a <- x$layout$annotations
if (!is.null(a) && !is.null(names(a))) {
x$layout$annotations <- list(x$layout$annotations)
}
# traces shouldn't have any names
x$data <- setNames(x$data, NULL)
# if this is a non-line scatter trace and no hovermode exists,
# set hovermode to closest
if (is.null(x$data[[1]]$type) || isTRUE(x$data[[1]]$type == "scatter")) {
if (!grepl("lines", x$data[[1]]$mode %||% "lines"))
x$layout$hovermode <- x$layout$hovermode %||% "closest"
}
# add plotly class mainly for printing method
structure(x, class = unique("plotly", class(x)))
}
# returns a _list of traces_.
colorize <- function(dat, title = "") {
cols <- dat[["color"]] %||% dat[["z"]]
if (is.numeric(cols)) {
# by default, use viridis::viridis(10) -> http://rud.is/b/2015/07/20/using-the-new-viridis-colormap-in-r-thanks-to-simon-garnier/
colors <- dat[["colors"]] %||% viridis::viridis(10)
cols <- as.vector(cols)
rng <- range(cols, na.rm = TRUE)
x <- seq(min(rng), max(rng), length.out = 10)
colz <- scales::col_numeric(colors, rng, na.color = "transparent")(x)
df <- if (length(cols) > 1) data.frame(scales::rescale(x), colz)
else data.frame(c(0, 1), rep(colz, 2))
col_list <- list(
colorbar = list(title = as.character(title)),
colorscale = setNames(df, NULL)
)
# scatter-like traces can have both line and marker objects
if (grepl("scatter", dat[["type"]] %||% "scatter")) {
col_list$color <- cols
dat[["marker"]] <- modifyList(col_list, dat[["marker"]] %||% list())
#mode <- dat[["mode"]] %||% "markers+lines"
# can't have a colorscale for both markers and lines???
#dat[["line"]] <- modifyList(col_list, dat[["line"]] %||% list())
} else {
dat <- c(dat, col_list)
}
dat <- list(dat)
} else { # discrete color scale
lvls <- unique(cols)
N <- length(lvls)
default <- if (is.ordered(cols)) viridis::viridis(N)
else RColorBrewer::brewer.pal(N, "Set2")
colors <- dat[["colors"]] %||% default
colz <- scales::col_factor(colors, levels = lvls, na.color = "transparent")(lvls)
dat <- traceify(dat, "color")
dat <- Map(function(x, y) { x[["marker"]] <- c(x[["marker"]], list(color = y)); x },
dat, colz)
}
dat <- lapply(dat, function(x) { x$color <- NULL; x$colors <- NULL; x })
dat
}
symbolize <- function(dat) {
# symbols really only make sense when markers are in the mode, right?
dat$mode <- dat$mode %||% "markers"
dat <- traceify(dat, "symbol")
dat <- lapply(dat, function(x) { x$symbol <- NULL; x })
N <- length(dat)
if (N > 8) warning("Plotly supports 8 different symbols, but you have ", N, " levels!")
symbols <- c('dot', 'cross', 'diamond', 'square', 'triangle-down', 'triangle-left', 'triangle-right', 'triangle-up')
sym <- dat[[1]][["symbols"]][seq_len(N)] %||% symbols[seq_len(N)]
dat <- Map(function(x, y) { x$marker$symbol <- y; x }, dat, sym)
dat
}
# break up a single trace into multiple traces according to values stored
# a particular key name
traceify <- function(dat, nm = "group") {
x <- dat[[nm]]
if (is.null(x)) {
return(list(dat))
} else {
# the order of lvls determines the order in which traces are drawn
# for ordered factors at least, it makes sense to draw the highest level first
# since that _should_ be the darkest color in a sequential pallette
lvls <- if (is.factor(x)) rev(levels(x)) else unique(x)
n <- length(x)
# recursively search for a non-list of appropriate length (if it is, subset it)
recurse <- function(z, n, idx) {
if (is.list(z)) lapply(z, recurse, n, idx) else if (length(z) == n) z[idx] else z
}
new_dat <- list()
for (j in seq_along(lvls)) {
new_dat[[j]] <- lapply(dat, function(y) recurse(y, n, x %in% lvls[j]))
new_dat[[j]]$name <- lvls[j]
}
return(new_dat)
}
}
axis_titles <- function(x, l) {
d <- l$data[[1]]
argz <- as.list(d$args)
scene <- if (isTRUE(d$type %in% c("scatter3d", "surface"))) TRUE else FALSE
for (i in c("x", "y", "z")) {
ax <- paste0(i, "axis")
t <- x$layout[[ax]]$title %||% x$layout$scene[[ax]]$title
if (is.null(t)) {
idx <- which(names(argz) %in% i)
if (length(idx)) {
title <- if (is.language(argz[[idx]])) deparse(argz[[idx]]) else i
if (scene) x$layout[["scene"]][[ax]]$title <- title
else x$layout[[ax]]$title <- title
}
}
}
x
}
#' Create a complete empty plotly graph.
#'
#' Useful when used with \link{subplot}
#'
#' @export
plotly_empty <- function() {
eaxis <- list(
showticklabels = FALSE,
showgrid = FALSE,
zeroline = FALSE
)
layout(plot_ly(), xaxis = eaxis, yaxis = eaxis)
}