Skip to content

Commit 015e197

Browse files
committed
fix traces subdivision (fixes plotly#418)
- add 'indices' property to the trace to reference the indices of the data elements associated with the trace - replace `tracify()` by `subdivide_traces()` that correctly subdivides the current set of traces based on the given property
1 parent b79e9d2 commit 015e197

File tree

1 file changed

+100
-66
lines changed

1 file changed

+100
-66
lines changed

R/plotly.R

+100-66
Original file line numberDiff line numberDiff line change
@@ -254,48 +254,54 @@ 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)) {
260259
dat <- do_eval(d)
260+
# put everything into a single trace
261+
trace <- dat
262+
trace[["indices"]] <- seq_along(trace[["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 <- list(trace)
284288
if (has_color) {
285289
title <- as.list(d$args)[["color"]] %||% as.list(d$args)[["z"]] %||% ""
286-
x$data <- c(x$data, colorize(dat, title))
290+
traces <- colorize(traces, dat, title)
287291
}
288292
# 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))
293+
if (has_symbol) traces <- symbolize(traces, dat)
294+
if (has_group) traces <- subdivide_traces(traces, dat, "group")
295+
traces <- lapply(traces, function(trace) {#print(attributes(trace));
296+
trace$indices<-NULL;
297+
trace})
298+
traces
292299
} else {
293-
x$data <- c(x$data, list(d))
300+
list(d)
294301
}
295-
}
302+
}), recursive=FALSE)
296303
# it's possible have nested layouts (e.g., plot_ly() %>% layout() %>% layout())
297-
nms <- names(l$layout)
298-
idx <- nms %in% "layout"
304+
idx <- names(l$layout) %in% "layout"
299305
l$layout <- c(list(l$layout[!idx]), setNames(l$layout[idx], NULL))
300306
for (i in seq_along(l$layout)) {
301307
x$layout[[i]] <- perform_eval(l$layout[[i]])
@@ -333,7 +339,7 @@ plotly_build <- function(l = last_plot()) {
333339
}
334340

335341
# returns a _list of traces_.
336-
colorize <- function(dat, title = "") {
342+
colorize <- function(traces, dat, title = "") {
337343
cols <- dat[["color"]] %||% dat[["z"]]
338344
if (is.numeric(cols)) {
339345
# by default, use viridis::viridis(10) -> http://rud.is/b/2015/07/20/using-the-new-viridis-colormap-in-r-thanks-to-simon-garnier/
@@ -348,68 +354,96 @@ colorize <- function(dat, title = "") {
348354
colorbar = list(title = as.character(title)),
349355
colorscale = setNames(df, NULL)
350356
)
351-
# scatter-like traces can have both line and marker objects
352-
if (grepl("scatter", dat[["type"]] %||% "scatter")) {
353-
col_list$color <- cols
354-
dat[["marker"]] <- modifyList(col_list, dat[["marker"]] %||% list())
355-
#mode <- dat[["mode"]] %||% "markers+lines"
356-
# can't have a colorscale for both markers and lines???
357-
#dat[["line"]] <- modifyList(col_list, dat[["line"]] %||% list())
358-
} else {
359-
dat <- c(dat, col_list)
360-
}
361-
dat <- list(dat)
357+
traces <- lapply(traces, function(trace) {
358+
# scatter-like traces can have both line and marker objects
359+
if (grepl("scatter", trace[["type"]] %||% "scatter")) {
360+
trace[["marker"]] <- modifyList(col_list, trace[["marker"]] %||% list())
361+
trace[["marker"]]$color <- cols[trace$indices]
362+
#mode <- dat[["mode"]] %||% "markers+lines"
363+
# can't have a colorscale for both markers and lines???
364+
#dat[["line"]] <- modifyList(col_list, dat[["line"]] %||% list())
365+
} else {
366+
trace <- c(trace, col_list)
367+
}
368+
trace$color <- NULL; trace$colors <- NULL
369+
trace
370+
})
362371
} else { # discrete color scale
363372
lvls <- unique(cols)
364373
N <- length(lvls)
365374
default <- if (is.ordered(cols)) viridis::viridis(N)
366375
else RColorBrewer::brewer.pal(N, "Set2")
367376
colors <- dat[["colors"]] %||% default
368377
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)
378+
traces <- subdivide_traces(traces, dat, "color", function(sub_trace, lvl) {
379+
sub_trace$marker <- c(sub_trace$marker, list(color = colz[lvl]))
380+
sub_trace$color <- NULL
381+
sub_trace$colors <- NULL
382+
sub_trace
383+
})
372384
}
373-
dat <- lapply(dat, function(x) { x$color <- NULL; x$colors <- NULL; x })
374-
dat
385+
traces
375386
}
376387

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)
388+
symbolize <- function(traces, dat) {
389+
N <- length(unique(dat[["symbol"]]))
383390
if (N > 8) warning("Plotly supports 8 different symbols, but you have ", N, " levels!")
384391
symbols <- c('dot', 'cross', 'diamond', 'square', 'triangle-down', 'triangle-left', 'triangle-right', 'triangle-up')
385-
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
392+
sym <- dat[["symbols"]][seq_len(N)] %||% symbols[seq_len(N)]
393+
subdivide_traces(traces, dat, "symbol", function(trace, lvl) {
394+
trace$symbol <- NULL
395+
trace$marker <- c(trace$marker, list(symbol=sym[[lvl]]))
396+
# symbols really only make sense when markers are in the mode, right?
397+
trace$mode <- dat$mode %||% "markers"
398+
trace
399+
})
388400
}
389401

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]]
402+
# break up each trace in a list into a smaller traces according to the values of
403+
# a given property and apply FUN to each resulting subtrace
404+
# FUN <- function(sub_trace, level_index) is a function taking the new sub-trace and
405+
# the corresponding index of the property value as its input and returning
406+
# the modified sub-trace
407+
subdivide_traces <- function(traces, dat, prop="group", FUN=function(trace, lvl) trace, ...){
408+
x <- dat[[prop]]
394409
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)
410+
return(traces) # property not found, no traces change
412411
}
412+
413+
# the order of lvls determines the order in which traces are drawn
414+
# for ordered factors at least, it makes sense to draw the highest level first
415+
# since that _should_ be the darkest color in a sequential pallette
416+
lvls <- if (is.factor(x)) rev(levels(x)) else unique(x)
417+
n <- length(x)
418+
419+
new_traces <- unlist(lapply(seq_along(lvls), function(lvl_ix) {
420+
lvl <- lvls[[lvl_ix]]
421+
indices <- which(x %in% lvl)
422+
423+
lvl_subtraces <- lapply(traces, function(trace){
424+
mask <- trace$indices %in% indices
425+
if (!any(mask)) return(NULL) # empty trace
426+
427+
trace_size <- length(mask)
428+
# subset the properties that are subsettable
429+
for (i in 1:length(trace)) {
430+
# FIXME better check for subsettable property
431+
tr_prop <- trace[[i]]
432+
if ((is.vector(tr_prop) || is.factor(tr_prop)) && (length(tr_prop) == trace_size)) {
433+
trace[[i]] <- tr_prop[mask]
434+
}
435+
}
436+
if ("name" %in% names(trace)) {
437+
# append lvl to an existing name
438+
trace$name <- paste0(trace$name, '/', lvl)
439+
} else {
440+
trace$name <- lvl
441+
}
442+
FUN(trace, lvl_ix, ...) # customize subtrace
443+
})
444+
}), recursive=FALSE)
445+
new_traces <- new_traces[!sapply(new_traces, is.null)] # remove NULL subtraces
446+
new_traces
413447
}
414448

415449
axis_titles <- function(x, l) {

0 commit comments

Comments
 (0)