Skip to content

Commit 39643eb

Browse files
committed
fix traces subdivision (fixes plotly#418)
- convert the list of traces into 'plotly_traces' class that also has 'data' attribute holding unmodified data - make trace the 'plotly_trace' object that also have an 'indices' property referencing rows of the original data - replace `tracify()` by `subdivide_traces()` that correctly subdivides the current set of traces based on the provided property and maintains the internals
1 parent b79e9d2 commit 39643eb

File tree

1 file changed

+97
-53
lines changed

1 file changed

+97
-53
lines changed

R/plotly.R

+97-53
Original file line numberDiff line numberDiff line change
@@ -254,45 +254,56 @@ plotly_build <- function(l = last_plot()) {
254254
# this is ugly, but I think it is necessary, since we don't know how many
255255
# traces we have until we evaluate args and call traceify() (or similar)
256256
x <- list()
257-
for (i in seq_along(l$data)) {
258-
d <- l$data[[i]]
257+
x$data <- unlist(lapply(l$data, function(d) {
259258
if (should_eval(d)) {
260-
dat <- do_eval(d)
259+
data <- do_eval(d)
260+
# put everything into a single trace
261+
trace <- structure(data, class="plotly_trace")
262+
trace$indices <- seq_along(data[["x"]]) # indices of the original data elements used in the trace FIXME properly define data length
261263
# start processing specially named arguments
262-
s <- dat[["size"]]
264+
s <- trace[["size"]]
263265
if (!is.null(s)) {
264266
if (!is.numeric(s)) warning("size should be numeric", call. = FALSE)
265267
# if autosizing is used, guess that the plot is 300 by 600
266-
auto <- dat[["layout"]][["autosize"]] %||% TRUE
268+
auto <- trace[["layout"]][["autosize"]] %||% TRUE
267269
hw <- if (auto) c(300, 600)
268-
else c(dat[["layout"]][["height"]], dat[["layout"]][["width"]])
270+
else c(trace[["layout"]][["height"]], trace[["layout"]][["width"]])
269271
# ensure that markers cover 30% of the plot area
270272
m <- list(
271273
size = 0.3 * prod(hw) * (s/sum(s)),
272274
sizemode = "area"
273275
)
274276
# the marker object is the only type of object which respects size
275-
dat[["marker"]] <- modifyList(dat[["marker"]] %||% list(), m)
277+
trace[["marker"]] <- modifyList(trace[["marker"]] %||% list(), m)
276278
# either add some appropriate hover text
277279
txt <- paste0(as.list(d$args)[["size"]], " (size): ", s)
278-
dat[["text"]] <- if (is.null(dat[["text"]])) txt else paste0(dat[["text"]], "<br>", txt)
280+
trace[["text"]] <- if (is.null(trace[["text"]])) txt else paste0(trace[["text"]], "<br>", txt)
279281
}
280-
has_color <- !is.null(dat[["color"]]) ||
281-
isTRUE(!is.null(dat[["z"]]) && !dat[["type"]] %in% "scatter3d")
282-
has_symbol <- !is.null(dat[["symbol"]])
283-
has_group <- !is.null(dat[["group"]])
282+
has_color <- !is.null(trace[["color"]]) ||
283+
isTRUE(!is.null(trace[["z"]]) && !trace[["type"]] %in% "scatter3d")
284+
has_symbol <- !is.null(trace[["symbol"]])
285+
has_group <- !is.null(trace[["group"]])
286+
# put the whole dat into a single trace first
287+
traces <- structure(list(trace),
288+
aes = as.character(), # aesthetic parameters that are used so far to define trace
289+
data = data,
290+
class="plotly_traces")
284291
if (has_color) {
285292
title <- as.list(d$args)[["color"]] %||% as.list(d$args)[["z"]] %||% ""
286-
x$data <- c(x$data, colorize(dat, title))
293+
traces <- colorize(traces, title)
287294
}
288295
# TODO: add a legend title (is this only possible via annotations?!?)
289-
if (has_symbol) x$data <- c(x$data, symbolize(dat))
290-
if (has_group) x$data <- c(x$data, traceify(dat, "group"))
291-
if (!has_color && !has_symbol && !has_group) x$data <- c(x$data, list(dat))
296+
if (has_symbol) traces <- symbolize(traces)
297+
if (has_group) traces <- subdivide_traces(traces, "group")
292298
} else {
293-
x$data <- c(x$data, list(d))
299+
# d should be plotly_trace already
300+
traces <- structure(list(d),
301+
aes = as.character(), # aesthetic parameters that are used so far to define trace
302+
data = d,
303+
class="plotly_traces")
294304
}
295-
}
305+
traces
306+
}), recursive=FALSE)
296307
# it's possible have nested layouts (e.g., plot_ly() %>% layout() %>% layout())
297308
nms <- names(l$layout)
298309
idx <- nms %in% "layout"
@@ -333,9 +344,11 @@ plotly_build <- function(l = last_plot()) {
333344
}
334345

335346
# returns a _list of traces_.
336-
colorize <- function(dat, title = "") {
347+
colorize <- function(traces, title = "") {
348+
dat <- attr(traces, "data")
337349
cols <- dat[["color"]] %||% dat[["z"]]
338350
if (is.numeric(cols)) {
351+
# FIXME needs to be updated for plotly_traces
339352
# by default, use viridis::viridis(10) -> http://rud.is/b/2015/07/20/using-the-new-viridis-colormap-in-r-thanks-to-simon-garnier/
340353
colors <- dat[["colors"]] %||% viridis::viridis(10)
341354
cols <- as.vector(cols)
@@ -359,57 +372,88 @@ colorize <- function(dat, title = "") {
359372
dat <- c(dat, col_list)
360373
}
361374
dat <- list(dat)
375+
lapply(traces, function(x) { x$color <- NULL; x$colors <- NULL; x })
376+
traces
362377
} else { # discrete color scale
363378
lvls <- unique(cols)
364379
N <- length(lvls)
365380
default <- if (is.ordered(cols)) viridis::viridis(N)
366381
else RColorBrewer::brewer.pal(N, "Set2")
367382
colors <- dat[["colors"]] %||% default
368383
colz <- scales::col_factor(colors, levels = lvls, na.color = "transparent")(lvls)
369-
dat <- traceify(dat, "color")
370-
dat <- Map(function(x, y) { x[["marker"]] <- c(x[["marker"]], list(color = y)); x },
371-
dat, colz)
384+
traces <- subdivide_traces(traces, "color", function(sub_trace, lvl) {
385+
sub_trace$marker <- c(sub_trace$marker, list(color = colz[lvl]))
386+
sub_trace$color <- NULL
387+
sub_trace$colors <- NULL
388+
sub_trace
389+
})
372390
}
373-
dat <- lapply(dat, function(x) { x$color <- NULL; x$colors <- NULL; x })
374-
dat
391+
traces
375392
}
376393

377-
symbolize <- function(dat) {
378-
# symbols really only make sense when markers are in the mode, right?
379-
dat$mode <- dat$mode %||% "markers"
380-
dat <- traceify(dat, "symbol")
381-
dat <- lapply(dat, function(x) { x$symbol <- NULL; x })
382-
N <- length(dat)
394+
symbolize <- function(traces) {
395+
dat <- attr(traces, "data")
396+
symb_data <- dat[["symbol"]]
397+
N <- length(unique(symb_data))
383398
if (N > 8) warning("Plotly supports 8 different symbols, but you have ", N, " levels!")
384399
symbols <- c('dot', 'cross', 'diamond', 'square', 'triangle-down', 'triangle-left', 'triangle-right', 'triangle-up')
385400
sym <- dat[[1]][["symbols"]][seq_len(N)] %||% symbols[seq_len(N)]
386-
dat <- Map(function(x, y) { x$marker$symbol <- y; x }, dat, sym)
387-
dat
401+
subdivide_traces(traces, "symbol", function(sub_trace, lvl) {
402+
trace$symbol <- NULL
403+
trace$marker$symbol <- sym[[lvl]]
404+
# symbols really only make sense when markers are in the mode, right?
405+
trace$mode <- dat$mode %||% "markers"
406+
trace
407+
})
388408
}
389409

390-
# break up a single trace into multiple traces according to values stored
391-
# a particular key name
392-
traceify <- function(dat, nm = "group") {
393-
x <- dat[[nm]]
410+
# break up each trace in a list into a smaller traces according to the values of
411+
# a given property and apply FUN to each resulting subtrace
412+
# FUN <- function(sub_trace, level_index) is a function taking the new sub-trace and
413+
# the corresponding index of the property value as its input and returning
414+
# the modified sub-trace
415+
subdivide_traces <- function(traces, prop="group", FUN=function(trace, lvl) trace, ...){
416+
x <- attr(traces, "data")[[prop]]
394417
if (is.null(x)) {
395-
return(list(dat))
396-
} else {
397-
# the order of lvls determines the order in which traces are drawn
398-
# for ordered factors at least, it makes sense to draw the highest level first
399-
# since that _should_ be the darkest color in a sequential pallette
400-
lvls <- if (is.factor(x)) rev(levels(x)) else unique(x)
401-
n <- length(x)
402-
# recursively search for a non-list of appropriate length (if it is, subset it)
403-
recurse <- function(z, n, idx) {
404-
if (is.list(z)) lapply(z, recurse, n, idx) else if (length(z) == n) z[idx] else z
405-
}
406-
new_dat <- list()
407-
for (j in seq_along(lvls)) {
408-
new_dat[[j]] <- lapply(dat, function(y) recurse(y, n, x %in% lvls[j]))
409-
new_dat[[j]]$name <- lvls[j]
410-
}
411-
return(new_dat)
418+
return(traces) # property not found, no traces change
412419
}
420+
421+
# the order of lvls determines the order in which traces are drawn
422+
# for ordered factors at least, it makes sense to draw the highest level first
423+
# since that _should_ be the darkest color in a sequential pallette
424+
lvls <- if (is.factor(x)) rev(levels(x)) else unique(x)
425+
n <- length(x)
426+
427+
new_traces <- unlist(lapply(seq_along(lvls), function(lvl_ix) {
428+
lvl <- lvls[[lvl_ix]]
429+
indices <- which(x %in% lvl)
430+
431+
lvl_subtraces <- lapply(traces, function(trace){
432+
mask <- trace$indices %in% indices
433+
if (sum(mask) == 0) return(NULL) # empty trace
434+
435+
trace_size <- length(mask)
436+
# subset the properties that are subsettable
437+
for (i in 1:length(trace)) {
438+
# FIXME better check for subsettable property
439+
prop <- trace[[i]]
440+
if ((is.vector(prop) || is.factor(prop)) && (length(prop) == trace_size)) {
441+
trace[[i]] <- prop[mask]
442+
}
443+
}
444+
if ("name" %in% names(trace)) {
445+
# append lvl to an existing name
446+
trace$name <- paste0(trace$name, '/', lvl)
447+
} else {
448+
trace$name <- lvl
449+
}
450+
FUN(trace, lvl_ix, ...) # customize subtrace
451+
})
452+
}), recursive=FALSE)
453+
new_traces <- new_traces[!sapply(new_traces, is.null)] # remove NULL subtraces
454+
attributes(new_traces) <- attributes(traces)
455+
attr(new_traces, "aes") <- c(attr(traces, "aes"), prop) # log that given aesthetic was processed
456+
new_traces
413457
}
414458

415459
axis_titles <- function(x, l) {

0 commit comments

Comments
 (0)