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/NAMESPACE b/NAMESPACE
index f490b95c66..ca6602e736 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -11,6 +11,12 @@ S3method(geom2trace,GeomPolygon)
S3method(geom2trace,GeomText)
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(plotly_build,plotly_subplot)
S3method(print,figure)
S3method(print,plotly_built)
S3method(print,plotly_hash)
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:
diff --git a/R/ggplotly.R b/R/ggplotly.R
index 2e9556b9d5..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
@@ -31,7 +32,46 @@
#' }
#'
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", ...) {
+ 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)
+ subplotList <- c(subplotList, list(s))
+ }
+ s <- layout(subplot(subplotList, nrows = 1), width = width, height = height)
+ if (nchar(p$title) > 0) {
+ s <- layout(s, title = p$title)
+ }
+ hash_plot(p$data, plotly_build(s))
+}
+
+#' @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)
}
@@ -44,9 +84,10 @@ ggplotly <- function(p = ggplot2::last_plot(), width = NULL, height = 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
@@ -425,55 +466,55 @@ 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"
)
)
}
}
}
-
- if (has_facet(p)) {
- gglayout[[axisName]]$title <- ""
- }
-
+ if (has_facet(p)) gglayout[[axisName]]$title <- ""
} # 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 (!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 = ", "
+ 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 = ", "
)
- 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(
+ 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),
+ 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 = ", "
)
- 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)
+ 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),
+ 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)
+ }
}
-
} # end of panel loop
# ------------------------------------------------------------------------
diff --git a/R/plotly.R b/R/plotly.R
index 995c108119..5e2a6eaaa1 100644
--- a/R/plotly.R
+++ b/R/plotly.R
@@ -221,20 +221,51 @@ 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()) {
- #if (inherits(l, "ggmatrix"))
- # ggplot objects don't need any special type of handling
- if (ggplot2::is.ggplot(l)) return(gg2list(l))
+ UseMethod("plotly_build")
+}
+
+#' @export
+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()) {
+ prefix_class(get_plot(ggplotly(l)), "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/R/subplots.R b/R/subplots.R
index 50b1794855..c3fd97b357 100644
--- a/R/subplots.R
+++ b/R/subplots.R
@@ -3,14 +3,23 @@
#' @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 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 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 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 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
+#' numeric vector specifying which plots to consider when merging.
#' @return A plotly object
#' @export
#' @author Carson Sievert
@@ -20,136 +29,252 @@
#' 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]
- )
+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[[1]]) && !is.plotly(dotz[[1]])) {
+ dotz <- dotz[[1]]
+ }
+ # build each plot
+ 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) {
+ 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 {
- 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]
- )
+ tr[["geo"]] <- NULL
+ tr[["xaxis"]] <- sub("^x1$", "x", tr[["xaxis"]][1] %||% "x")
+ tr[["yaxis"]] <- sub("^y1$", "y", tr[["yaxis"]][1] %||% "y")
}
- p_info[[ctr]] <- c(info, plot = i, trace = j)
- ctr <- ctr + 1
+ 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 <- 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)
+ # 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")
+ 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 (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) {
+ 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) {
+ 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 }))
+ }
+ if (!titleY) {
+ 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))
+ yAxisN <- vapply(yAxes, length, numeric(1))
+ # old -> new axis name dictionary
+ ncols <- ceiling(length(plots) / nrows)
+ 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 <- 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))
}
}
- # 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)
+ # current "axis" names
+ xCurrentNames <- unlist(lapply(xAxes, names))
+ yCurrentNames <- unlist(lapply(yAxes, names))
+ xNewNames <- paste0(
+ sub("[0-9]+$", "", xCurrentNames),
+ sub("^1$", "", xAxisID)
+ )
+ 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))
+ # domains of each subplot
+ domainInfo <- get_domains(
+ length(plots), nrows, margin, widths = widths, heights = heights
+ )
+ 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")])
+ 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 {
+ ax$domain <- reScale(ax$domain, xDom)
+ }
+ 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 {
+ ax$domain <- reScale(ax$domain, yDom)
+ }
+ ax
+ })
}
- # 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(modifyList, c(xAxes, rev(yAxes)))
)
- # merge layouts of the subplots
- ls <- if (which_layout == "merge") {
- lapply(dots, "[[", "layout")
- } else {
+ # 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") {
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)
+ res <- hash_plot(data.frame(), p)
+ prefix_class(res, "plotly_subplot")
}
-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 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")
+ }
+ 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)
@@ -158,8 +283,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))
@@ -172,3 +297,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
+}
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
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/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
+
}
diff --git a/man/subplot.Rd b/man/subplot.Rd
index f4a6c0499e..e76fbbe50a 100644
--- a/man/subplot.Rd
+++ b/man/subplot.Rd
@@ -4,7 +4,9 @@
\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, margin = 0.02,
+ shareX = FALSE, shareY = FALSE, titleX = shareX, titleY = shareY,
+ which_layout = "merge")
}
\arguments{
\item{...}{any number of plotly objects}
@@ -12,15 +14,30 @@ 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{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.}
}
\value{
A plotly object
diff --git a/tests/testthat/test-plotly-subplot.R b/tests/testthat/test-plotly-subplot.R
index d73b9b624e..c1a8ac13b2 100644
--- a/tests/testthat/test-plotly-subplot.R
+++ b/tests/testthat/test-plotly-subplot.R
@@ -45,5 +45,126 @@ test_that("group + [x/y]axis works", {
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
+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)
+})
+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")
+})
+
+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))
+})
diff --git a/vignettes/intro.Rmd b/vignettes/intro.Rmd
index d3d5ffe61d..8f0a3280f7 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)
```
@@ -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 @@
+
+
\ 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 @@
+