From 45219935fd44d8859ac40f149c1baa8c673fe670 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Sat, 19 Mar 2016 16:57:56 +1100 Subject: [PATCH 01/27] first stab at subplot rewrite --- R/subplots.R | 181 +++++++++++++++++++++------------------------------ 1 file changed, 76 insertions(+), 105 deletions(-) diff --git a/R/subplots.R b/R/subplots.R index 50b1794855..ffb018d6a2 100644 --- a/R/subplots.R +++ b/R/subplots.R @@ -20,122 +20,93 @@ #' subplot(p1, p2, p1, p2, nrows = 2) #' } - subplot <- function(..., nrows = 1, which_layout = "merge", margin = 0.02) { - # note that dots is a _list of plotlys_ - dots <- lapply(list(...), plotly_build) - # put existing plot anchors and domain information into a tidy format - # (geo, xaxis, or yaxis can be used to anchor traces on different plots) - p_info <- list() - ctr <- 1 - for (i in seq_along(dots)) { - dat <- dots[[i]]$data - layout <- dots[[i]]$layout - for (j in seq_along(dat)) { - tr <- dat[[j]] - idx <- if (j == 1) "" else j - geo <- unique(tr$geo) %||% "" - # if a valid geo property exists, use that and ignore x/y axis properties - info <- if (grepl("^geo[0-9]+$", geo)) { - d <- layout[[paste0("geo", idx)]][["domain"]] %||% list(x = NA, y = NA) - c( - geo = sub("^geo1$", "geo", geo), - xaxis = "", - xstart = d$x[1], - xend = d$x[2], - yaxis = "", - ystart = d$y[1], - yend = d$y[2] - ) - } else { - dx <- layout[[paste0("xaxis", idx)]][["domain"]] %||% NA - dy <- layout[[paste0("yaxis", idx)]][["domain"]] %||% NA - c( - geo = "", - xaxis = unique(tr$xaxis) %||% "", - xstart = dx[1], - xend = dx[2], - yaxis = unique(tr$yaxis) %||% "", - ystart = dy[1], - yend = dy[2] - ) - } - p_info[[ctr]] <- c(info, plot = i, trace = j) - ctr <- ctr + 1 + # build each plot + plots <- lapply(list(...), plotly_build) + # rename axes, respecting the fact that each plot could be a subplot itself + layouts <- lapply(plots, "[[", "layout") + traces <- lapply(plots, "[[", "data") + xAxes <- lapply(layouts, function(x) { + x[grepl("^xaxis", names(x))] %||% + list(xaxis = list(domain = c(0, 1), anchor = "y")) + }) + yAxes <- lapply(layouts, function(x) { + x[grepl("^yaxis", names(x))] %||% + list(yaxis = list(domain = c(0, 1), anchor = "x")) + }) + # number of x/y axes per plot + xAxisN <- vapply(xAxes, length, numeric(1)) + yAxisN <- vapply(yAxes, length, numeric(1)) + # old -> new axis name dictionary + xAxisMap <- setNames( + unlist(lapply(xAxes, names)), + paste0("xaxis", sub("^1$", "", seq_len(sum(xAxisN)))) + ) + yAxisMap <- setNames( + unlist(lapply(yAxes, names)), + paste0("yaxis", sub("^1$", "", seq_len(sum(yAxisN)))) + ) + # split the map by plot ID + xAxisMap <- split(xAxisMap, rep(seq_along(plots), xAxisN)) + yAxisMap <- split(yAxisMap, rep(seq_along(plots), yAxisN)) + # get the domain of each "viewport" + # TODO: allow control of column width and row height! + domainInfo <- get_domains(length(plots), nrows, margin) + for (i in seq_along(plots)) { + xMap <- xAxisMap[[i]] + yMap <- yAxisMap[[i]] + for (j in seq_along(xAxes[[i]])) { + # before bumping axis anchor, bump trace info, where appropriate + traces[[i]] <- lapply(traces[[i]], function(tr) { + tr$xaxis[tr$xaxis %in% sub("axis", "", xMap[[j]])] <- sub("axis", "", names(xMap[j])) + tr + }) + # bump anchors + map <- yMap[yMap %in% sub("y", "yaxis", xAxes[[i]][[j]]$anchor)] + xAxes[[i]][[j]]$anchor <- sub("axis", "", names(map)) + xAxes[[i]][[j]]$domain <- sort(scales::rescale( + xAxes[[i]][[j]]$domain, + as.numeric(domainInfo[i, c("xstart", "xend")]), + from = c(0, 1) + )) } + for (j in seq_along(yAxes[[i]])) { + traces[[i]] <- lapply(traces[[i]], function(tr) { + tr$yaxis[tr$yaxis == sub("axis", "", yMap[[j]])] <- sub("axis", "", names(yMap[j])) + tr + }) + map <- xMap[xMap %in% sub("x", "xaxis", yAxes[[i]][[j]]$anchor)] + yAxes[[i]][[j]]$anchor <- sub("axis", "", names(map)) + yAxes[[i]][[j]]$domain <- sort(scales::rescale( + yAxes[[i]][[j]]$domain, + as.numeric(domainInfo[i, c("yend", "ystart")]), + from = c(0, 1) + )) + } + xAxes[[i]] <- setNames(xAxes[[i]], names(xMap)) + yAxes[[i]] <- setNames(yAxes[[i]], names(yMap)) } - # put p_info into a data.frame() - p_info <- Reduce(rbind, p_info) - row.names(p_info) <- NULL - p_info <- data.frame(p_info, stringsAsFactors = FALSE) - # obtain the _actual_ plot id - key <- with(p_info, paste0(geo, xaxis, yaxis, plot)) - p_info$key <- match(key, unique(key)) - # bump x/y axis anchors appropriately - p_info$xaxis <- sub("^x1$", "x", paste0("x", p_info$key)) - p_info$yaxis <- sub("^y1$", "y", paste0("y", p_info$key)) - # Only do domain computations if they are _completely_ missing - # (I don't think it makes sense to support partial specification of domains) - if (all(is.na(with(p_info, c(xstart, xend, ystart, yend))))) { - doms <- get_domains(max(p_info$key), nrows, margin) - doms$key <- as.character(seq_len(nrow(doms))) - p_info <- p_info[!names(p_info) %in% c("xstart", "xend", "ystart", "yend")] - p_info <- merge(p_info, doms, by = "key", sort = FALSE) - } - # empty plot container that we'll fill up with new info + + # start merging the plots into a single subplot p <- list( - data = vector("list", nrow(p_info)) + data = Reduce(c, traces), + layout = Reduce(c, c(xAxes, yAxes)) ) - # merge layouts of the subplots - ls <- if (which_layout == "merge") { - lapply(dots, "[[", "layout") - } else { + # TODO: scale shape/annotation coordinates and incorporate them! + # Should we throw warning if [x-y]ref != "paper"? + + # merge non-axis layout stuff + layouts <- lapply(layouts, function(x) x[!grepl("^[x-y]axis", names(x))]) + if (which_layout != "merge") { if (!is.numeric(which_layout)) warning("which_layout must be numeric") - if (!all(idx <- which_layout %in% seq_along(dots))) { + if (!all(idx <- which_layout %in% seq_along(plots))) { warning("which_layout is referencing non-existant layouts") which_layout <- which_layout[idx] } - lapply(dots[which_layout], "[[", "layout") + layouts <- layouts[which_layout] } - ls <- ls[!vapply(ls, is.null, logical(1))] - p[["layout"]] <- Reduce(modifyList, ls) + p$layout <- c(p$layout, Reduce(modifyList, layouts)) - # tack on trace, domain, and anchor information - p_info$plot <- as.numeric(p_info$plot) - p_info$trace <- as.numeric(p_info$trace) - for (i in seq_along(p$data)) { - info <- p_info[i, ] - xdom <- sort(c(info$xstart, info$xend)) - ydom <- sort(c(info$ystart, info$yend)) - p$data[[i]] <- dots[[info$plot]]$data[[info$trace]] - if (grepl("^geo", info$geo)) { - # carry over first geo object if this one is missing - p$layout[[info$geo]] <- p$layout[[info$geo]] %||% p$layout[["geo"]] - # add domains to the layout - p$layout[[info$geo]] <- modifyList( - p$layout[[info$geo]] %||% list(), - list(domain = list(x = xdom, y = ydom)) - ) - # ensure the geo anchor is a single value - p$data[[i]]$geo <- info$geo - } else { - xaxis <- sub("x", "xaxis", info$xaxis) - yaxis <- sub("y", "yaxis", info$yaxis) - # does this plot contain x/y axis styling? If so, use it - # (but overwrite domain/anchor info) - l <- dots[[info$plot]]$layout - p$layout[[xaxis]] <- modifyList( - if (any(idx <- names(l) %in% "xaxis")) l[idx][[1]] else list(), - list(domain = xdom, anchor = info$yaxis) - ) - p$layout[[yaxis]] <- modifyList( - if (any(idx <- names(l) %in% "yaxis")) l[idx][[1]] else list(), - list(domain = ydom, anchor = info$xaxis) - ) - p$data[[i]]$xaxis <- info$xaxis - p$data[[i]]$yaxis <- info$yaxis - } - } hash_plot(data.frame(), p) } From c7098c71085d32ff49523e14f01609cc9a286914 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Wed, 23 Mar 2016 09:32:35 +1100 Subject: [PATCH 02/27] start thinking about shapes/annotations --- R/subplots.R | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/R/subplots.R b/R/subplots.R index ffb018d6a2..936f1bf671 100644 --- a/R/subplots.R +++ b/R/subplots.R @@ -24,8 +24,11 @@ subplot <- function(..., nrows = 1, which_layout = "merge", margin = 0.02) { # build each plot plots <- lapply(list(...), plotly_build) # rename axes, respecting the fact that each plot could be a subplot itself - layouts <- lapply(plots, "[[", "layout") traces <- lapply(plots, "[[", "data") + layouts <- lapply(plots, "[[", "layout") + + annotations <- compact(lapply(layouts, "[[", "annotations")) + shapes <- compact(lapply(layouts, "[[", "shapes")) xAxes <- lapply(layouts, function(x) { x[grepl("^xaxis", names(x))] %||% list(xaxis = list(domain = c(0, 1), anchor = "y")) @@ -55,6 +58,8 @@ subplot <- function(..., nrows = 1, which_layout = "merge", margin = 0.02) { for (i in seq_along(plots)) { xMap <- xAxisMap[[i]] yMap <- yAxisMap[[i]] + xDom <- as.numeric(domainInfo[i, c("xstart", "xend")]) + yDom <- as.numeric(domainInfo[i, c("yend", "ystart")]) for (j in seq_along(xAxes[[i]])) { # before bumping axis anchor, bump trace info, where appropriate traces[[i]] <- lapply(traces[[i]], function(tr) { @@ -64,10 +69,9 @@ subplot <- function(..., nrows = 1, which_layout = "merge", margin = 0.02) { # bump anchors map <- yMap[yMap %in% sub("y", "yaxis", xAxes[[i]][[j]]$anchor)] xAxes[[i]][[j]]$anchor <- sub("axis", "", names(map)) + browser() xAxes[[i]][[j]]$domain <- sort(scales::rescale( - xAxes[[i]][[j]]$domain, - as.numeric(domainInfo[i, c("xstart", "xend")]), - from = c(0, 1) + xAxes[[i]][[j]]$domain, xDom, from = c(0, 1) )) } for (j in seq_along(yAxes[[i]])) { @@ -78,9 +82,7 @@ subplot <- function(..., nrows = 1, which_layout = "merge", margin = 0.02) { map <- xMap[xMap %in% sub("x", "xaxis", yAxes[[i]][[j]]$anchor)] yAxes[[i]][[j]]$anchor <- sub("axis", "", names(map)) yAxes[[i]][[j]]$domain <- sort(scales::rescale( - yAxes[[i]][[j]]$domain, - as.numeric(domainInfo[i, c("yend", "ystart")]), - from = c(0, 1) + yAxes[[i]][[j]]$domain, yDom, from = c(0, 1) )) } xAxes[[i]] <- setNames(xAxes[[i]], names(xMap)) From 0dd4d6394f280b1ced6b034d22f1a8ed557130b4 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Thu, 24 Mar 2016 16:28:15 +1100 Subject: [PATCH 03/27] reposition shapes/annotations --- R/subplots.R | 58 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/R/subplots.R b/R/subplots.R index 936f1bf671..a08b2fe158 100644 --- a/R/subplots.R +++ b/R/subplots.R @@ -21,21 +21,28 @@ #' } subplot <- function(..., nrows = 1, which_layout = "merge", margin = 0.02) { - # build each plot + # build each plot and collect relevant info plots <- lapply(list(...), plotly_build) - # rename axes, respecting the fact that each plot could be a subplot itself traces <- lapply(plots, "[[", "data") layouts <- lapply(plots, "[[", "layout") - - annotations <- compact(lapply(layouts, "[[", "annotations")) - shapes <- compact(lapply(layouts, "[[", "shapes")) + shapes <- lapply(layouts, "[[", "shapes") + # keep non axis title annotations + annotations <- lapply(layouts, function(x) { + axes <- vapply(x$annotations, function(a) identical(a$annotationType, "axis"), logical(1)) + x$annotations[!axes] + }) + # collect axis objects, and remove their titles xAxes <- lapply(layouts, function(x) { - x[grepl("^xaxis", names(x))] %||% + xaxis <- x[grepl("^xaxis", names(x))] %||% list(xaxis = list(domain = c(0, 1), anchor = "y")) + xaxis$title <- NULL + xaxis }) yAxes <- lapply(layouts, function(x) { - x[grepl("^yaxis", names(x))] %||% + yaxis <- x[grepl("^yaxis", names(x))] %||% list(yaxis = list(domain = c(0, 1), anchor = "x")) + yaxis$title <- NULL + yaxis }) # number of x/y axes per plot xAxisN <- vapply(xAxes, length, numeric(1)) @@ -52,9 +59,13 @@ subplot <- function(..., nrows = 1, which_layout = "merge", margin = 0.02) { # split the map by plot ID xAxisMap <- split(xAxisMap, rep(seq_along(plots), xAxisN)) yAxisMap <- split(yAxisMap, rep(seq_along(plots), yAxisN)) - # get the domain of each "viewport" + # domains of each subplot # TODO: allow control of column width and row height! domainInfo <- get_domains(length(plots), nrows, margin) + # reposition shapes and annotations + annotations <- Map(reposition, annotations, split(domainInfo, seq_along(plots))) + shapes <- Map(reposition, shapes, split(domainInfo, seq_along(plots))) + # rename axis objects, anchors, and scale their domains for (i in seq_along(plots)) { xMap <- xAxisMap[[i]] yMap <- yAxisMap[[i]] @@ -69,7 +80,6 @@ subplot <- function(..., nrows = 1, which_layout = "merge", margin = 0.02) { # bump anchors map <- yMap[yMap %in% sub("y", "yaxis", xAxes[[i]][[j]]$anchor)] xAxes[[i]][[j]]$anchor <- sub("axis", "", names(map)) - browser() xAxes[[i]][[j]]$domain <- sort(scales::rescale( xAxes[[i]][[j]]$domain, xDom, from = c(0, 1) )) @@ -94,8 +104,8 @@ subplot <- function(..., nrows = 1, which_layout = "merge", margin = 0.02) { data = Reduce(c, traces), layout = Reduce(c, c(xAxes, yAxes)) ) - # TODO: scale shape/annotation coordinates and incorporate them! - # Should we throw warning if [x-y]ref != "paper"? + p$layout$annotations <- Reduce(c, annotations) + p$layout$shapes <- Reduce(c, shapes) # merge non-axis layout stuff layouts <- lapply(layouts, function(x) x[!grepl("^[x-y]axis", names(x))]) @@ -145,3 +155,29 @@ list2df <- function(x, nms) { df <- data.frame(m) if (!missing(nms)) setNames(df, nms) else df } + +# translate x/y positions according to domain objects +# (useful mostly for repositioning annotations/shapes in subplots) +reposition <- function(obj, domains) { + # we need x and y in order to rescale them! + for (i in seq_along(obj)) { + o <- obj[[i]] + # TODO: this implementation currently assumes xref/yref == "paper" + # should we support references to axis objects as well? + for (j in c("x", "x0", "x1")) { + if (is.numeric(o[[j]])) { + obj[[i]][[j]] <- scales::rescale( + o[[j]], as.numeric(domains[c("xstart", "xend")]), from = c(0, 1) + ) + } + } + for (j in c("y", "y0", "y1")) { + if (is.numeric(o[[j]])) { + obj[[i]][[j]] <- scales::rescale( + o[[j]], as.numeric(domains[c("yend", "ystart")]), from = c(0, 1) + ) + } + } + } + obj +} From 72cb6b81f6392faba8180d4553f1c551e6868750 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Thu, 24 Mar 2016 16:30:37 +1100 Subject: [PATCH 04/27] add meta-info to identify axis titles; simplify facet strip drawing logic --- R/ggplotly.R | 64 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/R/ggplotly.R b/R/ggplotly.R index 7670ec5489..9ec2c2c674 100644 --- a/R/ggplotly.R +++ b/R/ggplotly.R @@ -420,7 +420,7 @@ gg2list <- function(p, width = NULL, height = NULL, tooltip = "all", source = "A gglayout$annotations, make_label( faced(axisTitleText, axisTitle$face), x, y, el = axisTitle, - xanchor = "center", yanchor = "middle" + xanchor = "center", yanchor = "middle", annotationType = "axis" ) ) } @@ -432,36 +432,52 @@ gg2list <- function(p, width = NULL, height = NULL, tooltip = "all", source = "A ydom <- gglayout[[lay[, "yaxis"]]]$domain border <- make_panel_border(xdom, ydom, theme) gglayout$shapes <- c(gglayout$shapes, border) - # facet strips -> plotly annotations + if (has_facet(p)) { + col_vars <- ifelse(inherits(p$facet, "wrap"), "facets", "cols") + col_txt <- paste( + p$facet$labeller(lay[names(p$facet[[col_vars]])]), collapse = ", " + ) + if (nchar(col_txt) > 0) { + col_lab <- make_label( + col_txt, x = mean(xdom), y = max(ydom), + el = theme[["strip.text.x"]] %||% theme[["strip.text"]], + xanchor = "center", yanchor = "bottom" + ) + gglayout$annotations <- c(gglayout$annotations, col_lab) + strip <- make_strip_rect(xdom, ydom, theme, "top") + gglayout$shapes <- c(gglayout$shapes, strip) + } + row_txt <- paste( + p$facet$labeller(lay[names(p$facet$rows)]), collapse = ", " + ) + if (nchar(row_txt) > 0) { + row_lab <- make_label( + row_txt, x = max(xdom), y = mean(ydom), + el = theme[["strip.text.y"]] %||% theme[["strip.text"]], + xanchor = "left", yanchor = "middle" + ) + gglayout$annotations <- c(gglayout$annotations, row_lab) + strip <- make_strip_rect(xdom, ydom, theme, "right") + gglayout$shapes <- c(gglayout$shapes, strip) + } + + + + } + + if (!is_blank(theme[["strip.text.x"]]) && (inherits(p$facet, "wrap") || inherits(p$facet, "grid") && lay$ROW == 1)) { vars <- ifelse(inherits(p$facet, "wrap"), "facets", "cols") - txt <- paste( - p$facet$labeller(lay[names(p$facet[[vars]])]), collapse = ", " - ) - lab <- make_label( - txt, x = mean(xdom), y = max(ydom), - el = theme[["strip.text.x"]] %||% theme[["strip.text"]], - xanchor = "center", yanchor = "bottom" - ) - gglayout$annotations <- c(gglayout$annotations, lab) - strip <- make_strip_rect(xdom, ydom, theme, "top") - gglayout$shapes <- c(gglayout$shapes, strip) + + + } if (inherits(p$facet, "grid") && lay$COL == nCols && nRows > 1 && !is_blank(theme[["strip.text.y"]])) { - txt <- paste( - p$facet$labeller(lay[names(p$facet$rows)]), collapse = ", " - ) - lab <- make_label( - txt, x = max(xdom), y = mean(ydom), - el = theme[["strip.text.y"]] %||% theme[["strip.text"]], - xanchor = "left", yanchor = "middle" - ) - gglayout$annotations <- c(gglayout$annotations, lab) - strip <- make_strip_rect(xdom, ydom, theme, "right") - gglayout$shapes <- c(gglayout$shapes, strip) + + } } # end of panel loop From b6fb58eba94135c01c82a9e670b4eeebe88856c1 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Thu, 24 Mar 2016 16:37:05 +1100 Subject: [PATCH 05/27] safeguard against missing info --- R/subplots.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/R/subplots.R b/R/subplots.R index a08b2fe158..c1cf9c3897 100644 --- a/R/subplots.R +++ b/R/subplots.R @@ -74,7 +74,7 @@ subplot <- function(..., nrows = 1, which_layout = "merge", margin = 0.02) { for (j in seq_along(xAxes[[i]])) { # before bumping axis anchor, bump trace info, where appropriate traces[[i]] <- lapply(traces[[i]], function(tr) { - tr$xaxis[tr$xaxis %in% sub("axis", "", xMap[[j]])] <- sub("axis", "", names(xMap[j])) + tr$xaxis[sub("axis", "", xMap[[j]]) %in% tr$xaxis] <- sub("axis", "", names(xMap[j])) tr }) # bump anchors @@ -86,7 +86,7 @@ subplot <- function(..., nrows = 1, which_layout = "merge", margin = 0.02) { } for (j in seq_along(yAxes[[i]])) { traces[[i]] <- lapply(traces[[i]], function(tr) { - tr$yaxis[tr$yaxis == sub("axis", "", yMap[[j]])] <- sub("axis", "", names(yMap[j])) + tr$yaxis[sub("axis", "", yMap[[j]]) %in% tr$yaxis] <- sub("axis", "", names(yMap[j])) tr }) map <- xMap[xMap %in% sub("x", "xaxis", yAxes[[i]][[j]]$anchor)] @@ -108,7 +108,7 @@ subplot <- function(..., nrows = 1, which_layout = "merge", margin = 0.02) { p$layout$shapes <- Reduce(c, shapes) # merge non-axis layout stuff - layouts <- lapply(layouts, function(x) x[!grepl("^[x-y]axis", names(x))]) + layouts <- lapply(layouts, function(x) x[!grepl("^[x-y]axis", names(x))] %||% list()) if (which_layout != "merge") { if (!is.numeric(which_layout)) warning("which_layout must be numeric") if (!all(idx <- which_layout %in% seq_along(plots))) { From 70ca6744fa54acc72461b354305f8135989e450a Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Thu, 24 Mar 2016 17:51:36 +1100 Subject: [PATCH 06/27] safeguard against missing values in subplot; ggplotly is now generic with ggmatrix/ggplot methods --- NAMESPACE | 2 ++ R/ggplotly.R | 39 +++++++++++++++------------- R/plotly.R | 7 ++--- R/subplots.R | 25 +++++++++--------- tests/testthat/test-plotly-subplot.R | 3 --- 5 files changed, 39 insertions(+), 37 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index d2a56e4a85..b986eb36f4 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -11,6 +11,8 @@ S3method(geom2trace,GeomPolygon) S3method(geom2trace,GeomText) S3method(geom2trace,GeomTile) S3method(geom2trace,default) +S3method(ggplotly,ggmatrix) +S3method(ggplotly,ggplot) S3method(print,figure) S3method(print,plotly) S3method(to_basic,GeomAbline) diff --git a/R/ggplotly.R b/R/ggplotly.R index 9ec2c2c674..5e56a7e633 100644 --- a/R/ggplotly.R +++ b/R/ggplotly.R @@ -32,6 +32,25 @@ #' ggplotly <- function(p = ggplot2::last_plot(), width = NULL, height = NULL, tooltip = "all", source = "A") { + UseMethod("ggplotly") +} + +#' @export +ggplotly.ggmatrix <- function(p = ggplot2::last_plot(), width = NULL, + height = NULL, tooltip = "all", source = "A") { + plotList <- list() + for (i in seq_len(p$nrow)) { + for (j in seq_len(p$ncol)) { + plotList <- c(plotList, list(pm[i, j])) + } + } + # TODO: how to show x/y titles? Should these be arguments in subplot? + do.call(subplot, c(plotList, list(nrows = p$nrow))) +} + +#' @export +ggplotly.ggplot <- function(p = ggplot2::last_plot(), width = NULL, + height = NULL, tooltip = "all", source = "A") { l <- gg2list(p, width = width, height = height, tooltip = tooltip, source = source) hash_plot(p$data, l) } @@ -428,10 +447,12 @@ gg2list <- function(p, width = NULL, height = NULL, tooltip = "all", source = "A } # end of axis loop + # theme(panel.border = ) -> plotly rect shape xdom <- gglayout[[lay[, "xaxis"]]]$domain ydom <- gglayout[[lay[, "yaxis"]]]$domain border <- make_panel_border(xdom, ydom, theme) gglayout$shapes <- c(gglayout$shapes, border) + # facet strips -> plotly annotations if (has_facet(p)) { col_vars <- ifelse(inherits(p$facet, "wrap"), "facets", "cols") @@ -461,25 +482,7 @@ gg2list <- function(p, width = NULL, height = NULL, tooltip = "all", source = "A strip <- make_strip_rect(xdom, ydom, theme, "right") gglayout$shapes <- c(gglayout$shapes, strip) } - - - } - - - if (!is_blank(theme[["strip.text.x"]]) && - (inherits(p$facet, "wrap") || inherits(p$facet, "grid") && lay$ROW == 1)) { - vars <- ifelse(inherits(p$facet, "wrap"), "facets", "cols") - - - - } - if (inherits(p$facet, "grid") && lay$COL == nCols && nRows > 1 && - !is_blank(theme[["strip.text.y"]])) { - - - } - } # end of panel loop # ------------------------------------------------------------------------ diff --git a/R/plotly.R b/R/plotly.R index 4e63e37f1d..5cf44e66e2 100644 --- a/R/plotly.R +++ b/R/plotly.R @@ -232,9 +232,10 @@ style <- function(p = last_plot(), ..., traces = 1, evaluate = FALSE) { #' @param l a ggplot object, or a plotly object, or a list. #' @export plotly_build <- function(l = last_plot()) { - #if (inherits(l, "ggmatrix")) - # ggplot objects don't need any special type of handling - if (ggplot2::is.ggplot(l)) return(gg2list(l)) + # ggplot objects (including ggmatrix) don't need any special type of handling + if (inherits(l, "gg")) { + return(structure(get_plot(ggplotly(l)), class = "plotly")) + } l <- get_plot(l) # assume unnamed list elements are data/traces nms <- names(l) diff --git a/R/subplots.R b/R/subplots.R index c1cf9c3897..1e54bd46e2 100644 --- a/R/subplots.R +++ b/R/subplots.R @@ -31,19 +31,16 @@ subplot <- function(..., nrows = 1, which_layout = "merge", margin = 0.02) { axes <- vapply(x$annotations, function(a) identical(a$annotationType, "axis"), logical(1)) x$annotations[!axes] }) - # collect axis objects, and remove their titles + # collect axis objects xAxes <- lapply(layouts, function(x) { - xaxis <- x[grepl("^xaxis", names(x))] %||% - list(xaxis = list(domain = c(0, 1), anchor = "y")) - xaxis$title <- NULL - xaxis + x[grepl("^xaxis", names(x))] %||% list(xaxis = list(domain = c(0, 1), anchor = "y")) }) yAxes <- lapply(layouts, function(x) { - yaxis <- x[grepl("^yaxis", names(x))] %||% - list(yaxis = list(domain = c(0, 1), anchor = "x")) - yaxis$title <- NULL - yaxis + x[grepl("^yaxis", names(x))] %||% list(yaxis = list(domain = c(0, 1), anchor = "x")) }) + # remove their titles + xAxes <- lapply(xAxes, function(x) lapply(x, function(y) { y$title <- NULL; y })) + yAxes <- lapply(yAxes, function(x) lapply(x, function(y) { y$title <- NULL; y })) # number of x/y axes per plot xAxisN <- vapply(xAxes, length, numeric(1)) yAxisN <- vapply(yAxes, length, numeric(1)) @@ -74,25 +71,27 @@ subplot <- function(..., nrows = 1, which_layout = "merge", margin = 0.02) { for (j in seq_along(xAxes[[i]])) { # before bumping axis anchor, bump trace info, where appropriate traces[[i]] <- lapply(traces[[i]], function(tr) { + tr$xaxis <- tr$xaxis %||% "x" tr$xaxis[sub("axis", "", xMap[[j]]) %in% tr$xaxis] <- sub("axis", "", names(xMap[j])) tr }) # bump anchors - map <- yMap[yMap %in% sub("y", "yaxis", xAxes[[i]][[j]]$anchor)] + map <- yMap[yMap %in% sub("y", "yaxis", xAxes[[i]][[j]]$anchor %||% "y")] xAxes[[i]][[j]]$anchor <- sub("axis", "", names(map)) xAxes[[i]][[j]]$domain <- sort(scales::rescale( - xAxes[[i]][[j]]$domain, xDom, from = c(0, 1) + xAxes[[i]][[j]]$domain %||% c(0, 1), xDom, from = c(0, 1) )) } for (j in seq_along(yAxes[[i]])) { traces[[i]] <- lapply(traces[[i]], function(tr) { + tr$yaxis <- tr$yaxis %||% "y" tr$yaxis[sub("axis", "", yMap[[j]]) %in% tr$yaxis] <- sub("axis", "", names(yMap[j])) tr }) - map <- xMap[xMap %in% sub("x", "xaxis", yAxes[[i]][[j]]$anchor)] + map <- xMap[xMap %in% sub("x", "xaxis", yAxes[[i]][[j]]$anchor %||% "x")] yAxes[[i]][[j]]$anchor <- sub("axis", "", names(map)) yAxes[[i]][[j]]$domain <- sort(scales::rescale( - yAxes[[i]][[j]]$domain, yDom, from = c(0, 1) + yAxes[[i]][[j]]$domain %||% c(0, 1), yDom, from = c(0, 1) )) } xAxes[[i]] <- setNames(xAxes[[i]], names(xMap)) diff --git a/tests/testthat/test-plotly-subplot.R b/tests/testthat/test-plotly-subplot.R index d73b9b624e..c646dbfc89 100644 --- a/tests/testthat/test-plotly-subplot.R +++ b/tests/testthat/test-plotly-subplot.R @@ -44,6 +44,3 @@ test_that("group + [x/y]axis works", { expect_true(all(2/3 > xdom[[2]] & xdom[[2]] > 1/3)) expect_true(all(1 >= xdom[[3]] & xdom[[3]] > 2/3)) }) - - - From d8f2f7a1683994a44773e3edb7d899d723bcffb1 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Fri, 1 Apr 2016 10:03:53 +1100 Subject: [PATCH 07/27] add plot title to subplot --- R/ggplotly.R | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/R/ggplotly.R b/R/ggplotly.R index 5e56a7e633..f91770454a 100644 --- a/R/ggplotly.R +++ b/R/ggplotly.R @@ -41,11 +41,13 @@ ggplotly.ggmatrix <- function(p = ggplot2::last_plot(), width = NULL, plotList <- list() for (i in seq_len(p$nrow)) { for (j in seq_len(p$ncol)) { - plotList <- c(plotList, list(pm[i, j])) + plotList <- c(plotList, list(p[i, j])) } } # TODO: how to show x/y titles? Should these be arguments in subplot? - do.call(subplot, c(plotList, list(nrows = p$nrow))) + l <- get_plot(do.call(subplot, c(plotList, list(nrows = p$nrow)))) + l$layout$title <- p$title + hash_plot(p$data, l) } #' @export From a45749ccaae7240d793831120ef8daabdb426621 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Tue, 5 Apr 2016 13:11:23 +1000 Subject: [PATCH 08/27] Implement widths/heights arguments --- .gitignore | 3 ++- R/subplots.R | 46 +++++++++++++++++++++++++++++++++++++++------- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index f2df237368..00ac241117 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,8 @@ Rapp.history *.Rhistory *.RData *.Rproj.user -.Rproj.user +*.DS_Store build_site.R +todo.R inst/examples/*/*.html inst/examples/*/rsconnect/* diff --git a/R/subplots.R b/R/subplots.R index 1e54bd46e2..83f1ff2c08 100644 --- a/R/subplots.R +++ b/R/subplots.R @@ -3,6 +3,11 @@ #' @param ... any number of plotly objects #' @param nrows number of rows for laying out plots in a grid-like structure. #' Only used if no domain is already specified. +#' @param widths relative width of each column on a 0-1 scale. By default all +#' columns have an equal relative width. +#' @param heights relative height of each row on a 0-1 scale. By default all +#' rows have an equal relative height. +#' @param share determines whether x/y/both axes are shared. #' @param which_layout adopt the layout of which plot? If the default value of #' "merge" is used, all plot level layout options will be included in the final #' layout. This argument also accepts a numeric vector which will restric @@ -20,7 +25,8 @@ #' subplot(p1, p2, p1, p2, nrows = 2) #' } -subplot <- function(..., nrows = 1, which_layout = "merge", margin = 0.02) { +subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, share = NULL, + which_layout = "merge", margin = 0.02) { # build each plot and collect relevant info plots <- lapply(list(...), plotly_build) traces <- lapply(plots, "[[", "data") @@ -58,7 +64,9 @@ subplot <- function(..., nrows = 1, which_layout = "merge", margin = 0.02) { yAxisMap <- split(yAxisMap, rep(seq_along(plots), yAxisN)) # domains of each subplot # TODO: allow control of column width and row height! - domainInfo <- get_domains(length(plots), nrows, margin) + domainInfo <- get_domains( + length(plots), nrows, margin, widths = widths, heights = heights + ) # reposition shapes and annotations annotations <- Map(reposition, annotations, split(domainInfo, seq_along(plots))) shapes <- Map(reposition, shapes, split(domainInfo, seq_along(plots))) @@ -122,16 +130,40 @@ subplot <- function(..., nrows = 1, which_layout = "merge", margin = 0.02) { } -get_domains <- function(nplots = 1, nrows = 1, margins = 0.01) { +get_domains <- function(nplots = 1, nrows = 1, margins = 0.01, + widths = NULL, heights = NULL) { if (length(margins) == 1) margins <- rep(margins, 4) if (length(margins) != 4) stop("margins must be length 1 or 4", call. = FALSE) ncols <- ceiling(nplots / nrows) + widths <- widths %||% rep(1 / ncols, ncols) + heights <- heights %||% rep(1 / nrows, nrows) + if (length(widths) != ncols) { + stop("The length of the widths argument must be equal ", + "to the number of columns", call. = FALSE) + } + if (length(heights) != nrows) { + stop("The length of the heights argument must be equal ", + "to the number of rows", call. = FALSE) + } + if (any(widths < 0 | heights < 0)) { + stop("The widths and heights arguments must contain positive values") + } + if (sum(widths) > 1 | sum(heights) > 1) { + stop("The sum of the widths and heights arguments must be less than 1") + } + + widths <- cumsum(c(0, widths)) + heights <- cumsum(c(0, heights)) + # 'center' these values if there is still room left + widths <- widths + (1 - max(widths)) / 2 + heights <- heights + (1 - max(heights)) / 2 + xs <- vector("list", ncols) for (i in seq_len(ncols)) { xs[[i]] <- c( - xstart = ((i - 1) / ncols) + ifelse(i == 1, 0, margins[1]), - xend = (i / ncols) - ifelse(i == ncols, 0, margins[2]) + xstart = widths[i] + if (i == 1) 0 else margins[1], + xend = widths[i + 1] - if (i == ncols) 0 else margins[2] ) } xz <- rep_len(xs, nplots) @@ -140,8 +172,8 @@ get_domains <- function(nplots = 1, nrows = 1, margins = 0.01) { for (i in seq_len(nplots)) { j <- ceiling(i / ncols) ys[[i]] <- c( - ystart = 1 - ((j - 1) / nrows) - ifelse(j == 1, 0, margins[3]), - yend = 1 - (j / nrows) + ifelse(j == nrows, 0, margins[4]) + ystart = 1 - (heights[j]) - if (j == 1) 0 else margins[3], + yend = 1 - (heights[j + 1]) + if (j == nrows) 0 else margins[4] ) } list2df(Map(c, xz, ys)) From a8c162857302ec6173fefe4bffe5790194e3c9a7 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Tue, 5 Apr 2016 18:10:32 +1000 Subject: [PATCH 09/27] add shareX/shareY arguments --- R/ggplotly.R | 4 ++- R/subplots.R | 30 ++++++++++++------ man/subplot.Rd | 19 ++++++++--- tests/testthat/test-plotly-subplot.R | 47 ++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 14 deletions(-) diff --git a/R/ggplotly.R b/R/ggplotly.R index f91770454a..cbcc67537a 100644 --- a/R/ggplotly.R +++ b/R/ggplotly.R @@ -44,7 +44,9 @@ ggplotly.ggmatrix <- function(p = ggplot2::last_plot(), width = NULL, plotList <- c(plotList, list(p[i, j])) } } - # TODO: how to show x/y titles? Should these be arguments in subplot? + # TODO: + # (1) how to show x/y titles? Should these be arguments in subplot? + # (2) it only makes since to share axes on the lower diagonal l <- get_plot(do.call(subplot, c(plotList, list(nrows = p$nrow)))) l$layout$title <- p$title hash_plot(p$data, l) diff --git a/R/subplots.R b/R/subplots.R index 83f1ff2c08..05d5f81dee 100644 --- a/R/subplots.R +++ b/R/subplots.R @@ -7,15 +7,16 @@ #' columns have an equal relative width. #' @param heights relative height of each row on a 0-1 scale. By default all #' rows have an equal relative height. -#' @param share determines whether x/y/both axes are shared. -#' @param which_layout adopt the layout of which plot? If the default value of -#' "merge" is used, all plot level layout options will be included in the final -#' layout. This argument also accepts a numeric vector which will restric +#' @param shareX should the x-axis be shared amongst the subplots? +#' @param shareY should the y-axis be shared amongst the subplots? #' @param margin either a single value or four values (all between 0 and 1). #' If four values are provided, the first is used as the left margin, the second #' is used as the right margin, the third is used as the top margin, and the #' fourth is used as the bottom margin. #' If a single value is provided, it will be used as all four margins. +#' @param which_layout adopt the layout of which plot? If the default value of +#' "merge" is used, all plot level layout options will be included in the final +#' layout. This argument also accepts a numeric vector specifying #' @return A plotly object #' @export #' @author Carson Sievert @@ -25,8 +26,8 @@ #' subplot(p1, p2, p1, p2, nrows = 2) #' } -subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, share = NULL, - which_layout = "merge", margin = 0.02) { +subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, shareX = FALSE, + shareY = FALSE, margin = 0.02, which_layout = "merge") { # build each plot and collect relevant info plots <- lapply(list(...), plotly_build) traces <- lapply(plots, "[[", "data") @@ -51,13 +52,24 @@ subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, share = NULL, xAxisN <- vapply(xAxes, length, numeric(1)) yAxisN <- vapply(yAxes, length, numeric(1)) # old -> new axis name dictionary + ncols <- ceiling(length(plots) / nrows) + xAxisID <- if (shareX) { + rep(rep(1:ncols, length.out = length(plots)), xAxisN) + } else { + seq_len(sum(xAxisN)) + } + yAxisID <- if (shareY) { + rep(rep(1:nrows, each = ncols, length.out = length(plots)), yAxisN) + } else { + seq_len(sum(yAxisN)) + } xAxisMap <- setNames( unlist(lapply(xAxes, names)), - paste0("xaxis", sub("^1$", "", seq_len(sum(xAxisN)))) + paste0("xaxis", sub("^1$", "", xAxisID)) ) yAxisMap <- setNames( unlist(lapply(yAxes, names)), - paste0("yaxis", sub("^1$", "", seq_len(sum(yAxisN)))) + paste0("yaxis", sub("^1$", "", yAxisID)) ) # split the map by plot ID xAxisMap <- split(xAxisMap, rep(seq_along(plots), xAxisN)) @@ -109,7 +121,7 @@ subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, share = NULL, # start merging the plots into a single subplot p <- list( data = Reduce(c, traces), - layout = Reduce(c, c(xAxes, yAxes)) + layout = Reduce(modifyList, c(xAxes, rev(yAxes))) ) p$layout$annotations <- Reduce(c, annotations) p$layout$shapes <- Reduce(c, shapes) diff --git a/man/subplot.Rd b/man/subplot.Rd index f4a6c0499e..a093265ce0 100644 --- a/man/subplot.Rd +++ b/man/subplot.Rd @@ -4,7 +4,8 @@ \alias{subplot} \title{View multiple plots in a single view} \usage{ -subplot(..., nrows = 1, which_layout = "merge", margin = 0.02) +subplot(..., nrows = 1, widths = NULL, heights = NULL, shareX = FALSE, + shareY = FALSE, margin = 0.02, which_layout = "merge") } \arguments{ \item{...}{any number of plotly objects} @@ -12,15 +13,25 @@ subplot(..., nrows = 1, which_layout = "merge", margin = 0.02) \item{nrows}{number of rows for laying out plots in a grid-like structure. Only used if no domain is already specified.} -\item{which_layout}{adopt the layout of which plot? If the default value of -"merge" is used, all plot level layout options will be included in the final -layout. This argument also accepts a numeric vector which will restric} +\item{widths}{relative width of each column on a 0-1 scale. By default all +columns have an equal relative width.} + +\item{heights}{relative height of each row on a 0-1 scale. By default all +rows have an equal relative height.} + +\item{shareX}{should the x-axis be shared amongst the subplots?} + +\item{shareY}{should the y-axis be shared amongst the subplots?} \item{margin}{either a single value or four values (all between 0 and 1). If four values are provided, the first is used as the left margin, the second is used as the right margin, the third is used as the top margin, and the fourth is used as the bottom margin. If a single value is provided, it will be used as all four margins.} + +\item{which_layout}{adopt the layout of which plot? If the default value of +"merge" is used, all plot level layout options will be included in the final +layout. This argument also accepts a numeric vector specifying} } \value{ A plotly object diff --git a/tests/testthat/test-plotly-subplot.R b/tests/testthat/test-plotly-subplot.R index c646dbfc89..f798b54f78 100644 --- a/tests/testthat/test-plotly-subplot.R +++ b/tests/testthat/test-plotly-subplot.R @@ -44,3 +44,50 @@ test_that("group + [x/y]axis works", { expect_true(all(2/3 > xdom[[2]] & xdom[[2]] > 1/3)) expect_true(all(1 >= xdom[[3]] & xdom[[3]] > 2/3)) }) + +test_that("shareX produces one x-axis", { + s <- subplot(plot_ly(x = 1), plot_ly(x = 1), nrows = 2, shareX = TRUE) + l <- expect_traces(s, 2, "shareX") + expect_true(sum(grepl("^xaxis", names(l$layout))) == 1) +}) + +test_that("shareY produces one y-axis", { + s <- subplot(plot_ly(x = 1), plot_ly(x = 1), shareY = TRUE) + l <- expect_traces(s, 2, "shareY") + expect_true(sum(grepl("^yaxis", names(l$layout))) == 1) +}) + +test_that("share both axes", { + s <- subplot( + plot_ly(x = 1), plot_ly(x = 1), plot_ly(x = 1), plot_ly(x = 1), + nrows = 2, shareX = TRUE, shareY = TRUE + ) + l <- expect_traces(s, 4, "shareBoth") + expect_true(sum(grepl("^yaxis", names(l$layout))) == 2) + expect_true(sum(grepl("^xaxis", names(l$layout))) == 2) +}) + +# https://github.com/ropensci/plotly/issues/376 +library(plotly) +d <- data.frame( + x = rnorm(100), + y = rnorm(100) +) +hist_top <- ggplot(d) + geom_histogram(aes(x = x)) +empty <- ggplot() + geom_blank() +scatter <- ggplot(d) + geom_point(aes(x = x, y = y)) +hist_right <- ggplot(d) + geom_histogram(aes(x = y)) + coord_flip() +s <- subplot( + hist_top, empty, scatter, hist_right, + nrows = 2, widths = c(0.8, 0.2), heights = c(0.2, 0.8), + margin = 0.005, shareX = TRUE, shareY = TRUE +) + +test_that("Row/column height/width", { + l <- expect_traces(s, 3, "width-height") + expect_equal(diff(l$layout$xaxis$domain), 0.8 - 0.005) + expect_equal(diff(l$layout$xaxis2$domain), 0.2 - 0.005) + expect_equal(diff(l$layout$yaxis$domain), 0.2 - 0.005) + expect_equal(diff(l$layout$yaxis2$domain), 0.8 - 0.005) +}) + From c61675f8e4925a7932aa8de666f3e66a44bba7cf Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Tue, 5 Apr 2016 18:25:40 +1000 Subject: [PATCH 10/27] fix test typo --- tests/testthat/test-plotly-subplot.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testthat/test-plotly-subplot.R b/tests/testthat/test-plotly-subplot.R index f798b54f78..60fe4dfcdd 100644 --- a/tests/testthat/test-plotly-subplot.R +++ b/tests/testthat/test-plotly-subplot.R @@ -84,7 +84,7 @@ s <- subplot( ) test_that("Row/column height/width", { - l <- expect_traces(s, 3, "width-height") + l <- expect_traces(s, 4, "width-height") expect_equal(diff(l$layout$xaxis$domain), 0.8 - 0.005) expect_equal(diff(l$layout$xaxis2$domain), 0.2 - 0.005) expect_equal(diff(l$layout$yaxis$domain), 0.2 - 0.005) From d10b5a98e790640445f434d3448ae012e665a619 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Fri, 6 May 2016 09:42:23 +1000 Subject: [PATCH 11/27] make plotly_build a generic function --- NAMESPACE | 3 +++ R/ggplotly.R | 2 +- R/plotly.R | 47 ++++++++++++++++++++++++++++++++++----------- man/plotly_build.Rd | 29 +++++++++++++++++++++------- 4 files changed, 62 insertions(+), 19 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index eb5f1b1d6d..91da4e6889 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -13,6 +13,9 @@ S3method(geom2trace,GeomTile) S3method(geom2trace,default) S3method(ggplotly,ggmatrix) S3method(ggplotly,ggplot) +S3method(plotly_build,gg) +S3method(plotly_build,plotly_built) +S3method(plotly_build,plotly_hash) S3method(print,figure) S3method(print,plotly_built) S3method(print,plotly_hash) diff --git a/R/ggplotly.R b/R/ggplotly.R index da7f1f1bc9..90b8cc524d 100644 --- a/R/ggplotly.R +++ b/R/ggplotly.R @@ -448,7 +448,7 @@ gg2list <- function(p, width = NULL, height = NULL, tooltip = "all", source = "A gglayout$annotations, make_label( faced(axisTitleText, axisTitle$face), x, y, el = axisTitle, - xanchor = "center", yanchor = "middle" + xanchor = "center", yanchor = "middle", annotationType = "axis" ) ) } diff --git a/R/plotly.R b/R/plotly.R index b7a785374c..a8e8e3a07a 100644 --- a/R/plotly.R +++ b/R/plotly.R @@ -221,21 +221,46 @@ style <- function(p = last_plot(), ..., traces = 1, evaluate = FALSE) { hash_plot(data, p) } -#' Build a plotly object before viewing it +#' Create a 'plotly_built' object #' -#' 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. +#' This generic function creates the list object sent to plotly.js +#' for rendering. Using this function can be useful for overriding defaults +#' provided by \code{ggplotly}/\code{plot_ly} or for debugging rendering +#' errors. #' -#' @param l a ggplot object, or a plotly object, or a list. +#' @param l a ggplot object, or a plotly_hash object, or a list. #' @export +#' @examples +#' +#' p <- plot_ly() +#' # data frame +#' str(p) +#' # the actual list of options sent to plotly.js +#' str(plotly_build(p)) +#' +#' p <- qplot(data = mtcars, wt, mpg, geom = c("point", "smooth")) +#' l <- plotly_build(p) +#' # turn off hoverinfo for the smooth (but keep it for the points) +#' l$data[[2]]$hoverinfo <- "none" +#' l$data[[3]]$hoverinfo <- "none" +#' l +#' plotly_build <- function(l = last_plot()) { - # ggplot objects (including ggmatrix) don't need any special type of handling - if (inherits(l, "gg")) { - return(structure(get_plot(ggplotly(l)), class = "plotly")) - } + UseMethod("plotly_build") +} + +#' @export +plotly_build.plotly_built <- function(l = last_plot()) { + l +} + +#' @export +plotly_build.gg <- function(l = last_plot()) { + structure(get_plot(ggplotly(l)), class = "plotly_built") +} + +#' @export +plotly_build.plotly_hash <- function(l = last_plot()) { l <- get_plot(l) # assume unnamed list elements are data/traces nms <- names(l) diff --git a/man/plotly_build.Rd b/man/plotly_build.Rd index 689ede07d0..b074b94518 100644 --- a/man/plotly_build.Rd +++ b/man/plotly_build.Rd @@ -2,18 +2,33 @@ % Please edit documentation in R/plotly.R \name{plotly_build} \alias{plotly_build} -\title{Build a plotly object before viewing it} +\title{Create a 'plotly_built' object} \usage{ plotly_build(l = last_plot()) } \arguments{ -\item{l}{a ggplot object, or a plotly object, or a list.} +\item{l}{a ggplot object, or a plotly_hash object, or a list.} } \description{ -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. +This generic function creates the list object sent to plotly.js +for rendering. Using this function can be useful for overriding defaults +provided by \code{ggplotly}/\code{plot_ly} or for debugging rendering +errors. +} +\examples{ + +p <- plot_ly() +# data frame +str(p) +# the actual list of options sent to plotly.js +str(plotly_build(p)) + +p <- qplot(data = mtcars, wt, mpg, geom = c("point", "smooth")) +l <- plotly_build(p) +# turn off hoverinfo for the smooth (but keep it for the points) +l$data[[2]]$hoverinfo <- "none" +l$data[[3]]$hoverinfo <- "none" +l + } From 975733de11b9947819862cd5bedac3cb0105ba76 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Fri, 6 May 2016 10:49:41 +1000 Subject: [PATCH 12/27] add keep_titles argument --- R/subplots.R | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/R/subplots.R b/R/subplots.R index 05d5f81dee..08c1f124fa 100644 --- a/R/subplots.R +++ b/R/subplots.R @@ -14,9 +14,11 @@ #' is used as the right margin, the third is used as the top margin, and the #' fourth is used as the bottom margin. #' If a single value is provided, it will be used as all four margins. +#' @param keep_titles should axis titles be retained? #' @param which_layout adopt the layout of which plot? If the default value of -#' "merge" is used, all plot level layout options will be included in the final -#' layout. This argument also accepts a numeric vector specifying +#' "merge" is used, layout options found later in the sequence of plots will +#' override options found earlier in the sequence. This argument also accepts a +#' numeric vector specifying which plots to consider when merging. #' @return A plotly object #' @export #' @author Carson Sievert @@ -27,7 +29,8 @@ #' } subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, shareX = FALSE, - shareY = FALSE, margin = 0.02, which_layout = "merge") { + shareY = FALSE, margin = 0.02, which_layout = "merge", + keep_titles = FALSE) { # build each plot and collect relevant info plots <- lapply(list(...), plotly_build) traces <- lapply(plots, "[[", "data") @@ -46,8 +49,10 @@ subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, shareX = FALS x[grepl("^yaxis", names(x))] %||% list(yaxis = list(domain = c(0, 1), anchor = "x")) }) # remove their titles - xAxes <- lapply(xAxes, function(x) lapply(x, function(y) { y$title <- NULL; y })) - yAxes <- lapply(yAxes, function(x) lapply(x, function(y) { y$title <- NULL; y })) + if (!keep_titles) { + xAxes <- lapply(xAxes, function(x) lapply(x, function(y) { y$title <- NULL; y })) + yAxes <- lapply(yAxes, function(x) lapply(x, function(y) { y$title <- NULL; y })) + } # number of x/y axes per plot xAxisN <- vapply(xAxes, length, numeric(1)) yAxisN <- vapply(yAxes, length, numeric(1)) @@ -75,7 +80,6 @@ subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, shareX = FALS xAxisMap <- split(xAxisMap, rep(seq_along(plots), xAxisN)) yAxisMap <- split(yAxisMap, rep(seq_along(plots), yAxisN)) # domains of each subplot - # TODO: allow control of column width and row height! domainInfo <- get_domains( length(plots), nrows, margin, widths = widths, heights = heights ) @@ -137,7 +141,6 @@ subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, shareX = FALS layouts <- layouts[which_layout] } p$layout <- c(p$layout, Reduce(modifyList, layouts)) - hash_plot(data.frame(), p) } From 98dcd96d1a3f83b0b09e04aef23803119c556034 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Thu, 12 May 2016 14:49:48 +1000 Subject: [PATCH 13/27] traces with missing axis object references should generate new plots --- NAMESPACE | 1 + R/ggplotly.R | 2 +- R/plotly.R | 7 ++- R/subplots.R | 157 +++++++++++++++++++++++++++++++++++++------------ man/subplot.Rd | 10 +++- 5 files changed, 134 insertions(+), 43 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 91da4e6889..ca6602e736 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -16,6 +16,7 @@ S3method(ggplotly,ggplot) S3method(plotly_build,gg) S3method(plotly_build,plotly_built) S3method(plotly_build,plotly_hash) +S3method(plotly_build,plotly_subplot) S3method(print,figure) S3method(print,plotly_built) S3method(print,plotly_hash) diff --git a/R/ggplotly.R b/R/ggplotly.R index 90b8cc524d..5208120836 100644 --- a/R/ggplotly.R +++ b/R/ggplotly.R @@ -32,7 +32,7 @@ #' ggplotly <- function(p = ggplot2::last_plot(), width = NULL, height = NULL, tooltip = "all", source = "A") { - UseMethod("ggplotly") + UseMethod("ggplotly", p) } #' @export diff --git a/R/plotly.R b/R/plotly.R index a8e8e3a07a..5e2a6eaaa1 100644 --- a/R/plotly.R +++ b/R/plotly.R @@ -254,9 +254,14 @@ plotly_build.plotly_built <- function(l = last_plot()) { l } +#' @export +plotly_build.plotly_subplot <- function(l = last_plot()) { + prefix_class(get_plot(l), "plotly_built") +} + #' @export plotly_build.gg <- function(l = last_plot()) { - structure(get_plot(ggplotly(l)), class = "plotly_built") + prefix_class(get_plot(ggplotly(l)), "plotly_built") } #' @export diff --git a/R/subplots.R b/R/subplots.R index 08c1f124fa..eee7ea5810 100644 --- a/R/subplots.R +++ b/R/subplots.R @@ -31,27 +31,73 @@ subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, shareX = FALSE, shareY = FALSE, margin = 0.02, which_layout = "merge", keep_titles = FALSE) { - # build each plot and collect relevant info - plots <- lapply(list(...), plotly_build) + # build each plot + plotz <- lapply(list(...), plotly_build) + # ensure "axis-reference" trace attributes are properly formatted + # TODO: should this go inside plotly_build()? + plotz <- lapply(plotz, function(p) { + p$data <- lapply(p$data, function(tr) { + if (length(tr[["geo"]])) { + tr[["geo"]] <- sub("^geo1$", "geo", tr[["geo"]][1]) %||% NULL + tr[["xaxis"]] <- NULL + tr[["yaxis"]] <- NULL + } else { + tr[["geo"]] <- NULL + tr[["xaxis"]] <- sub("^x1$", "x", tr[["xaxis"]][1] %||% "x") + tr[["yaxis"]] <- sub("^y1$", "y", tr[["yaxis"]][1] %||% "y") + } + tr + }) + p + }) + # Are any traces referencing "axis-like" layout attributes that are missing? + # If so, move those traces to a "new plot", and inherit layout attributes, + # which makes this sort of thing possible: + # https://plot.ly/r/map-subplots-and-small-multiples/ + plots <- list() + for (i in seq_along(plotz)) { + p <- plots[[i]] <- plotz[[i]] + layoutAttrs <- names(p$layout) + xTraceAttrs <- sub("^x", "xaxis", sapply(p$data, function(tr) tr[["geo"]] %||% tr[["xaxis"]])) + yTraceAttrs <- sub("^y", "yaxis", sapply(p$data, function(tr) tr[["geo"]] %||% tr[["yaxis"]])) + missingAttrs <- setdiff(c(xTraceAttrs, yTraceAttrs), layoutAttrs) + # move to next iteration if trace references are complete + if (!length(missingAttrs)) next + # remove each "missing" trace from this plot + missingTraces <- xTraceAttrs %in% missingAttrs | yTraceAttrs %in% missingAttrs + plots[[i]]$data[missingTraces] <- NULL + # move traces with "similar missingness" to a new plot + for (j in missingAttrs) { + newPlot <- list( + data = p$data[xTraceAttrs %in% j | yTraceAttrs %in% j], + layout = p$layout + ) + # reset the anchors + newPlot$data <- lapply(newPlot$data, function(tr) { + for (k in c("geo", "xaxis", "yaxis")) { + tr[[k]] <- sub("[0-9]+", "", tr[[k]]) %||% NULL + } + tr + }) + plots <- c(plots, list(newPlot)) + } + } + # main plot objects traces <- lapply(plots, "[[", "data") layouts <- lapply(plots, "[[", "layout") shapes <- lapply(layouts, "[[", "shapes") - # keep non axis title annotations annotations <- lapply(layouts, function(x) { + # keep non axis title annotations axes <- vapply(x$annotations, function(a) identical(a$annotationType, "axis"), logical(1)) x$annotations[!axes] }) # collect axis objects - xAxes <- lapply(layouts, function(x) { - x[grepl("^xaxis", names(x))] %||% list(xaxis = list(domain = c(0, 1), anchor = "y")) - }) - yAxes <- lapply(layouts, function(x) { - x[grepl("^yaxis", names(x))] %||% list(yaxis = list(domain = c(0, 1), anchor = "x")) - }) + xAxes <- lapply(layouts, function(lay) lay[grepl("^xaxis|^geo", names(lay))]) + yAxes <- lapply(layouts, function(lay) lay[grepl("^yaxis|^geo", names(lay))]) # remove their titles if (!keep_titles) { - xAxes <- lapply(xAxes, function(x) lapply(x, function(y) { y$title <- NULL; y })) - yAxes <- lapply(yAxes, function(x) lapply(x, function(y) { y$title <- NULL; y })) + xAxes <- lapply(xAxes, function(ax) lapply(ax, function(y) { y$title <- NULL; y })) + yAxes <- lapply(yAxes, function(ax) lapply(ax, function(y) { y$title <- NULL; y })) } # number of x/y axes per plot xAxisN <- vapply(xAxes, length, numeric(1)) @@ -68,14 +114,19 @@ subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, shareX = FALS } else { seq_len(sum(yAxisN)) } - xAxisMap <- setNames( - unlist(lapply(xAxes, names)), - paste0("xaxis", sub("^1$", "", xAxisID)) + # current "axis" names + xCurrentNames <- unlist(lapply(xAxes, names)) + yCurrentNames <- unlist(lapply(yAxes, names)) + xNewNames <- paste0( + sub("[0-9]+$", "", xCurrentNames), + sub("^1$", "", xAxisID) ) - yAxisMap <- setNames( - unlist(lapply(yAxes, names)), - paste0("yaxis", sub("^1$", "", yAxisID)) + yNewNames <- paste0( + sub("[0-9]+$", "", yCurrentNames), + sub("^1$", "", yAxisID) ) + xAxisMap <- setNames(xCurrentNames, xNewNames) + yAxisMap <- setNames(yCurrentNames, yNewNames) # split the map by plot ID xAxisMap <- split(xAxisMap, rep(seq_along(plots), xAxisN)) yAxisMap <- split(yAxisMap, rep(seq_along(plots), yAxisN)) @@ -93,35 +144,64 @@ subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, shareX = FALS xDom <- as.numeric(domainInfo[i, c("xstart", "xend")]) yDom <- as.numeric(domainInfo[i, c("yend", "ystart")]) for (j in seq_along(xAxes[[i]])) { - # before bumping axis anchor, bump trace info, where appropriate + # TODO: support ternary as well! + isGeo <- grepl("^geo", xMap[[j]]) + anchorKey <- if (isGeo) "geo" else "xaxis" traces[[i]] <- lapply(traces[[i]], function(tr) { - tr$xaxis <- tr$xaxis %||% "x" - tr$xaxis[sub("axis", "", xMap[[j]]) %in% tr$xaxis] <- sub("axis", "", names(xMap[j])) + tr[[anchorKey]] <- tr[[anchorKey]] %||% sub("axis", "", anchorKey) + # bump trace anchors, where appropriate + if (sub("axis", "", xMap[[j]]) %in% tr[[anchorKey]]) { + tr[[anchorKey]] <- sub("axis", "", names(xMap[j])) + } tr }) - # bump anchors - map <- yMap[yMap %in% sub("y", "yaxis", xAxes[[i]][[j]]$anchor %||% "y")] - xAxes[[i]][[j]]$anchor <- sub("axis", "", names(map)) - xAxes[[i]][[j]]$domain <- sort(scales::rescale( - xAxes[[i]][[j]]$domain %||% c(0, 1), xDom, from = c(0, 1) - )) + if (isGeo) { + xAxes[[i]][[j]]$domain$x <- sort(scales::rescale( + xAxes[[i]][[j]]$domain$x %||% c(0, 1), xDom, from = c(0, 1) + )) + xAxes[[i]][[j]]$domain$y <- sort(scales::rescale( + xAxes[[i]][[j]]$domain$y %||% c(0, 1), yDom, from = c(0, 1) + )) + } else { + xAxes[[i]][[j]]$domain <- sort(scales::rescale( + xAxes[[i]][[j]]$domain %||% c(0, 1), xDom, from = c(0, 1) + )) + # for cartesian, bump corresponding axis + map <- yMap[yMap %in% sub("y", "yaxis", xAxes[[i]][[j]]$anchor %||% "y")] + xAxes[[i]][[j]]$anchor <- sub("axis", "", names(map)) + } } for (j in seq_along(yAxes[[i]])) { + # TODO: support ternary as well! + isGeo <- grepl("^geo", yMap[[j]]) + anchorKey <- if (isGeo) "geo" else "yaxis" traces[[i]] <- lapply(traces[[i]], function(tr) { - tr$yaxis <- tr$yaxis %||% "y" - tr$yaxis[sub("axis", "", yMap[[j]]) %in% tr$yaxis] <- sub("axis", "", names(yMap[j])) + tr[[anchorKey]] <- tr[[anchorKey]] %||% sub("axis", "", anchorKey) + # bump trace anchors, where appropriate + if (sub("axis", "", yMap[[j]]) %in% tr[[anchorKey]]) { + tr[[anchorKey]] <- sub("axis", "", names(yMap[j])) + } tr }) - map <- xMap[xMap %in% sub("x", "xaxis", yAxes[[i]][[j]]$anchor %||% "x")] - yAxes[[i]][[j]]$anchor <- sub("axis", "", names(map)) - yAxes[[i]][[j]]$domain <- sort(scales::rescale( - yAxes[[i]][[j]]$domain %||% c(0, 1), yDom, from = c(0, 1) - )) + if (isGeo) { + yAxes[[i]][[j]]$domain$x <- sort(scales::rescale( + yAxes[[i]][[j]]$domain$x %||% c(0, 1), xDom, from = c(0, 1) + )) + yAxes[[i]][[j]]$domain$y <- sort(scales::rescale( + yAxes[[i]][[j]]$domain$y %||% c(0, 1), yDom, from = c(0, 1) + )) + } else { + yAxes[[i]][[j]]$domain <- sort(scales::rescale( + yAxes[[i]][[j]]$domain %||% c(0, 1), yDom, from = c(0, 1) + )) + # for cartesian, bump corresponding axis + map <- xMap[xMap %in% sub("x", "xaxis", yAxes[[i]][[j]]$anchor %||% "x")] + yAxes[[i]][[j]]$anchor <- sub("axis", "", names(map)) + } } xAxes[[i]] <- setNames(xAxes[[i]], names(xMap)) yAxes[[i]] <- setNames(yAxes[[i]], names(yMap)) } - # start merging the plots into a single subplot p <- list( data = Reduce(c, traces), @@ -131,7 +211,7 @@ subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, shareX = FALS p$layout$shapes <- Reduce(c, shapes) # merge non-axis layout stuff - layouts <- lapply(layouts, function(x) x[!grepl("^[x-y]axis", names(x))] %||% list()) + layouts <- lapply(layouts, function(x) x[!grepl("^[x-y]axis|^geo", names(x))] %||% list()) if (which_layout != "merge") { if (!is.numeric(which_layout)) warning("which_layout must be numeric") if (!all(idx <- which_layout %in% seq_along(plots))) { @@ -141,7 +221,9 @@ subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, shareX = FALS layouts <- layouts[which_layout] } p$layout <- c(p$layout, Reduce(modifyList, layouts)) - hash_plot(data.frame(), p) + + res <- hash_plot(data.frame(), p) + prefix_class(res, "plotly_subplot") } @@ -160,7 +242,7 @@ get_domains <- function(nplots = 1, nrows = 1, margins = 0.01, stop("The length of the heights argument must be equal ", "to the number of rows", call. = FALSE) } - if (any(widths < 0 | heights < 0)) { + if (any(widths < 0) | any(heights < 0)) { stop("The widths and heights arguments must contain positive values") } if (sum(widths) > 1 | sum(heights) > 1) { @@ -173,7 +255,6 @@ get_domains <- function(nplots = 1, nrows = 1, margins = 0.01, widths <- widths + (1 - max(widths)) / 2 heights <- heights + (1 - max(heights)) / 2 - xs <- vector("list", ncols) for (i in seq_len(ncols)) { xs[[i]] <- c( diff --git a/man/subplot.Rd b/man/subplot.Rd index a093265ce0..470322480a 100644 --- a/man/subplot.Rd +++ b/man/subplot.Rd @@ -5,7 +5,8 @@ \title{View multiple plots in a single view} \usage{ subplot(..., nrows = 1, widths = NULL, heights = NULL, shareX = FALSE, - shareY = FALSE, margin = 0.02, which_layout = "merge") + shareY = FALSE, margin = 0.02, which_layout = "merge", + keep_titles = FALSE) } \arguments{ \item{...}{any number of plotly objects} @@ -30,8 +31,11 @@ fourth is used as the bottom margin. If a single value is provided, it will be used as all four margins.} \item{which_layout}{adopt the layout of which plot? If the default value of -"merge" is used, all plot level layout options will be included in the final -layout. This argument also accepts a numeric vector specifying} +"merge" is used, layout options found later in the sequence of plots will +override options found earlier in the sequence. This argument also accepts a +numeric vector specifying which plots to consider when merging.} + +\item{keep_titles}{should axis titles be retained?} } \value{ A plotly object From 79650d7ac516a6f34be464cba7f4d56ebadadc59 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Thu, 12 May 2016 16:18:54 +1000 Subject: [PATCH 14/27] better defaults; start on a vignette --- R/subplots.R | 10 +++- tests/testthat/test-plotly-subplot.R | 2 +- vignettes/intro.Rmd | 4 +- vignettes/subplot.Rmd | 82 ++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 vignettes/subplot.Rmd diff --git a/R/subplots.R b/R/subplots.R index eee7ea5810..09b328d50f 100644 --- a/R/subplots.R +++ b/R/subplots.R @@ -57,7 +57,7 @@ subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, shareX = FALS plots <- list() for (i in seq_along(plotz)) { p <- plots[[i]] <- plotz[[i]] - layoutAttrs <- names(p$layout) + layoutAttrs <- c(names(p$layout), c("geo", "xaxis", "yaxis")) xTraceAttrs <- sub("^x", "xaxis", sapply(p$data, function(tr) tr[["geo"]] %||% tr[["xaxis"]])) yTraceAttrs <- sub("^y", "yaxis", sapply(p$data, function(tr) tr[["geo"]] %||% tr[["yaxis"]])) missingAttrs <- setdiff(c(xTraceAttrs, yTraceAttrs), layoutAttrs) @@ -92,8 +92,12 @@ subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, shareX = FALS x$annotations[!axes] }) # collect axis objects - xAxes <- lapply(layouts, function(lay) lay[grepl("^xaxis|^geo", names(lay))]) - yAxes <- lapply(layouts, function(lay) lay[grepl("^yaxis|^geo", names(lay))]) + xAxes <- lapply(layouts, function(lay) { + lay[grepl("^xaxis|^geo", names(lay))] %||% list(xaxis = list(domain = c(0, 1))) + }) + yAxes <- lapply(layouts, function(lay) { + lay[grepl("^yaxis|^geo", names(lay))] %||% list(yaxis = list(domain = c(0, 1))) + }) # remove their titles if (!keep_titles) { xAxes <- lapply(xAxes, function(ax) lapply(ax, function(y) { y$title <- NULL; y })) diff --git a/tests/testthat/test-plotly-subplot.R b/tests/testthat/test-plotly-subplot.R index 60fe4dfcdd..f798b54f78 100644 --- a/tests/testthat/test-plotly-subplot.R +++ b/tests/testthat/test-plotly-subplot.R @@ -84,7 +84,7 @@ s <- subplot( ) test_that("Row/column height/width", { - l <- expect_traces(s, 4, "width-height") + l <- expect_traces(s, 3, "width-height") expect_equal(diff(l$layout$xaxis$domain), 0.8 - 0.005) expect_equal(diff(l$layout$xaxis2$domain), 0.2 - 0.005) expect_equal(diff(l$layout$yaxis$domain), 0.2 - 0.005) diff --git a/vignettes/intro.Rmd b/vignettes/intro.Rmd index d3d5ffe61d..a11e46fce2 100644 --- a/vignettes/intro.Rmd +++ b/vignettes/intro.Rmd @@ -36,7 +36,7 @@ You can manually add a trace to an existing plot with `add_trace()`. In that cas ```{r} m <- loess(unemploy / pop ~ as.numeric(date), data = economics) p <- plot_ly(economics, x = date, y = unemploy / pop, name = "raw") -add_trace(p, y = fitted(m), name = "loess") +add_trace(p, x = date, y = fitted(m), name = "loess") ``` __plotly__ was designed with a [pure, predictable, and pipeable interface](https://dl.dropboxusercontent.com/u/41902/pipe-dsls.pdf) in mind, so you can also use the `%>%` operator to create a visualization pipeline: @@ -44,7 +44,7 @@ __plotly__ was designed with a [pure, predictable, and pipeable interface](https ```{r} economics %>% plot_ly(x = date, y = unemploy / pop) %>% - add_trace(y = fitted(m)) %>% + add_trace(x = date, y = fitted(m)) %>% layout(showlegend = F) ``` diff --git a/vignettes/subplot.Rmd b/vignettes/subplot.Rmd new file mode 100644 index 0000000000..28c1b41046 --- /dev/null +++ b/vignettes/subplot.Rmd @@ -0,0 +1,82 @@ +--- +title: "The `subplot()` function" +author: "Carson Sievert" +output: rmarkdown::html_vignette +vignette: > + %\VignetteEngine{knitr::rmarkdown} + %\VignetteIndexEntry{subplot} +--- + +```{r, echo = FALSE} +knitr::opts_chunk$set( + message = FALSE, + warning = FALSE, + fig.width = 4, + fig.height = 2 +) +``` + +The `subplot()` function provides a flexible interface for arranging multiple plots in a single view. There are a few different ways to use `subplot()`, but the simplest way to pass plotly visualization objects directly to `subplot()`. + +```{r, fig.width = 2} +library(plotly) +p1 <- plot_ly(economics, x = date, y = unemploy) +p2 <- plot_ly(economics, x = date, y = uempmed) +subplot(p1, p2) +``` + +This particular subplot could be improved if we share the x-axis so that zooming and panning events are synchronized. It also makes sense to keep x/y titles and remove the redundant legend (since `subplot()` returns a plotly visualization object, any [layout attribute](https://plot.ly/r/reference/#layout) can be altered via `layout()`). + +```{r} +s <- subplot(p1, p2, nrows = 2, shareX = TRUE, keep_titles = TRUE) +layout(s, showlegend = FALSE) +``` + +By default, every subplot is allocated equal spacing, and the margins between them are set somewhat arbitrarily, but that can easily be altered: + +```{r} +map <- plot_ly( + z = state.area, text = state.name, locations = state.abb, + type = 'choropleth', locationmode = 'USA-states', geo = "geo" +) +# specify some map projection/options +g <- list( + scope = 'usa', + projection = list(type = 'albers usa'), + lakecolor = toRGB('white') +) +map <- layout(map, geo = g) +subplot( + plot_ly(x = state.name, y = state.area, type = "bar"), map, + nrows = 2, margin = c(0, 0, 0.25, 0), heights = c(0.2, 0.8) +) +``` + +In addition to a `heights` argument for controlling the proportional height of each row, there is also a `widths` argument for controlling the proportional width of each column: + +```{r} +x <- rnorm(100) +y <- rnorm(100) +m <- list(color = "black") +subplot( + plot_ly(x = x, type = "histogram", marker = m), + plotly_empty(), + plot_ly(x = x, y = y, mode = "markers", marker = m), + plot_ly(y = y, type = "histogram", marker = m), + nrows = 2, heights = c(0.2, 0.8), widths = c(0.8, 0.2), + shareX = TRUE, shareY = TRUE +) %>% layout(showlegend = FALSE) +``` + +The `subplot()` function also understands ggplot2 objects and converts them via `ggplotly()`. + +```{r} +e <- tidyr::gather(economics, variable, value, -date) +gg1 <- ggplot(e, aes(date, value)) + geom_line() + + facet_wrap(~variable, scales = "free_y", ncol = 1) +gg2 <- ggplot(e, aes(factor(1), value)) + geom_violin() + + facet_wrap(~variable, scales = "free_y", ncol = 1) + + theme(axis.text.x = element_blank()) +subplot(gg1, gg2) +``` + From dee87c2fac7a84320f06d1617482bc617f882ada Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Thu, 12 May 2016 16:44:54 +1000 Subject: [PATCH 15/27] Remove keep_titles in favor of titleX/titleY --- R/subplots.R | 18 ++++++++++-------- man/subplot.Rd | 20 +++++++++++--------- tests/testthat/test-plotly-subplot.R | 1 - vignettes/subplot.Rmd | 15 +++++++++------ 4 files changed, 30 insertions(+), 24 deletions(-) diff --git a/R/subplots.R b/R/subplots.R index 09b328d50f..539d282c03 100644 --- a/R/subplots.R +++ b/R/subplots.R @@ -7,14 +7,15 @@ #' columns have an equal relative width. #' @param heights relative height of each row on a 0-1 scale. By default all #' rows have an equal relative height. -#' @param shareX should the x-axis be shared amongst the subplots? -#' @param shareY should the y-axis be shared amongst the subplots? #' @param margin either a single value or four values (all between 0 and 1). #' If four values are provided, the first is used as the left margin, the second #' is used as the right margin, the third is used as the top margin, and the #' fourth is used as the bottom margin. #' If a single value is provided, it will be used as all four margins. -#' @param keep_titles should axis titles be retained? +#' @param shareX should the x-axis be shared amongst the subplots? +#' @param shareY should the y-axis be shared amongst the subplots? +#' @param titleX should x-axis titles be retained? +#' @param titleY should y-axis titles be retained? #' @param which_layout adopt the layout of which plot? If the default value of #' "merge" is used, layout options found later in the sequence of plots will #' override options found earlier in the sequence. This argument also accepts a @@ -28,9 +29,9 @@ #' subplot(p1, p2, p1, p2, nrows = 2) #' } -subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, shareX = FALSE, - shareY = FALSE, margin = 0.02, which_layout = "merge", - keep_titles = FALSE) { +subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, margin = 0.02, + shareX = FALSE, shareY = FALSE, titleX = shareX, + titleY = shareY, which_layout = "merge") { # build each plot plotz <- lapply(list(...), plotly_build) # ensure "axis-reference" trace attributes are properly formatted @@ -98,9 +99,10 @@ subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, shareX = FALS yAxes <- lapply(layouts, function(lay) { lay[grepl("^yaxis|^geo", names(lay))] %||% list(yaxis = list(domain = c(0, 1))) }) - # remove their titles - if (!keep_titles) { + if (!titleX) { xAxes <- lapply(xAxes, function(ax) lapply(ax, function(y) { y$title <- NULL; y })) + } + if (!titleY) { yAxes <- lapply(yAxes, function(ax) lapply(ax, function(y) { y$title <- NULL; y })) } # number of x/y axes per plot diff --git a/man/subplot.Rd b/man/subplot.Rd index 470322480a..e76fbbe50a 100644 --- a/man/subplot.Rd +++ b/man/subplot.Rd @@ -4,9 +4,9 @@ \alias{subplot} \title{View multiple plots in a single view} \usage{ -subplot(..., nrows = 1, widths = NULL, heights = NULL, shareX = FALSE, - shareY = FALSE, margin = 0.02, which_layout = "merge", - keep_titles = FALSE) +subplot(..., nrows = 1, widths = NULL, heights = NULL, margin = 0.02, + shareX = FALSE, shareY = FALSE, titleX = shareX, titleY = shareY, + which_layout = "merge") } \arguments{ \item{...}{any number of plotly objects} @@ -20,22 +20,24 @@ columns have an equal relative width.} \item{heights}{relative height of each row on a 0-1 scale. By default all rows have an equal relative height.} -\item{shareX}{should the x-axis be shared amongst the subplots?} - -\item{shareY}{should the y-axis be shared amongst the subplots?} - \item{margin}{either a single value or four values (all between 0 and 1). If four values are provided, the first is used as the left margin, the second is used as the right margin, the third is used as the top margin, and the fourth is used as the bottom margin. If a single value is provided, it will be used as all four margins.} +\item{shareX}{should the x-axis be shared amongst the subplots?} + +\item{shareY}{should the y-axis be shared amongst the subplots?} + +\item{titleX}{should x-axis titles be retained?} + +\item{titleY}{should y-axis titles be retained?} + \item{which_layout}{adopt the layout of which plot? If the default value of "merge" is used, layout options found later in the sequence of plots will override options found earlier in the sequence. This argument also accepts a numeric vector specifying which plots to consider when merging.} - -\item{keep_titles}{should axis titles be retained?} } \value{ A plotly object diff --git a/tests/testthat/test-plotly-subplot.R b/tests/testthat/test-plotly-subplot.R index f798b54f78..1843910df0 100644 --- a/tests/testthat/test-plotly-subplot.R +++ b/tests/testthat/test-plotly-subplot.R @@ -68,7 +68,6 @@ test_that("share both axes", { }) # https://github.com/ropensci/plotly/issues/376 -library(plotly) d <- data.frame( x = rnorm(100), y = rnorm(100) diff --git a/vignettes/subplot.Rmd b/vignettes/subplot.Rmd index 28c1b41046..2235635a2c 100644 --- a/vignettes/subplot.Rmd +++ b/vignettes/subplot.Rmd @@ -18,17 +18,17 @@ knitr::opts_chunk$set( The `subplot()` function provides a flexible interface for arranging multiple plots in a single view. There are a few different ways to use `subplot()`, but the simplest way to pass plotly visualization objects directly to `subplot()`. -```{r, fig.width = 2} +```{r} library(plotly) p1 <- plot_ly(economics, x = date, y = unemploy) p2 <- plot_ly(economics, x = date, y = uempmed) subplot(p1, p2) ``` -This particular subplot could be improved if we share the x-axis so that zooming and panning events are synchronized. It also makes sense to keep x/y titles and remove the redundant legend (since `subplot()` returns a plotly visualization object, any [layout attribute](https://plot.ly/r/reference/#layout) can be altered via `layout()`). +This particular subplot could be improved if we share the x-axis so that zooming and panning events are synchronized. It also makes sense to keep axis titles and remove the redundant legend (since `subplot()` returns a plotly visualization object, any [layout attribute](https://plot.ly/r/reference/#layout) can be altered via `layout()`). ```{r} -s <- subplot(p1, p2, nrows = 2, shareX = TRUE, keep_titles = TRUE) +s <- subplot(p1, p2, nrows = 2, shareX = TRUE, titleY = TRUE) layout(s, showlegend = FALSE) ``` @@ -70,13 +70,16 @@ subplot( The `subplot()` function also understands ggplot2 objects and converts them via `ggplotly()`. -```{r} +```{r, fig.height = 8} e <- tidyr::gather(economics, variable, value, -date) gg1 <- ggplot(e, aes(date, value)) + geom_line() + facet_wrap(~variable, scales = "free_y", ncol = 1) gg2 <- ggplot(e, aes(factor(1), value)) + geom_violin() + facet_wrap(~variable, scales = "free_y", ncol = 1) + - theme(axis.text.x = element_blank()) -subplot(gg1, gg2) + theme(axis.text = element_blank(), axis.ticks = element_blank()) +subplot(gg1, gg2) %>% layout(margin = list(l = 50)) ``` + + + From f342c0b8a53d90a134333d03534417e1e79d3d90 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Thu, 12 May 2016 16:45:31 +1000 Subject: [PATCH 16/27] better sizing defaults --- R/print.R | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/R/print.R b/R/print.R index 355b79a39f..5539a6e6c5 100644 --- a/R/print.R +++ b/R/print.R @@ -64,7 +64,9 @@ as.widget <- function(x, ...) { height = x$height, sizingPolicy = htmlwidgets::sizingPolicy( padding = 5, - browser.fill = TRUE + browser.fill = TRUE, + defaultWidth = '100%', + defaultHeight = 400 ), ... ) From ab93d5a398d870b6eff48e04a11976f658f2c4f1 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Sat, 14 May 2016 11:52:18 +1000 Subject: [PATCH 17/27] subplot now accepts a list of plots --- R/subplots.R | 11 ++++++++--- R/utils.R | 4 +++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/R/subplots.R b/R/subplots.R index 539d282c03..7440347e54 100644 --- a/R/subplots.R +++ b/R/subplots.R @@ -32,8 +32,13 @@ subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, margin = 0.02, shareX = FALSE, shareY = FALSE, titleX = shareX, titleY = shareY, which_layout = "merge") { + # are the dots a list of plotly objects? + dotz <- list(...) + if (length(dotz) == 1 && is.list(dotz) && !is.plotly(dotz)) { + dotz <- dotz[[1]] + } # build each plot - plotz <- lapply(list(...), plotly_build) + plotz <- lapply(dotz, plotly_build) # ensure "axis-reference" trace attributes are properly formatted # TODO: should this go inside plotly_build()? plotz <- lapply(plotz, function(p) { @@ -245,8 +250,8 @@ get_domains <- function(nplots = 1, nrows = 1, margins = 0.01, "to the number of columns", call. = FALSE) } if (length(heights) != nrows) { - stop("The length of the heights argument must be equal ", - "to the number of rows", call. = FALSE) + stop("The length of the heights argument is ", length(heights), + ", but the number of rows is ", nrows, call. = FALSE) } if (any(widths < 0) | any(heights < 0)) { stop("The widths and heights arguments must contain positive values") diff --git a/R/utils.R b/R/utils.R index 06eccf9dd1..ab838f958a 100644 --- a/R/utils.R +++ b/R/utils.R @@ -1,4 +1,6 @@ -is.plotly <- function(x) inherits(x, "plotly") +is.plotly <- function(x) { + inherits(x, c("plotly_hash", "plotly_built", "plotly_subplot")) +} "%||%" <- function(x, y) { if (length(x) > 0 || is_blank(x)) x else y From 81bcdd5b84daad7ca23c49009ce957a73845bdbc Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Sat, 14 May 2016 14:57:41 +1000 Subject: [PATCH 18/27] don't draw blank or interior facet strip in grid layout; add ... argument to ggplotly() --- R/ggplotly.R | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/R/ggplotly.R b/R/ggplotly.R index 5208120836..883a6f3d62 100644 --- a/R/ggplotly.R +++ b/R/ggplotly.R @@ -31,13 +31,13 @@ #' } #' ggplotly <- function(p = ggplot2::last_plot(), width = NULL, height = NULL, - tooltip = "all", source = "A") { + tooltip = "all", source = "A", ...) { UseMethod("ggplotly", p) } #' @export ggplotly.ggmatrix <- function(p = ggplot2::last_plot(), width = NULL, - height = NULL, tooltip = "all", source = "A") { + height = NULL, tooltip = "all", source = "A", ...) { plotList <- list() for (i in seq_len(p$nrow)) { for (j in seq_len(p$ncol)) { @@ -47,14 +47,14 @@ ggplotly.ggmatrix <- function(p = ggplot2::last_plot(), width = NULL, # TODO: # (1) how to show x/y titles? Should these be arguments in subplot? # (2) it only makes since to share axes on the lower diagonal - l <- get_plot(do.call(subplot, c(plotList, list(nrows = p$nrow)))) + l <- get_plot(do.call(subplot, c(plotList, list(nrows = p$nrow, ...)))) l$layout$title <- p$title hash_plot(p$data, l) } #' @export ggplotly.ggplot <- function(p = ggplot2::last_plot(), width = NULL, - height = NULL, tooltip = "all", source = "A") { + height = NULL, tooltip = "all", source = "A", ...) { l <- gg2list(p, width = width, height = height, tooltip = tooltip, source = source) hash_plot(p$data, l) } @@ -67,9 +67,10 @@ ggplotly.ggplot <- function(p = ggplot2::last_plot(), width = NULL, #' tooltip. The default, "all", means show all the aesthetic tooltips #' (including the unofficial "text" aesthetic). #' @param source Only relevant for \link{event_data}. +#' @param ... currently not used #' @return a 'built' plotly object (list with names "data" and "layout"). #' @export -gg2list <- function(p, width = NULL, height = NULL, tooltip = "all", source = "A") { +gg2list <- function(p, width = NULL, height = NULL, tooltip = "all", source = "A", ...) { # ------------------------------------------------------------------------ # Our internal version of ggplot2::ggplot_build(). Modified from # https://github.com/hadley/ggplot2/blob/0cd0ba/R/plot-build.r#L18-L92 @@ -469,6 +470,8 @@ gg2list <- function(p, width = NULL, height = NULL, tooltip = "all", source = "A col_txt <- paste( p$facet$labeller(lay[names(p$facet[[col_vars]])]), collapse = ", " ) + if (is_blank(theme[["strip.text.x"]])) col_txt <- "" + if (inherits(p$facet, "grid") && lay$ROW != 1) col_txt <- "" if (nchar(col_txt) > 0) { col_lab <- make_label( col_txt, x = mean(xdom), y = max(ydom), @@ -482,6 +485,8 @@ gg2list <- function(p, width = NULL, height = NULL, tooltip = "all", source = "A row_txt <- paste( p$facet$labeller(lay[names(p$facet$rows)]), collapse = ", " ) + if (is_blank(theme[["strip.text.y"]])) row_txt <- "" + if (inherits(p$facet, "grid") && lay$COL != nCols) row_txt <- "" if (nchar(row_txt) > 0) { row_lab <- make_label( row_txt, x = max(xdom), y = mean(ydom), From 0020348b1a4749aacb50ac62c49a052641f716fc Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Sat, 14 May 2016 16:32:52 +1000 Subject: [PATCH 19/27] add to vignette; better check for list of plots --- R/subplots.R | 2 +- man/gg2list.Rd | 5 +- man/ggplotly.Rd | 2 +- vignettes/intro.Rmd | 43 ------------ vignettes/proportions.svg | 2 + vignettes/proportions.xml | 1 + vignettes/subplot.Rmd | 134 +++++++++++++++++++++++++++++--------- vignettes/subplot.png | Bin 0 -> 5989 bytes vignettes/subplot.svg | 2 + vignettes/subplot.xml | 1 + 10 files changed, 114 insertions(+), 78 deletions(-) create mode 100644 vignettes/proportions.svg create mode 100644 vignettes/proportions.xml create mode 100644 vignettes/subplot.png create mode 100644 vignettes/subplot.svg create mode 100644 vignettes/subplot.xml diff --git a/R/subplots.R b/R/subplots.R index 7440347e54..cb73d15836 100644 --- a/R/subplots.R +++ b/R/subplots.R @@ -34,7 +34,7 @@ subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, margin = 0.02 titleY = shareY, which_layout = "merge") { # are the dots a list of plotly objects? dotz <- list(...) - if (length(dotz) == 1 && is.list(dotz) && !is.plotly(dotz)) { + if (length(dotz) == 1 && is.list(dotz[[1]]) && !is.plotly(dotz[[1]])) { dotz <- dotz[[1]] } # build each plot diff --git a/man/gg2list.Rd b/man/gg2list.Rd index d125d1f699..4104a0e6db 100644 --- a/man/gg2list.Rd +++ b/man/gg2list.Rd @@ -4,7 +4,8 @@ \alias{gg2list} \title{Convert a ggplot to a list.} \usage{ -gg2list(p, width = NULL, height = NULL, tooltip = "all", source = "A") +gg2list(p, width = NULL, height = NULL, tooltip = "all", source = "A", + ...) } \arguments{ \item{p}{ggplot2 plot.} @@ -18,6 +19,8 @@ tooltip. The default, "all", means show all the aesthetic tooltips (including the unofficial "text" aesthetic).} \item{source}{Only relevant for \link{event_data}.} + +\item{...}{currently not used} } \value{ a 'built' plotly object (list with names "data" and "layout"). diff --git a/man/ggplotly.Rd b/man/ggplotly.Rd index 1df2e6c8f4..be0b7d66ff 100644 --- a/man/ggplotly.Rd +++ b/man/ggplotly.Rd @@ -5,7 +5,7 @@ \title{Create plotly graphs using ggplot2 syntax} \usage{ ggplotly(p = ggplot2::last_plot(), width = NULL, height = NULL, - tooltip = "all", source = "A") + tooltip = "all", source = "A", ...) } \arguments{ \item{p}{a ggplot object.} diff --git a/vignettes/intro.Rmd b/vignettes/intro.Rmd index a11e46fce2..8f0a3280f7 100644 --- a/vignettes/intro.Rmd +++ b/vignettes/intro.Rmd @@ -161,46 +161,3 @@ To change the default symbols used, use the symbols argument. All the valid symb plot_ly(iris, x = Petal.Length, y = Petal.Width, mode = "markers", symbol = Species, symbols = c("cross", "square", "triangle-down")) ``` - - -### The group argument and `subplot()` - -Using the group argument splits the data into different plotly "traces". - -```{r} -plot_ly(iris, x = Petal.Length, y = Petal.Width, - group = Species, mode = "markers") -``` - -Although we haven't specified a coloring scheme, plotly will employ one on it's own default scheme. The group argument is quite powerful when used in conjunction with `subplot()` in order to anchor traces onto different axes. - -```{r} -iris$id <- as.integer(iris$Species) -p <- plot_ly(iris, x = Petal.Length, y = Petal.Width, group = Species, - xaxis = paste0("x", id), mode = "markers") -subplot(p) -``` - -Since `subplot()` does not assume x/y axes are on a common scale, it does not impose any restrictions on the range by default. However, you can change this by pre-specifying the range of the [axis objects](https://plot.ly/r/reference/#xaxis) via the `layout()` function. - -```{r} -p2 <- layout( - p, - xaxis = list(range = range(Petal.Length) + c(-0.1, 0.1)), - yaxis = list(range = range(Petal.Width) + c(-0.1, 0.1)) -) -subplot(p2) -``` - -Part of the magic of `subplot()` is that it generates axis objects with appropriate anchor and domain properties. After generating a subplot, you can always reference these axis objects to customize each plot. - -```{r} -layout( - subplot(p2), - yaxis2 = list(title = ""), - yaxis3 = list(title = "") -) -``` - - -[See here](https://plot.ly/r/map-subplots-and-small-multiples/) for another example of using the group argument to make small multiples (with maps!). diff --git a/vignettes/proportions.svg b/vignettes/proportions.svg new file mode 100644 index 0000000000..969f049d0e --- /dev/null +++ b/vignettes/proportions.svg @@ -0,0 +1,2 @@ + +
nrows = 2,
heights = c(.4, .6),
widths = c(1/4, 1/4, 1/2)
[Not supported by viewer]
1
1
2
2
3
3
4
4
5
5
1
1
2
2
3
3
4
4
5
5
\ No newline at end of file diff --git a/vignettes/proportions.xml b/vignettes/proportions.xml new file mode 100644 index 0000000000..19ec767f68 --- /dev/null +++ b/vignettes/proportions.xml @@ -0,0 +1 @@ +7VpLb+MgEP41kXYvkR9pHsemr72sVKmH3T1SQ2xUYiLsNOn++g72YGPjtpGaxtF6c4jggxkm3wwwQEbh1Xp/p8gm+SkpE6PAo/tReD0Kgtl8Ad8aeCmBi9AvgVhxWkIW8MD/MgQ9RLecsqzRMZdS5HzTBCOZpizKGxhRSu6a3VZSNEfdkNiMWAMPEREu+ovTPEF0juZp/AfjcYIjBx42PJLoKVZym+JwoyBcFZ+yeU2MKuyfJYTKnQWFN8CqkhIU69J6f8WEZtawVsrdvtFama1Yiqa9LwBNWuCZiC3+crQrfzFU7BKes4cNiXR9B94ehcskXwuo+VBccSGupJAK6qlModMSdTKVM4yHDrsKCI26Y3LNcvUCXVAgCEsJDB9D1672hYGShhswAtD7caW3pgAKyEI3I7MzZWQy74sR/+JMKfGbQeKbxcPixJ92kGJm8adImX5MiuDFL6VcwRLFZQpoJrfatA+4yXIln6qFR3eiJEuYHlpXjh9NndR9FXMHzLAzZm6Oju+DOXTaAcxZLDUIcPk5zdQMcQ35aGoa6j5Dk1nzLJrSIiOABl3zoBGUTAWMuXxUUIp1ySDZhqQGK02rJaNv40kh7I2n3y0tYJIt9rZyyp8NVFBhqwZGb1F7qxjAWNVAlgrH++Cwwuza+6XDWw7u8DkRPNbTLAIHM8CX2v0cEqJLbFhzSvUwy66lXkLvlSgSmQT6MRA4+sQLMIRMRtkx7wxmR5TBPhVR4Iuz3AFnaNjpkwLjjbOjZIEnjx4oOSAl6CdP6i+bPkI6fbItqzmXuvb1EGPLZmlyDJbcjR3NH8ra3l+Aokst6t0I/Zep7+9cGbp5GobBQKjvb/c2t3EW9WjLQKjvL0sI3Ss3TKUGQn2P2UiII//fZ83lhXss/zLu3TPUsDbaaY/cu4e1Ye207TXnpOS7x8Jh7bVt7jtubr6Me/f4OazNtr3onJD7yQGXZvD8udHFlWD7S/1sC7+apRSL15EgWcajt+9I6udVLQcG/gbQG3uzCwP8KQG4j0bgnikOv0W76xquXpdsz3MjtcC6FvLHHsRKWXdkUlrdR2Adn7DnY29eeY5R55G55TcgQ25VERTWZUlOVMxMtyJeXf9aDnzv1lMxQXL+3DSjy6s4wr3kYGC9bqIaDB9zY28UlMajjP3K3FJj5FpXK0ZN+XsdNUWAVT+5K+agWj+Vl93rfyOEN68= \ No newline at end of file diff --git a/vignettes/subplot.Rmd b/vignettes/subplot.Rmd index 2235635a2c..f6f2081c7b 100644 --- a/vignettes/subplot.Rmd +++ b/vignettes/subplot.Rmd @@ -1,7 +1,10 @@ --- title: "The `subplot()` function" author: "Carson Sievert" -output: rmarkdown::html_vignette +output: + rmarkdown::html_vignette: + toc: true + standalone: false vignette: > %\VignetteEngine{knitr::rmarkdown} %\VignetteIndexEntry{subplot} @@ -10,63 +13,121 @@ vignette: > ```{r, echo = FALSE} knitr::opts_chunk$set( message = FALSE, - warning = FALSE, - fig.width = 4, - fig.height = 2 + warning = FALSE, + comment = "#>", + fig.width = 7 ) ``` -The `subplot()` function provides a flexible interface for arranging multiple plots in a single view. There are a few different ways to use `subplot()`, but the simplest way to pass plotly visualization objects directly to `subplot()`. +## Introduction + +The `subplot()` function provides a flexible interface for arranging multiple **plotly** plots in a single view. There are a few different ways to use `subplot()`, but the simplest way to pass plotly visualization objects directly to `subplot()`. ```{r} library(plotly) -p1 <- plot_ly(economics, x = date, y = unemploy) -p2 <- plot_ly(economics, x = date, y = uempmed) +p1 <- plot_ly(economics, x = date, y = unemploy, name = "unemploy") +p2 <- plot_ly(economics, x = date, y = uempmed, name = "uempmed") subplot(p1, p2) ``` -This particular subplot could be improved if we share the x-axis so that zooming and panning events are synchronized. It also makes sense to keep axis titles and remove the redundant legend (since `subplot()` returns a plotly visualization object, any [layout attribute](https://plot.ly/r/reference/#layout) can be altered via `layout()`). +Although `subplot()` accepts an arbitrary number of plot objects, passing a _list_ of plots can save typing and redundant code when dealing with a large number of plots. To demonstrate, let's create one time series for each variable in the `economics` dataset and share the x-axis so that zoom/pan events are synchronized across each series: -```{r} -s <- subplot(p1, p2, nrows = 2, shareX = TRUE, titleY = TRUE) -layout(s, showlegend = FALSE) +```{r, fig.height = 5} +vars <- setdiff(names(economics), "date") +plots <- lapply(vars, function(var) { + plot_ly(x = economics$date, y = economics[[var]], name = var) +}) +subplot(plots, nrows = length(plots), shareX = TRUE, titleX = FALSE) ``` -By default, every subplot is allocated equal spacing, and the margins between them are set somewhat arbitrarily, but that can easily be altered: +```{r, echo = FALSE, eval = FALSE} +# this works too, but I'm not sure we should advertise... +elong <- tidyr::gather(economics, variable, value, -date) +elong$id <- as.integer(factor(elong$variable)) +p <- plot_ly(elong, x = date, y = value, group = variable, yaxis = paste0("y", id)) +subplot(p, nrows = 5, shareX = TRUE) +``` + +Conceptually, `subplot()` provides a way to place a collection of plots into a table with a given number of rows and columns. The number of rows (and, by consequence, the number of columns) is specified via the `nrows` argument. By default each row/column shares an equal proportion of the overall height/width, but as shown in the diagram below, that default can be changed via the `heights` and `widths` arguments. + +
+ +
+ +This can be quite useful for a number of visualizations, for example, a joint density plot: ```{r} -map <- plot_ly( - z = state.area, text = state.name, locations = state.abb, - type = 'choropleth', locationmode = 'USA-states', geo = "geo" +x <- rnorm(100) +y <- rnorm(100) +m <- list(color = "black") +s <- subplot( + plot_ly(x = x, type = "histogram", marker = m), + plotly_empty(), + plot_ly(x = x, y = y, mode = "markers", marker = m), + plot_ly(y = y, type = "histogram", marker = m), + nrows = 2, heights = c(0.2, 0.8), widths = c(0.8, 0.2), + shareX = TRUE, shareY = TRUE, titleX = FALSE, titleY = FALSE ) +layout(s, showlegend = FALSE) +``` + +Note that, since `subplot()` returns a plotly object, any [layout attribute](https://plot.ly/r/reference/#layout) can be modified downstream via `layout()`. + +## Recursive subplots + +The `subplot()` function is designed to work recursively so that you can have subplots of subplots. This idea is useful when your desired layout doesn't conform to the table structure described in the previous section. In fact, you can think of a subplot of subplots like an excel spreadsheet with merged cells. + + +
+ +
+ +```{r, fig.height = 5} +plotList <- function(nplots) { + # TODO: use new images infrastructure to overlay an R image on each plot + lapply(seq_len(nplots), function(x) plot_ly()) +} +s1 <- subplot(plotList(6), nrows = 2, shareX = TRUE, shareY = TRUE) +s2 <- subplot(plotList(2), shareY = TRUE) +subplot(s1, s2, plot_ly(), nrows = 3, margin = 0.04, heights = c(0.6, 0.3, 0.1)) +``` + + + + +```{r, fig.height = 6} # specify some map projection/options g <- list( scope = 'usa', projection = list(type = 'albers usa'), lakecolor = toRGB('white') ) -map <- layout(map, geo = g) +# create a map of population density +density <- state.x77[, "Population"] / state.x77[, "Area"] +map <- plot_ly( + z = density, + text = state.name, locations = state.abb, + type = 'choropleth', locationmode = 'USA-states', geo = "geo" +) %>% layout(geo = g) +# create a bunch of horizontal bar charts +vars <- colnames(state.x77) +barcharts <- lapply(vars, function(var) { + plot_ly(x = state.x77[, var], y = state.name, type = "bar", + orientation = "h", name = var) %>% + layout(showlegend = FALSE, hovermode = "y", + yaxis = list(showticklabels = FALSE)) +}) subplot( - plot_ly(x = state.name, y = state.area, type = "bar"), map, - nrows = 2, margin = c(0, 0, 0.25, 0), heights = c(0.2, 0.8) + subplot(barcharts, margin = 0.01), map, + nrows = 2, heights = c(0.3, 0.7) ) ``` -In addition to a `heights` argument for controlling the proportional height of each row, there is also a `widths` argument for controlling the proportional width of each column: -```{r} -x <- rnorm(100) -y <- rnorm(100) -m <- list(color = "black") -subplot( - plot_ly(x = x, type = "histogram", marker = m), - plotly_empty(), - plot_ly(x = x, y = y, mode = "markers", marker = m), - plot_ly(y = y, type = "histogram", marker = m), - nrows = 2, heights = c(0.2, 0.8), widths = c(0.8, 0.2), - shareX = TRUE, shareY = TRUE -) %>% layout(showlegend = FALSE) -``` +## subplots with ggplot2 + The `subplot()` function also understands ggplot2 objects and converts them via `ggplotly()`. @@ -80,6 +141,15 @@ gg2 <- ggplot(e, aes(factor(1), value)) + geom_violin() + subplot(gg1, gg2) %>% layout(margin = list(l = 50)) ``` +The new `subplot()` infrastructure allows `ggplotly()` to understand ggmatrix +objects -- the class of object returned by the `ggpairs()` function in the +**GGally** package. + +```{r} +pm <- GGally::ggpairs(reshape::tips[, 1:3]) +ggplotly(pm, margin = 0.04) +``` + diff --git a/vignettes/subplot.png b/vignettes/subplot.png new file mode 100644 index 0000000000000000000000000000000000000000..a51f055472dd42ca9354ca0e2e4fec8eebcc2da2 GIT binary patch literal 5989 zcmeHLSyWT$nhh}*p%h}GUMN8WAwj?ZiUS}RPyzwMpgQ3vWm_=P+3EAUAjlwR$3vmaBsX-u1~Zif=}An=`TdETPM zp)B#^N#b}A+%`s#uk53l8@~URnL)pvx5v59r$O=W6zYY)he3n`9*YS6yPbPuGxT0y&tAuq=0e7IX zYEMIcim$7;g5^tAkDDo~xAkdI^8MShpixWb5S{+r3a_7aH=Q5jUcNIDsYSauY$I5D z@kgOmr(SbyUvl4)CH;U!JDV8S6CXG!nK@<+3O0Twy($f7ch-rltpwrzk{^?8 zK4z^3k*tDrVC?y)7I=+;U&h&rxKq2mO3H)qu&KL+4CDBr(i>&kOGk<=?%pl*Q;JNN zYvOcco=hH^^z`srSR5fu+cs`wE;_edmai^;TZWD6Rob7p6hX~dj2#rKb^&W^jf(%R{|M!Umx9{BE2%zMOCYHZW$`E=dz zVae41tX9)(L0|r}ES2{T=C#_wUpq}k*;o8S-f55!os%zFTh<$KTwr{K6qpW9|xTPT@Z>@%!U%rAQ z5{Xy)??F)I_qnST%cA16LpkMMH@7+ zp4$%Hf?${`wjn5aJ0NuI*ke!O)#@^Y(OXWcHsCvQ{f}@P$sf$}S~@w9?CaO_vVR~S zoGsWG6!mLU{KG4oPOJX6kpdSD>DC^ZxLJ_0rXvc=@+B8bl;%@6RHxEf2z>Yi?29H# zP`9?760;l#>?KiMG?4tCsj@}bbM-myw14r!bDY{Sq3KWbT8rt`_GTc-Fw8c5#tJc3 z?z5l7%n!(~6EPGP?;nQdgj&yd0>_Dbko+y0zQyymSJeOZH61~sU!TSrZj$QAm*?D5 zB^zgCL}8gB83L2MbyA&&{_BUYPudgA!586?RZtA`NPuYIza)km@{YgplmB`61e54? zvBOV+EtS-^rU0QrSKgrp26_Nrvyarev`GFE%<`09=cb1s;6AP6hDMG#;-?ZMVjeS4RfXHwdiVgNY2rDUV4WHHY{x%2QY00p5iZQ-LX29?% zA4h3}hK-#Qg^Zb-1?xZTlmiB2?&zqBiNmr3Hs>4j%%*5{BXr^p;Fs%~E zb>KBeZou*Z{Hu#MlKsAD)HFCLE^IH!LiGlkJtK=@`utJmmWgE_37UKCqm4lGNZxU3utca7?lJt930hrND4qZ zQ)ccg;(MbkM;U2*IkhoA#}53;2`qp0A@Dd~e zc@B^lzYn}Z%RE9wPTCL075AK zbH*8|MfZJ0WZ%KEp{a8h4{l39(Oj?s6Z2j!)U4M_2b_Dae~*D>?{b|#BB4n;EcBB# z%n|n!g_cr9^nVcQ7CmIl{F7k(ZhJ}I{k53MW2=#9jsw(iaJ8o> zbW9B`1kG+~|LIdqorl@nV$AKW<9E0hIKt?;>19&gms?r%o>H_LEeI@Jp`k|xtj)m^ z3od*?4LJKJQPEDr$1yU%`FxQ2H$DLjEx}8Rr1iJeg8PV1Q~w-P2RbwMByR1BMBH!D zXHp?nd&+TEzMtA$Z4WCS-79fWKZ1 z`^y`y47`56f7qms4km&x(APM>M-d-7Xv(B>{U{ZrrJ$)MpJ)OYzJ{R{NOBuFxYQFL zf4;c#8KowZPDP*VTGExr0KtpNm}`cs0WDGXf^_aV;4pA*@)_n^$v0m9U&R3I_%Pm6 z&2TsT&@ySW`YYwbxYU}dr`j21V&HR~rvhMylxs?NxyFE1eytY&2KfZo=u=7f1^H~i ztHudW%548;5lvw0-8!zPBO~t#sh6;q%_tQ>oLT@U=F?z_tmQ!(b1h`P4)}Zyvaxi) JJv-+a`!6suG0y-1 literal 0 HcmV?d00001 diff --git a/vignettes/subplot.svg b/vignettes/subplot.svg new file mode 100644 index 0000000000..6acc8168dd --- /dev/null +++ b/vignettes/subplot.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/vignettes/subplot.xml b/vignettes/subplot.xml new file mode 100644 index 0000000000..7e0fb040fb --- /dev/null +++ b/vignettes/subplot.xml @@ -0,0 +1 @@ +zZfPc6sgEMf/Gu9RTGKuTdP00lMOPfOUKPPQdQipSf/6YFz8UW2nU1595JCBLwvsfmABPbLNL3tJy+wFEia8YJFcPPLoBcE62uj/Wrg2QhitGyGVPGkkvxMO/J2huED1zBN2GhgqAKF4ORRjKAoWq4FGpYRqaHYEMZy1pKmZsRMOMRVj9ZUnKkN1uez0Z8bTDGcOF+j3Hxr/TSWcC5zOC8jx/muac2qGQvtTRhOoehLZaaoSQA9cl/LLlomarKHW9Hv6pLV1W7ICXfu6g26qO7xRccbI0S91NSiqjCt2KGlc1yu92h55yFQudM3XRRyAScVw8SecuEvowZ5BzpS8ahPssEQYuFcIVqsO/BqlrMd8hRrFpU7bcbt4dQFDng6fuBC+bwKeP/7QhfjbpJ8/fkxnp7a/b86SGeJfubj/5wSAt5JbCTAngMgawJELsQUBUtcLKLTRv2ASDpGY+7JHpM2bPhLfiDZM8O3iOhM/+i4Us79soJgxnKPy8fjcjKm0APpUjJ0VFPTWOSi+OS/+CxX7Z9UsCUQmztnJBDKJZgXF/q3xO1BGt++YydTlQ34ARVe7T5t7W+/rkexu \ No newline at end of file From 8b15c000274fccf99eaddb1d91faf000984b5eca Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Mon, 16 May 2016 12:51:37 +1000 Subject: [PATCH 20/27] make code a bit more readable; improve axis sharing logic --- R/subplots.R | 131 +++++++++++++++++++++++++-------------------------- 1 file changed, 63 insertions(+), 68 deletions(-) diff --git a/R/subplots.R b/R/subplots.R index cb73d15836..7d498a6814 100644 --- a/R/subplots.R +++ b/R/subplots.R @@ -97,7 +97,7 @@ subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, margin = 0.02 axes <- vapply(x$annotations, function(a) identical(a$annotationType, "axis"), logical(1)) x$annotations[!axes] }) - # collect axis objects + # collect axis objects (note a _single_ geo object counts a both an x and y) xAxes <- lapply(layouts, function(lay) { lay[grepl("^xaxis|^geo", names(lay))] %||% list(xaxis = list(domain = c(0, 1))) }) @@ -115,15 +115,21 @@ subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, margin = 0.02 yAxisN <- vapply(yAxes, length, numeric(1)) # old -> new axis name dictionary ncols <- ceiling(length(plots) / nrows) - xAxisID <- if (shareX) { - rep(rep(1:ncols, length.out = length(plots)), xAxisN) - } else { - seq_len(sum(xAxisN)) + xAxisID <- seq_len(sum(xAxisN)) + if (shareX) { + if (length(unique(xAxisN)) > 1) { + warning("Must have a consistent number of axes per 'subplot' to share them.") + } else { + xAxisID <- rep(rep(seq_len(ncols * unique(xAxisN)), length.out = length(plots)), unique(xAxisN)) + } } - yAxisID <- if (shareY) { - rep(rep(1:nrows, each = ncols, length.out = length(plots)), yAxisN) - } else { - seq_len(sum(yAxisN)) + yAxisID <- seq_len(sum(yAxisN)) + if (shareY) { + if (length(unique(yAxisN)) > 1) { + warning("Must have a consistent number of axes per 'subplot' to share them.") + } else { + yAxisID <- rep(rep(seq_len(nrows * unique(xAxisN)), each = ncols, length.out = length(plots)), unique(yAxisN)) + } } # current "axis" names xCurrentNames <- unlist(lapply(xAxes, names)) @@ -145,82 +151,71 @@ subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, margin = 0.02 domainInfo <- get_domains( length(plots), nrows, margin, widths = widths, heights = heights ) - # reposition shapes and annotations - annotations <- Map(reposition, annotations, split(domainInfo, seq_along(plots))) - shapes <- Map(reposition, shapes, split(domainInfo, seq_along(plots))) - # rename axis objects, anchors, and scale their domains for (i in seq_along(plots)) { + # map axis object names xMap <- xAxisMap[[i]] yMap <- yAxisMap[[i]] + xAxes[[i]] <- setNames(xAxes[[i]], names(xMap)) + yAxes[[i]] <- setNames(yAxes[[i]], names(yMap)) + # for cartesian, bump corresponding axis anchor + for (j in seq_along(xAxes[[i]])) { + if (grepl("^geo", names(xAxes[[i]][j]))) next + map <- yMap[yMap %in% sub("y", "yaxis", xAxes[[i]][[j]]$anchor %||% "y")] + xAxes[[i]][[j]]$anchor <- sub("axis", "", names(map)) + } + for (j in seq_along(yAxes[[i]])) { + if (grepl("^geo", names(yAxes[[i]][j]))) next + map <- xMap[xMap %in% sub("x", "xaxis", yAxes[[i]][[j]]$anchor %||% "x")] + yAxes[[i]][[j]]$anchor <- sub("axis", "", names(map)) + } + # map trace xaxis/yaxis/geo attributes + for (key in c("geo", "xaxis", "yaxis")) { + oldAnchors <- unlist(lapply(traces[[i]], "[[", key)) + if (!length(oldAnchors)) next + axisMap <- if (key == "yaxis") yMap else xMap + axisMap <- setNames(sub("axis", "", axisMap), sub("axis", "", names(axisMap))) + newAnchors <- names(axisMap)[match(oldAnchors, axisMap)] + traces[[i]] <- Map(function(tr, a) { tr[[key]] <- a; tr }, traces[[i]], newAnchors) + } + # rescale domains according to the tabular layout xDom <- as.numeric(domainInfo[i, c("xstart", "xend")]) yDom <- as.numeric(domainInfo[i, c("yend", "ystart")]) - for (j in seq_along(xAxes[[i]])) { - # TODO: support ternary as well! - isGeo <- grepl("^geo", xMap[[j]]) - anchorKey <- if (isGeo) "geo" else "xaxis" - traces[[i]] <- lapply(traces[[i]], function(tr) { - tr[[anchorKey]] <- tr[[anchorKey]] %||% sub("axis", "", anchorKey) - # bump trace anchors, where appropriate - if (sub("axis", "", xMap[[j]]) %in% tr[[anchorKey]]) { - tr[[anchorKey]] <- sub("axis", "", names(xMap[j])) - } - tr - }) - if (isGeo) { - xAxes[[i]][[j]]$domain$x <- sort(scales::rescale( - xAxes[[i]][[j]]$domain$x %||% c(0, 1), xDom, from = c(0, 1) - )) - xAxes[[i]][[j]]$domain$y <- sort(scales::rescale( - xAxes[[i]][[j]]$domain$y %||% c(0, 1), yDom, from = c(0, 1) - )) + reScale <- function(old, new) { + sort(scales::rescale( + old %||% c(0, 1), new, from = c(0, 1) + )) + } + xAxes[[i]] <- lapply(xAxes[[i]], function(ax) { + if (all(c("x", "y") %in% names(ax$domain))) { + # geo domains are different from cartesian + ax$domain$x <- reScale(ax$domain$x, xDom) + ax$domain$y <- reScale(ax$domain$y, yDom) } else { - xAxes[[i]][[j]]$domain <- sort(scales::rescale( - xAxes[[i]][[j]]$domain %||% c(0, 1), xDom, from = c(0, 1) - )) - # for cartesian, bump corresponding axis - map <- yMap[yMap %in% sub("y", "yaxis", xAxes[[i]][[j]]$anchor %||% "y")] - xAxes[[i]][[j]]$anchor <- sub("axis", "", names(map)) + ax$domain <- reScale(ax$domain, xDom) } - } - for (j in seq_along(yAxes[[i]])) { - # TODO: support ternary as well! - isGeo <- grepl("^geo", yMap[[j]]) - anchorKey <- if (isGeo) "geo" else "yaxis" - traces[[i]] <- lapply(traces[[i]], function(tr) { - tr[[anchorKey]] <- tr[[anchorKey]] %||% sub("axis", "", anchorKey) - # bump trace anchors, where appropriate - if (sub("axis", "", yMap[[j]]) %in% tr[[anchorKey]]) { - tr[[anchorKey]] <- sub("axis", "", names(yMap[j])) - } - tr - }) - if (isGeo) { - yAxes[[i]][[j]]$domain$x <- sort(scales::rescale( - yAxes[[i]][[j]]$domain$x %||% c(0, 1), xDom, from = c(0, 1) - )) - yAxes[[i]][[j]]$domain$y <- sort(scales::rescale( - yAxes[[i]][[j]]$domain$y %||% c(0, 1), yDom, from = c(0, 1) - )) + ax + }) + yAxes[[i]] <- lapply(yAxes[[i]], function(ax) { + if (all(c("x", "y") %in% names(ax$domain))) { + # geo domains are different from cartesian + ax$domain$x <- reScale(ax$domain$x, xDom) + ax$domain$y <- reScale(ax$domain$y, yDom) } else { - yAxes[[i]][[j]]$domain <- sort(scales::rescale( - yAxes[[i]][[j]]$domain %||% c(0, 1), yDom, from = c(0, 1) - )) - # for cartesian, bump corresponding axis - map <- xMap[xMap %in% sub("x", "xaxis", yAxes[[i]][[j]]$anchor %||% "x")] - yAxes[[i]][[j]]$anchor <- sub("axis", "", names(map)) + ax$domain <- reScale(ax$domain, yDom) } - } - xAxes[[i]] <- setNames(xAxes[[i]], names(xMap)) - yAxes[[i]] <- setNames(yAxes[[i]], names(yMap)) + ax + }) } # start merging the plots into a single subplot p <- list( data = Reduce(c, traces), layout = Reduce(modifyList, c(xAxes, rev(yAxes))) ) + # reposition shapes and annotations + annotations <- Map(reposition, annotations, split(domainInfo, seq_along(plots))) + shapes <- Map(reposition, shapes, split(domainInfo, seq_along(plots))) p$layout$annotations <- Reduce(c, annotations) p$layout$shapes <- Reduce(c, shapes) - # merge non-axis layout stuff layouts <- lapply(layouts, function(x) x[!grepl("^[x-y]axis|^geo", names(x))] %||% list()) if (which_layout != "merge") { From 88591f6f1f9d7d1b267b4cdaad5ca7aeb8f36efe Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Mon, 16 May 2016 12:52:14 +1000 Subject: [PATCH 21/27] improve ggplotly.ggmatrix logic --- R/ggplotly.R | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/R/ggplotly.R b/R/ggplotly.R index 883a6f3d62..fa1057db9e 100644 --- a/R/ggplotly.R +++ b/R/ggplotly.R @@ -38,18 +38,35 @@ ggplotly <- function(p = ggplot2::last_plot(), width = NULL, height = NULL, #' @export ggplotly.ggmatrix <- function(p = ggplot2::last_plot(), width = NULL, height = NULL, tooltip = "all", source = "A", ...) { - plotList <- list() - for (i in seq_len(p$nrow)) { - for (j in seq_len(p$ncol)) { - plotList <- c(plotList, list(p[i, j])) + subplotList <- list() + for (i in seq_len(p$ncol)) { + columnList <- list() + for (j in seq_len(p$nrow)) { + thisPlot <- p[j, i] + if (i == 1) { + if (p$showYAxisPlotLabels) thisPlot <- thisPlot + ylab(p$yAxisLabels[j]) + } else { + # y-axes are never drawn on the interior, and diagonal plots are densities, + # so it doesn't make sense to synch zoom actions on y + thisPlot <- thisPlot + + theme( + axis.ticks.y = element_blank(), + axis.text.y = element_blank() + ) + } + columnList <- c(columnList, list(ggplotly(thisPlot, tooltip = tooltip))) } + # conditioned on a column in a ggmatrix, the x-axis should be on the + # same scale. + s <- subplot(columnList, nrows = p$nrow, margin = 0.01, shareX = TRUE, titleY = TRUE) + #if (i == 3) browser() + subplotList <- c(subplotList, list(s)) } - # TODO: - # (1) how to show x/y titles? Should these be arguments in subplot? - # (2) it only makes since to share axes on the lower diagonal - l <- get_plot(do.call(subplot, c(plotList, list(nrows = p$nrow, ...)))) - l$layout$title <- p$title - hash_plot(p$data, l) + s <- subplot(subplotList, nrows = 1) + if (nchar(p$title) > 0) { + s <- layout(s, title = p$title) + } + layout(s, width = width, height = height) } #' @export @@ -673,8 +690,8 @@ gg2list <- function(p, width = NULL, height = NULL, tooltip = "all", source = "A l <- list(data = setNames(traces, NULL), layout = compact(gglayout)) # ensure properties are boxed correctly l <- add_boxed(rm_asis(l)) - l$width <- width - l$height <- height + l$layout$width <- width + l$layout$height <- height l$source <- source structure(l, class = "plotly_built") } From b88191779153dd4ca82c3283af9d83be6df82526 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Mon, 16 May 2016 12:52:48 +1000 Subject: [PATCH 22/27] moar subplot tests --- tests/testthat/test-plotly-subplot.R | 46 ++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/testthat/test-plotly-subplot.R b/tests/testthat/test-plotly-subplot.R index 1843910df0..487a8bdb7f 100644 --- a/tests/testthat/test-plotly-subplot.R +++ b/tests/testthat/test-plotly-subplot.R @@ -90,3 +90,49 @@ test_that("Row/column height/width", { expect_equal(diff(l$layout$yaxis2$domain), 0.8 - 0.005) }) +test_that("recursive subplots work", { + p1 <- plot_ly(economics, x = date, y = unemploy) + p2 <- plot_ly(economics, x = date, y = uempmed) + s1 <- subplot(p1, p1, shareY = TRUE) + s2 <- subplot(p2, p2, shareY = TRUE) + s <- subplot(s1, s2, nrows = 2, shareX = TRUE) + l <- expect_traces(s, 4, "recursive") + xaxes <- l$layout[grepl("^xaxis", names(l$layout))] + yaxes <- l$layout[grepl("^yaxis", names(l$layout))] + expect_true(length(xaxes) == 2) + expect_true(length(yaxes) == 2) + # both x-axes are anchored on the same y-axis + yanchor <- unique(unlist(lapply(xaxes, "[[", "anchor"))) + expect_true(length(yanchor) == 1) + # both y-axes are anchored on the same x-axis + xanchor <- unique(unlist(lapply(yaxes, "[[", "anchor"))) + expect_true(length(xanchor) == 1) + # x/y are anchored on the bottom/left + expect_true(l$layout[[sub("x", "xaxis", xanchor)]]$domain[1] == 0) + expect_true(l$layout[[sub("y", "yaxis", yanchor)]]$domain[1] == 0) + # every trace is anchored on a different x/y axis pair + xTraceAnchors <- sapply(l$data, "[[", "xaxis") + yTraceAnchors <- sapply(l$data, "[[", "yaxis") + expect_true(length(unique(paste(xTraceAnchors, yTraceAnchors))) == 4) +}) + +test_that("subplot accepts a list of plots", { + vars <- setdiff(names(economics), "date") + plots <- lapply(vars, function(var) { + plot_ly(x = economics$date, y = economics[[var]], name = var) + }) + s <- subplot(plots, nrows = length(plots), shareX = TRUE, titleX = FALSE) + l <- expect_traces(s, 5, "plot-list") + xaxes <- l$layout[grepl("^xaxis", names(l$layout))] + yaxes <- l$layout[grepl("^yaxis", names(l$layout))] + expect_true(length(xaxes) == 1) + expect_true(length(yaxes) == 5) + # x-axis is anchored at the bottom + expect_true(l$layout[[sub("y", "yaxis", xaxes[[1]]$anchor)]]$domain[1] == 0) +}) + + +test_that("ggplotly understands ggmatrix", { + L <- save_outputs(GGally::ggpairs(iris), "plotly-subplot-ggmatrix") +}) + From 35ae74a97fe90004344ceea9f8782ae98ba2d3d9 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Mon, 16 May 2016 13:19:10 +1000 Subject: [PATCH 23/27] supply defaults for geo --- R/subplots.R | 13 +++++++++-- tests/testthat/test-plotly-subplot.R | 32 ++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/R/subplots.R b/R/subplots.R index 7d498a6814..c3fd97b357 100644 --- a/R/subplots.R +++ b/R/subplots.R @@ -98,11 +98,20 @@ subplot <- function(..., nrows = 1, widths = NULL, heights = NULL, margin = 0.02 x$annotations[!axes] }) # collect axis objects (note a _single_ geo object counts a both an x and y) + geoDomainDefault <- list(x = c(0, 1), y = c(0, 1)) xAxes <- lapply(layouts, function(lay) { - lay[grepl("^xaxis|^geo", names(lay))] %||% list(xaxis = list(domain = c(0, 1))) + keys <- grep("^geo|^xaxis", names(lay), value = TRUE) %||% "xaxis" + for (k in keys) { + lay[[k]]$domain <- lay[[k]]$domain %||% if (grepl("^geo", k)) geoDomainDefault else c(0, 1) + } + lay[keys] }) yAxes <- lapply(layouts, function(lay) { - lay[grepl("^yaxis|^geo", names(lay))] %||% list(yaxis = list(domain = c(0, 1))) + keys <- grep("^geo|^yaxis", names(lay), value = TRUE) %||% "yaxis" + for (k in keys) { + lay[[k]]$domain <- lay[[k]]$domain %||% if (grepl("^geo", k)) geoDomainDefault else c(0, 1) + } + lay[keys] }) if (!titleX) { xAxes <- lapply(xAxes, function(ax) lapply(ax, function(y) { y$title <- NULL; y })) diff --git a/tests/testthat/test-plotly-subplot.R b/tests/testthat/test-plotly-subplot.R index 487a8bdb7f..c1a8ac13b2 100644 --- a/tests/testthat/test-plotly-subplot.R +++ b/tests/testthat/test-plotly-subplot.R @@ -136,3 +136,35 @@ test_that("ggplotly understands ggmatrix", { L <- save_outputs(GGally::ggpairs(iris), "plotly-subplot-ggmatrix") }) +test_that("geo+cartesian behaves", { + # specify some map projection/options + g <- list( + scope = 'usa', + projection = list(type = 'albers usa'), + lakecolor = toRGB('white') + ) + # create a map of population density + density <- state.x77[, "Population"] / state.x77[, "Area"] + map <- plot_ly( + z = density, + text = state.name, locations = state.abb, + type = 'choropleth', locationmode = 'USA-states', geo = "geo" + ) %>% layout(geo = g) + # create a bunch of horizontal bar charts + vars <- colnames(state.x77) + barcharts <- lapply(vars, function(var) { + plot_ly(x = state.x77[, var], y = state.name, type = "bar", + orientation = "h", name = var) %>% + layout(showlegend = FALSE, hovermode = "y", + yaxis = list(showticklabels = FALSE)) + }) + s <- subplot( + subplot(barcharts, margin = 0.01), map, + nrows = 2, heights = c(0.3, 0.7) + ) + l <- expect_traces(s, 9, "geo-cartesian") + geoDom <- l$layout[[grep("^geo", names(l$layout))]]$domain + expect_equal(geoDom$x, c(0, 1)) + expect_equal(geoDom$y, c(0, 0.68)) +}) + From 957923ce57b3dadf2ffefe163d9addd0aee8fb37 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Mon, 16 May 2016 15:01:26 +1000 Subject: [PATCH 24/27] cleanup --- R/ggplotly.R | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/R/ggplotly.R b/R/ggplotly.R index fa1057db9e..ab4b955d97 100644 --- a/R/ggplotly.R +++ b/R/ggplotly.R @@ -59,14 +59,13 @@ ggplotly.ggmatrix <- function(p = ggplot2::last_plot(), width = NULL, # conditioned on a column in a ggmatrix, the x-axis should be on the # same scale. s <- subplot(columnList, nrows = p$nrow, margin = 0.01, shareX = TRUE, titleY = TRUE) - #if (i == 3) browser() subplotList <- c(subplotList, list(s)) } - s <- subplot(subplotList, nrows = 1) + s <- layout(subplot(subplotList, nrows = 1), width = width, height = height) if (nchar(p$title) > 0) { s <- layout(s, title = p$title) } - layout(s, width = width, height = height) + hash_plot(pm$data, plotly_build(s)) } #' @export @@ -690,8 +689,8 @@ gg2list <- function(p, width = NULL, height = NULL, tooltip = "all", source = "A l <- list(data = setNames(traces, NULL), layout = compact(gglayout)) # ensure properties are boxed correctly l <- add_boxed(rm_asis(l)) - l$layout$width <- width - l$layout$height <- height + l$width <- width + l$height <- height l$source <- source structure(l, class = "plotly_built") } From 80007acd9888b0d0c36a6e1e118f54c4189c4379 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Mon, 16 May 2016 15:01:44 +1000 Subject: [PATCH 25/27] a few vignette edits --- vignettes/subplot.Rmd | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/vignettes/subplot.Rmd b/vignettes/subplot.Rmd index f6f2081c7b..72e25739fa 100644 --- a/vignettes/subplot.Rmd +++ b/vignettes/subplot.Rmd @@ -21,7 +21,7 @@ knitr::opts_chunk$set( ## Introduction -The `subplot()` function provides a flexible interface for arranging multiple **plotly** plots in a single view. There are a few different ways to use `subplot()`, but the simplest way to pass plotly visualization objects directly to `subplot()`. +The `subplot()` function provides a flexible interface for arranging multiple **plotly** plots in a single view. The simplest way to use it is to pass plotly visualizations directly to `subplot()`. ```{r} library(plotly) @@ -54,7 +54,7 @@ Conceptually, `subplot()` provides a way to place a collection of plots into a t -This can be quite useful for a number of visualizations, for example, a joint density plot: +This flexibility is quite useful for a number of visualizations, for example, a joint density plot (the new [heatmaply](https://github.com/talgalili/heatmaply) package is another good example). ```{r} x <- rnorm(100) @@ -75,11 +75,8 @@ Note that, since `subplot()` returns a plotly object, any [layout attribute](htt ## Recursive subplots -The `subplot()` function is designed to work recursively so that you can have subplots of subplots. This idea is useful when your desired layout doesn't conform to the table structure described in the previous section. In fact, you can think of a subplot of subplots like an excel spreadsheet with merged cells. +The `subplot()` function is designed to work recursively so that you can have subplots of subplots. This idea is useful when your desired layout doesn't conform to the table structure described in the previous section. In fact, you can think of a subplot of subplots like a spreadsheet with merged cells. -
@@ -94,8 +91,7 @@ s2 <- subplot(plotList(2), shareY = TRUE) subplot(s1, s2, plot_ly(), nrows = 3, margin = 0.04, heights = c(0.6, 0.3, 0.1)) ``` - - +The concept is particularly useful when you want plot(s) in a given row to have different widths from plot(s) in another row. ```{r, fig.height = 6} # specify some map projection/options @@ -125,13 +121,11 @@ subplot( ) ``` +## ggplot2 subplots -## subplots with ggplot2 - - -The `subplot()` function also understands ggplot2 objects and converts them via `ggplotly()`. +The `subplot()` function also understands ggplot2 objects, and converts them to an interactive web-based version via `ggplotly()` before arranging them in the final layout. -```{r, fig.height = 8} +```{r, fig.height = 6} e <- tidyr::gather(economics, variable, value, -date) gg1 <- ggplot(e, aes(date, value)) + geom_line() + facet_wrap(~variable, scales = "free_y", ncol = 1) @@ -141,13 +135,13 @@ gg2 <- ggplot(e, aes(factor(1), value)) + geom_violin() + subplot(gg1, gg2) %>% layout(margin = list(l = 50)) ``` -The new `subplot()` infrastructure allows `ggplotly()` to understand ggmatrix +This infrastructure allows `ggplotly()` to understand ggmatrix objects -- the class of object returned by the `ggpairs()` function in the **GGally** package. -```{r} -pm <- GGally::ggpairs(reshape::tips[, 1:3]) -ggplotly(pm, margin = 0.04) +```{r, fig.height = 5} +pm <- GGally::ggpairs(iris) +ggplotly(pm) ``` From f1eee2b46c983caf2e84af3f90584e9b87034946 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Mon, 16 May 2016 15:01:58 +1000 Subject: [PATCH 26/27] bump version; update news --- DESCRIPTION | 2 +- NEWS | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index e476c4b03b..0ff68574fe 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: plotly Title: Create Interactive Web Graphics via 'plotly.js' -Version: 3.5.7 +Version: 3.6.0 Authors@R: c(person("Carson", "Sievert", role = c("aut", "cre"), email = "cpsievert1@gmail.com"), person("Chris", "Parmer", role = c("aut", "cph"), diff --git a/NEWS b/NEWS index d413d0476d..95c7bbd810 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,20 @@ +3.6.0 -- 16 May 2016 + +NEW FEATURES & CHANGES: + +* Many improvements to the subplot() function: + * ggplot2 objects are now officially supported (#520). + * Several new arguments allow one to synchronize x/y axes (#298), height/width (#376), hide/show x/y axis titles. + * A list of plots can now be passed to the first argument. + * A new vignette with examples and more explanation can be accessed via `vignette("subplot")`. + +* ggplotly() is now a generic function with a method for ggmatrix objects. +* plotly_build() is now a generic function. + +BUG FIX: + +Column facet strips will no longer be drawn when there is only one column. + 3.5.7 -- 13 May 2016 CHANGES: From cd62ed55f736344076a25e8917b9f838486cce62 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Mon, 16 May 2016 15:12:42 +1000 Subject: [PATCH 27/27] fix typo; document --- R/ggplotly.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/R/ggplotly.R b/R/ggplotly.R index ab4b955d97..a1ab6b6522 100644 --- a/R/ggplotly.R +++ b/R/ggplotly.R @@ -13,6 +13,7 @@ #' \code{tooltip = c("y", "x", "colour")} if you want y first, x second, and #' colour last. #' @param source Only relevant for \link{event_data}. +#' @param ... arguments passed onto methods. #' @seealso \link{signup}, \link{plot_ly} #' @return a plotly object #' @export @@ -65,7 +66,7 @@ ggplotly.ggmatrix <- function(p = ggplot2::last_plot(), width = NULL, if (nchar(p$title) > 0) { s <- layout(s, title = p$title) } - hash_plot(pm$data, plotly_build(s)) + hash_plot(p$data, plotly_build(s)) } #' @export