diff --git a/.gitignore b/.gitignore index 413e2122e6..b9f826b92a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ R/.Rhistory Rapp.history /Karthik_local.R +*~ +.Rhistory \ No newline at end of file diff --git a/DESCRIPTION b/DESCRIPTION index fe2d84adbb..be1728b08e 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: plotly Type: Package Title: Interactive, publication-quality graphs online. -Version: 0.3.5 +Version: 0.3.6 Authors@R: c(person("Chris", "Parmer", role = c("aut", "cre"), email = "chris@plot.ly"), person("Scott", "Chamberlain", role = "aut", @@ -19,6 +19,11 @@ URL: https://github.com/ropensci/plotly BugReports: https://github.com/ropensci/plotly/issues Depends: RCurl, - RJSONIO + RJSONIO, + ggplot2, + plyr Imports: knitr +Suggests: + maps, + testthat diff --git a/NAMESPACE b/NAMESPACE index fc8367508a..dc662d89bc 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,11 @@ +export(getLegendList) +export(gg2list) +export(layer2list) export(plotly) export(signup) +export(toRGB) import(RCurl) import(RJSONIO) +import(ggplot2) import(knitr) +import(plyr) diff --git a/NEWS b/NEWS new file mode 100644 index 0000000000..6af1b52ba9 --- /dev/null +++ b/NEWS @@ -0,0 +1,5 @@ +0.3.6 -- 10 March 2014. + +Merge ggplotly code. + +0.3.5 \ No newline at end of file diff --git a/R/ggplotly.R b/R/ggplotly.R new file mode 100644 index 0000000000..118ffb23c5 --- /dev/null +++ b/R/ggplotly.R @@ -0,0 +1,618 @@ +#' Convert R pch point codes to plotly "symbol" codes. +pch2symbol <- c("0"="square", + "1"="circle", + "2"="triangle-up", + "3"="cross", + "4"="x", + "5"="diamond", + "6"="triangle-down", + "15"="square", + "16"="circle", + "17"="triangle-up", + "18"="diamond", + "19"="circle", + "20"="circle", + "22"="square", + "23"="diamond", + "24"="triangle-up", + "25"="triangle-down", + "o"="circle", + "O"="circle", + "+"="cross") + +#' Convert ggplot2 aes to plotly "marker" codes. +aes2marker <- c(alpha="opacity", + pch="symbol", + colour="color", + size="size", + ##TODO="line", ## line color, size, and dash + shape="symbol", + text="text") +marker.defaults <- c(alpha=1, + shape="o", + pch="o", + colour="black") +#' Convert ggplot2 aes to line parameters. +aes2line <- c(linetype="dash", + colour="color", + size="width", + text="text") +line.defaults <- + list(linetype="solid", + colour="black", + size=2) + +numeric.lty <- + c("1"="solid", + "2"="dash", + "3"="dot", + "4"="dashdot", + "5"="longdash", + "6"="longdashdot") + +named.lty <- + c("solid"="solid", + "blank"="none", + "dashed"="dash", + "dotted"="dotted", + "dotdash"="dashdot", + "longdash"="longdash", + "twodash"="dash") + +## TODO: does plotly support this?? +coded.lty <- + c("22"="dash", + "42"="dot", + "44"="dashdot", + "13"="longdash", + "1343"="longdashdot", + "73"="dash", + "2262"="dotdash", + "12223242"="dotdash", + "F282"="dash", + "F4448444"="dash", + "224282F2"="dash", + "F1"="dash") + +#' Convert R lty line type codes to plotly "dash" codes. +lty2dash <- c(numeric.lty, named.lty, coded.lty) + +#' Convert a ggplot to a list. +#' @import ggplot2 +#' @param p ggplot2 plot. +#' @return list representing a ggplot. +#' @export +gg2list <- function(p){ + ## Always use identity size scale so that plot.ly gets the real + ## units for the size variables. + p <- p+scale_size_identity() + plist <- list() + ## Before building the ggplot, we would like to add aes(name) to + ## figure out what the object group is later. + for(layer.i in seq_along(p$layers)){ + a <- c(p$layers[[layer.i]]$mapping, p$mapping) + group.vars <- c("colour", "color", "col", + "fill", + "linetype", "lty", + "shape", "pch") + group.var <- a$name + for(gv in group.vars){ + if(is.null(group.var)){ + g.expr <- a[[gv]] + if(!is.null(g.expr)){ + group.var <- g.expr + } + } + } + p$layers[[layer.i]]$mapping$name <- group.var + } + plistextra <- ggplot2::ggplot_build(p) + ## NOTE: data from ggplot_build have scales already applied. This + ## may be a bad thing for log scales. + for(sc in plistextra$plot$scales$scales){ + if(sc$scale_name == "manual"){ + plist$scales[[sc$aesthetics]] <- sc$palette(0) + }else if(sc$scale_name == "brewer"){ + plist$scales[[sc$aesthetics]] <- sc$palette(length(sc$range$range)) + }else if(sc$scale_name == "hue"){ + plist$scales[[sc$aesthetics]] <- sc$palette(length(sc$range$range)) + }else if(sc$scale_name == "linetype_d"){ + plist$scales[[sc$aesthetics]] <- sc$palette(length(sc$range$range)) + }else if(sc$scale_name == "alpha_c"){ + plist$scales[[sc$aesthetics]] <- sc$palette(sc$range$range) + }else if(sc$scale_name == "size_c"){ + plist$scales[[sc$aesthetics]] <- sc$palette(sc$range$range) + }else if(sc$scale_name == "gradient"){ + plist$scales[[sc$aesthetics]] <- ggplot2:::scale_map(sc, ggplot2:::scale_breaks(sc)) + } + } + for(i in seq_along(plistextra$plot$layers)){ + ## This is the layer from the original ggplot object. + L <- plistextra$plot$layers[[i]] + + ## for each layer, there is a correpsonding data.frame which + ## evaluates the aesthetic mapping. + df <- plistextra$data[[i]] + + ## This extracts essential info for this geom/layer. + g <- layer2list(L, df, plistextra$panel$ranges[[1]]) + + ## Idea: use the ggplot2:::coord_transform(coords, data, scales) + ## function to handle cases like coord_flip. scales is a list of + ## 12, coords is a list(limits=list(x=NULL,y=NULL)) with class + ## e.g. c("cartesian","coord"). The result is a transformed data + ## frame where all the data values are between 0 and 1. + + ## TODO: coord_transform maybe won't work for + ## geom_dotplot|rect|segment and polar/log transformations, which + ## could result in something nonlinear. For the time being it is + ## best to just ignore this, but you can look at the source of + ## e.g. geom-rect.r in ggplot2 to see how they deal with this by + ## doing a piecewise linear interpolation of the shape. + + g$data <- ggplot2:::coord_transform(plistextra$plot$coord, g$data, + plistextra$panel$ranges[[1]]) + plist$geoms[[i]] <- g + } + # Export axis specification as a combination of breaks and + # labels, on the relevant axis scale (i.e. so that it can + # be passed into d3 on the x axis scale instead of on the + # grid 0-1 scale). This allows transformations to be used + # out of the box, with no additional d3 coding. + theme.pars <- ggplot2:::plot_theme(p) + + ## Flip labels if coords are flipped - transform does not take care + ## of this. Do this BEFORE checking if it is blank or not, so that + ## individual axes can be hidden appropriately, e.g. #1. + ranges <- plistextra$panel$ranges[[1]] + if("flip"%in%attr(plistextra$plot$coordinates, "class")){ + temp <- plistextra$plot$labels$x + plistextra$plot$labels$x <- plistextra$plot$labels$y + plistextra$plot$labels$y <- temp + } + is.blank <- function(el.name){ + x <- ggplot2::calc_element(el.name, p$theme) + "element_blank"%in%attr(x,"class") + } + plist$axis <- list() + for(xy in c("x","y")){ + s <- function(tmp)sprintf(tmp, xy) + plist$axis[[xy]] <- ranges[[s("%s.major")]] + plist$axis[[s("%slab")]] <- if(is.blank(s("axis.text.%s"))){ + NULL + }else{ + ranges[[s("%s.labels")]] + } + plist$axis[[s("%srange")]] <- ranges[[s("%s.range")]] + plist$axis[[s("%sname")]] <- if(is.blank(s("axis.title.%s"))){ + "" + }else{ + plistextra$plot$labels[[xy]] + } + plist$axis[[s("%sline")]] <- !is.blank(s("axis.line.%s")) + plist$axis[[s("%sticks")]] <- !is.blank(s("axis.ticks.%s")) + } + + plist$legend <- getLegendList(plistextra) + if(length(plist$legend)>0){ + plist$legend <- plist$legend[which(sapply(plist$legend, function(i) length(i)>0))] + } # only pass out legends that have guide = "legend" or guide="colorbar" + + # Remove legend if theme has no legend position + if(theme.pars$legend.position=="none") plist$legend <- NULL + + if("element_blank"%in%attr(theme.pars$plot.title, "class")){ + plist$title <- "" + } else { + plist$title <- plistextra$plot$labels$title + } + + pargs <- list() + for(g in plist$geoms){ + pargs <- c(pargs, g$traces) + } + pargs$kwargs <- list() + pargs +} + +#' Convert a layer to a list. Called from gg2list() +#' @param l one layer of the ggplot object +#' @param d one layer of calculated data from ggplot2::ggplot_build(p) +#' @param ranges axes ranges +#' @return list representing a layer, with corresponding aesthetics, ranges, and groups. +#' @export +layer2list <- function(l, d, ranges){ + g <- list(geom=l$geom$objname, + data=d) + g$aes <- sapply(l$mapping, function(k) as.character(as.expression(k))) # needed for when group, etc. is an expression + + ## use un-named parameters so that they will not be exported + ## to JSON as a named object, since that causes problems with + ## e.g. colour. + g$params <- c(l$geom_params, l$stat_params) + ## non-ggplot2 params like name are useful for plot.ly and ggplot2 + ## places them into stat_params. + for(p.name in names(g$params)){ + names(g$params[[p.name]]) <- NULL + } + + ## Convert complex ggplot2 geoms so that they are treated as special + ## cases of basic geoms. In ggplot2, this processing is done in the + ## draw method of the geoms. + + ## Every plotly trace has one of these types + ## type=scatter,bar,box,histogramx,histogram2d,heatmap + + ## for type=scatter, you can define + ## mode=none,markers,lines,lines+markers where "lines" is the + ## default for 20 or more points, "lines+markers" is the default for + ## <20 points. "none" is useful mainly if fill is used to make area + ## plots with no lines. + + ## marker=list(size,line,color="rgb(54,144,192)",opacity,symbol) + + ## symbol=circle,square,diamond,cross,x, + ## triangle-up,triangle-down,triangle-left,triangle-right + + geom <- function(...){ + gnames <- c(...) + g$geom %in% gnames + } + g$geom <- if(geom("abline")){ + # "Trick" ggplot coord_transform into transforming the slope and intercept + g$data[,"x"] <- ranges$x.range[1] + g$data[,"xend"] <- ranges$x.range[2] + g$data[,"y"] <- g$data$slope*ranges$x.range[1]+g$data$intercept + g$data[,"yend"] <- g$data$slope*ranges$x.range[2]+g$data$intercept + g$data <- as.data.frame(g$data) + if(g$aes[["group"]]=="1"){ + # ggplot2 defaults to adding a group attribute + # which misleads for situations where there are + # multiple lines with the same group. + # if the group attribute conveys no additional + # information, remove it. + ## TODO: Figure out a better way to handle this... + g$aes <- g$aes[-which(names(g$aes)=="group")] + } + "segment" + } else if(geom("point")){ + g$data$group <- 1 + # Fill set to match ggplot2 default of filled in circle. + if(!"fill"%in%names(g$data) & "colour"%in%names(g$data)){ + g$data[["fill"]] <- g$data[["colour"]] + } + "point" + } else if(geom("ribbon")){ + # Color set to match ggplot2 default of fill with no outside border. + if("fill"%in%names(g$data) & !"colour"%in%names(g$data)){ + g$data[["colour"]] <- g$data[["fill"]] + } + "ribbon" + } else if(geom("density") | geom("area")){ + "ribbon" + } else if(geom("tile") | geom("raster") | geom("histogram") ){ + # Color set to match ggplot2 default of tile with no outside border. + if(!"colour"%in%names(g$data) & "fill"%in%names(g$data)){ + g$data[["colour"]] <- g$data[["fill"]] + # Make outer border of 0 size if size isn't already specified. + if(!"size"%in%names(g$data)) g$data[["size"]] <- 0 + } + "rect" + } else if(geom("bar")){ + "rect" + } else if(g$geom=="bin2d"){ + stop("TODO") + } else if(geom("boxplot")){ + stop("boxplots are not supported. Workaround: rects, lines, and points") + ## TODO: boxplot support. But it is hard since boxplots are drawn + ## using multiple geoms and it is not straightforward to deal with + ## that using our current JS code. There is a straightforward + ## workaround: combine working geoms (rects, lines, and points). + + g$data$outliers <- sapply(g$data$outliers, FUN=paste, collapse=" @ ") + # outliers are specified as a list... + } else if(geom("violin")){ + x <- g$data$x + vw <- g$data$violinwidth + xmin <- g$data$xmin + xmax <- g$data$xmax + g$data$xminv <- x-vw*(x-xmin) + g$data$xmaxv <- x+vw*(xmax-x) + newdata <- ddply(g$data, .(group), function(df){ + rbind(arrange(transform(df, x=xminv), y), arrange(transform(df, x=xmaxv), -y)) + }) + newdata <- ddply(newdata, .(group), function(df) rbind(df, df[1,])) + g$data <- newdata + "polygon" + } else if(geom("step")){ + datanames <- names(g$data) + g$data <- ddply(g$data, .(group), function(df) ggplot2:::stairstep(df)) + "path" + } else if(geom("contour") | g$geom=="density2d"){ + g$aes[["group"]] <- "piece" + "path" + } else if(geom("freqpoly")){ + "line" + } else if(geom("quantile")){ + "path" + } else if(geom("hex")){ + ## TODO: for interactivity we will run into the same problems as + ## we did with histograms. Again, if we put several + ## clickSelects/showSelected values in the same hexbin, then + ## clicking/hiding hexbins doesn't really make sense. Need to stop + ## with an error if showSelected/clickSelects is used with hex. + g$aes[["group"]] <- "group" + dx <- ggplot2::resolution(g$data$x, FALSE) + dy <- ggplot2::resolution(g$data$y, FALSE) / sqrt(3) / 2 * 1.15 + hex <- as.data.frame(hexcoords(dx, dy))[,1:2] + hex <- rbind(hex, hex[1,]) # to join hexagon back to first point + g$data$group <- as.numeric(interaction(g$data$group, 1:nrow(g$data))) + ## this has the potential to be a bad assumption - + ## by default, group is identically 1, if the user + ## specifies group, polygons aren't possible to plot + ## using d3, because group will have a different meaning + ## than "one single polygon". + newdata <- ddply(g$data, .(group), function(df){ + df$xcenter <- df$x + df$ycenter <- df$y + cbind(x=df$x+hex$x, y=df$y+hex$y, df[,-which(names(df)%in%c("x", "y"))]) + }) + g$data <- newdata + # Color set to match ggplot2 default of tile with no outside border. + if(!"colour"%in%names(g$data) & "fill"%in%names(g$data)){ + g$data[["colour"]] <- g$data[["fill"]] + # Make outer border of 0 size if size isn't already specified. + if(!"size"%in%names(g$data)) g$data[["size"]] <- 0 + } + "polygon" + } else if(geom("polygon", "line", "segment")) { + ## all other geoms are basic, and keep the same name. + g$geom + } else { + stop("unsupported geom ", g$geom) + } + + ## For ggplot2 polygons, change convert groups to vectors with NA. + if(geom("polygon")){ + poly.list <- split(g$data, g$data$group) + is.group <- names(g$data) == "group" + poly.na.df <- data.frame() + for(i in seq_along(poly.list)){ + no.group <- poly.list[[i]][,!is.group,drop=FALSE] + poly.na.df <- rbind(poly.na.df, no.group, NA) + } + g$data <- poly.na.df + } + + ## Check g$data for color/fill - convert to hexadecimal so JS can + ## parse correctly. + for(color.var in c("colour", "color", "fill")){ + if(color.var %in% names(g$data)){ + g$data[,color.var] <- toRGB(g$data[,color.var]) + } + } + + if(any(g$data$size == 0, na.rm=TRUE)){ + warning(sprintf("geom_%s with size=0 will be invisible",g$geom)) + } + + g$traces <- list() + group.vars <- c("group", + "color", "colour", + "fill") #TODO. + group.var <- NULL + found.groups <- 0 + for(gv in group.vars){ + if(is.null(group.var)){ + g.col <- g$data[[gv]] + n.groups <- length(unique(g.col)) + if(n.groups > 1){ + group.var <- g.col + found.groups <- n.groups + } + } + } + group.list <- if(found.groups){ + split(g$data, group.var) + }else{ + list(g$data) + } + for(group.i in seq_along(group.list)){ + group.data <- group.list[[group.i]] + tr <- group2trace(group.data, g$params, g$geom) + if(is.null(tr$name)){ + tr$name <- group.data$name + } + tr$name <- as.character(tr$name[1]) + g$traces[[group.i]] <- tr + } + g +} + +getMarker <- function(df, params, aesConverter, defaults, only=NULL){ + marker <- list() + for(name in names(aesConverter)){ + plotly.name <- aesConverter[[name]] + take.from <- if(name %in% names(params)){ + params + } else if(name %in% names(df)){ + df + } else { + defaults + } + take.from <- as.list(take.from) + to.write <- take.from[[name]] + ## if(is.null(to.write)){ + ## print(take.from) + ## stop("undefined marker ", name) + ## } + marker[[plotly.name]] <- if(!is.null(only)){ + to.write[only] + }else{ + to.write + } + } + if(length(marker$size) > 1){ + marker$sizeref <- min(marker$size) + marker$sizemode <- "area" + } + if("dash" %in% names(marker)){ + marker$dash <- lty2dash[[marker$dash]] + } + marker +} + +##' Convert 1 ggplot2 group to 1 plotly trace. +##' @param df data.frame. +##' @param params list of defaults. +##' @param geom length 1 character. +##' @return a list to be passed to plotly(). +##' @author Toby Dylan Hocking +group2trace <- function(df, params, geom){ + ## Add plotly type/mode info based on geom type. + tr <- if(geom == "point"){ + marker <- getMarker(df, params, aes2marker, marker.defaults) + list(type="scatter", + mode="markers", + marker=marker) + }else if(geom %in% c("line", "polygon")){ + list(type="scatter", + mode="lines", + line=getMarker(df, params, aes2line, line.defaults, 1)) + }else{ + stop("group2trace does not support geom ", geom) + } + ## Copy data to output trace + for(name in c("x", "y", "text", "name")){ + take.from <- if(name %in% names(df)){ + df + }else if(name %in% names(params)){ + params + } + tr[[name]] <- take.from[[name]] + } + tr +} +#' Get legend information. +#' @import plyr +#' @param plistextra output from ggplot2::ggplot_build(p) +#' @return list containing information for each legend +#' @export +getLegendList <- function(plistextra){ + plot <- plistextra$plot + scales <- plot$scales + layers <- plot$layers + default_mapping <- plot$mapping + theme <- ggplot2:::plot_theme(plot) + position <- theme$legend.position + # by default, guide boxes are vertically aligned + theme$legend.box <- if(is.null(theme$legend.box)) "vertical" else theme$legend.box + + # size of key (also used for bar in colorbar guide) + theme$legend.key.width <- if(is.null(theme$legend.key.width)) theme$legend.key.size + theme$legend.key.height <- if(is.null(theme$legend.key.height)) theme$legend.key.size + # by default, direction of each guide depends on the position of the guide. + theme$legend.direction <- if(is.null(theme$legend.direction)){ + if (length(position) == 1 && position %in% c("top", "bottom", "left", "right")) + switch(position[1], top =, bottom = "horizontal", left =, right = "vertical") + else + "vertical" + } + # justification of legend boxes + theme$legend.box.just <- + if(is.null(theme$legend.box.just)) { + if (length(position) == 1 && position %in% c("top", "bottom", "left", "right")) + switch(position, bottom =, top = c("center", "top"), left =, right = c("left", "top")) + else + c("center", "center") + } + + position <- theme$legend.position + guides <- plyr::defaults(plot$guides, guides(colour="legend", fill="legend")) + labels <- plot$labels + gdefs <- ggplot2:::guides_train(scales = scales, theme = theme, guides = guides, labels = labels) + if (length(gdefs) != 0) { + gdefs <- ggplot2:::guides_merge(gdefs) + gdefs <- ggplot2:::guides_geom(gdefs, layers, default_mapping) + } else (ggplot2:::zeroGrob()) + names(gdefs) <- sapply(gdefs, function(i) i$title) + lapply(gdefs, getLegend) +} + +#' Function to get legend information for each scale +#' @param mb single entry from ggplot2:::guides_merge() list of legend data +#' @return list of legend information, NULL if guide=FALSE. +getLegend <- function(mb){ + guidetype <- mb$name + ## The main idea of legends: + + ## 1. Here in getLegend I export the legend entries as a list of + ## rows that can be used in a data() bind in D3. + + ## 2. In add_legend in the JS code I create a for every + ## legend, and then I bind the legend entries to ,
, and + ## elements. + geoms <- sapply(mb$geoms, function(i) i$geom$objname) + cleanData <- function(data, key, geom, params){ + if(nrow(data)==0) return(data.frame()); # if no rows, return an empty df. + if("guide"%in%names(params)){ + if(params[["guide"]]=="none") return(data.frame()); # if no guide, return an empty df + } + data$order <- 1:nrow(data) + data <- merge(data, key) + data <- data[order(data$order),] + if(!".label"%in%names(data)) return(data.frame()); # if there are no labels, return an empty df. + if(nrow(data)==0) return(data.frame()); + data <- data[,which(colSums(!is.na(data))>0)] # remove cols that are entirely na + if("colour"%in%names(data)) data[["colour"]] <- toRGB(data[["colour"]]) # color hex values + if("fill"%in%names(data)) data[["fill"]] <- toRGB(data[["fill"]]) # fill hex values + names(data) <- paste(geom, names(data), sep="") # aesthetics by geom + names(data) <- gsub(paste(geom, ".", sep=""), "", names(data), fixed=TRUE) # label isn't geom-specific + data + } + dataframes <- lapply(mb$geoms, function(i) cleanData(i$data, mb$key, i$geom$objname, i$params)) + dataframes <- dataframes[which(sapply(dataframes, nrow)>0)] + # Check to make sure datframes is non-empty. If it is empty, return NULL. + if(length(dataframes)>0) { + data <- merge_recurse(dataframes) + } else return(NULL) + data <- lapply(nrow(data):1, function(i) as.list(data[i,])) + if(guidetype=="none"){ + NULL + } else{ + list(guide = guidetype, + geoms = geoms, + title = mb$title, + entries = data) + } +} + +#' Convert R colors to RGB hexadecimal color values +#' @param x character +#' @return hexadecimal color value (if is.na(x), return "none" for compatibility with JavaScript) +#' @export +toRGB <- function(x){ + rgb.matrix <- col2rgb(x) + rgb.text <- apply(rgb.matrix, 2, paste, collapse=",") + rgb.css <- sprintf("rgb(%s)", rgb.text) + ifelse(is.na(x), "none", rgb.css) +} + +#' Function to merge a list of data frames (from the reshape package) +#' @param dfs list of data frames +#' @param ... other arguments to merge +#' @return data frame of merged lists +merge_recurse = function (dfs, ...) +{ + if (length(dfs) == 1) { + dfs[[1]] + } + else if (length(dfs) == 2) { + merge(dfs[[1]], dfs[[2]], all.x = TRUE, sort = FALSE, ...) + } + else { + merge(dfs[[1]], Recall(dfs[-1]), all.x = TRUE, sort = FALSE, + ...) + } +} + diff --git a/R/plotly.R b/R/plotly.R index f819763f48..5c7a2f53f9 100644 --- a/R/plotly.R +++ b/R/plotly.R @@ -40,8 +40,20 @@ #' response <- py$plot(x,y) #' response$url # view your plot at this URL #' browseURL(response$url) # use browseURL to go to the URL in your browser +#' +#' ## Export ggplots directly to plot.ly. +#' ggiris <- qplot(Petal.Width, Sepal.Length, data=iris, color=Species) +#' py$ggplotly(ggiris) +#' data(canada.cities, package="maps") +#' viz <- ggplot(canada.cities, aes(long, lat))+ +#' borders(regions="canada", name="borders")+ +#' coord_equal()+ +#' geom_point(aes(text=name, size=pop), colour="red", +#' alpha=1/2, name="cities") +#' py$ggplotly(viz) #' } + plotly <- function(username=NULL, key=NULL){ if(is.null(username)) @@ -91,6 +103,17 @@ plotly <- function(username=NULL, key=NULL){ args <- list(...) return(pub$makecall(args = args, kwargs = kwargs, origin = "plot")) } + pub$ggplotly <- function(gg){ + if(!is.ggplot(gg)){ + stop("gg must be a ggplot") + } + pargs <- gg2list(gg) + resp <- do.call(pub$plotly, pargs) + if(interactive()){ + browseURL(resp$url) + } + invisible(list(data=pargs, response=resp)) + } pub$iplot <- function(..., kwargs = list(filename = NULL, fileopt = NULL)) { # Embed plotly graphs as iframes for knitr documents r <- pub$plotly(..., kwargs = kwargs) diff --git a/inst/tests/ggplots.R b/inst/tests/ggplots.R new file mode 100644 index 0000000000..1f6b1912a4 --- /dev/null +++ b/inst/tests/ggplots.R @@ -0,0 +1,10 @@ +context("do not generate") + +test_that("size is not generated if it is not specified",{ + iplot <- ggplot(iris)+ + geom_point(aes(Petal.Width, Sepal.Width)) + L <- gg2list(iplot) + m <- L[[1]]$marker + expect_that(m, is_a("list")) + expect_that(m$size, is_a("NULL")) +}) diff --git a/man/aes2line.Rd b/man/aes2line.Rd new file mode 100644 index 0000000000..b779bd69ea --- /dev/null +++ b/man/aes2line.Rd @@ -0,0 +1,15 @@ +\docType{data} +\name{aes2line} +\alias{aes2line} +\title{Convert ggplot2 aes to line parameters.} +\format{\preformatted{ Named chr [1:4] "dash" "color" "width" "text" + - attr(*, "names")= chr [1:4] "linetype" "colour" "size" "text" +}} +\usage{ +aes2line +} +\description{ +Convert ggplot2 aes to line parameters. +} +\keyword{datasets} + diff --git a/man/aes2marker.Rd b/man/aes2marker.Rd new file mode 100644 index 0000000000..e8b8ae6ca0 --- /dev/null +++ b/man/aes2marker.Rd @@ -0,0 +1,15 @@ +\docType{data} +\name{aes2marker} +\alias{aes2marker} +\title{Convert ggplot2 aes to plotly "marker" codes.} +\format{\preformatted{ Named chr [1:6] "opacity" "symbol" "color" "size" "symbol" ... + - attr(*, "names")= chr [1:6] "alpha" "pch" "colour" "size" ... +}} +\usage{ +aes2marker +} +\description{ +Convert ggplot2 aes to plotly "marker" codes. +} +\keyword{datasets} + diff --git a/man/getLegend.Rd b/man/getLegend.Rd new file mode 100644 index 0000000000..b5f891c1a1 --- /dev/null +++ b/man/getLegend.Rd @@ -0,0 +1,17 @@ +\name{getLegend} +\alias{getLegend} +\title{Function to get legend information for each scale} +\usage{ +getLegend(mb) +} +\arguments{ + \item{mb}{single entry from ggplot2:::guides_merge() list + of legend data} +} +\value{ +list of legend information, NULL if guide=FALSE. +} +\description{ +Function to get legend information for each scale +} + diff --git a/man/getLegendList.Rd b/man/getLegendList.Rd new file mode 100644 index 0000000000..26ea84803c --- /dev/null +++ b/man/getLegendList.Rd @@ -0,0 +1,16 @@ +\name{getLegendList} +\alias{getLegendList} +\title{Get legend information.} +\usage{ +getLegendList(plistextra) +} +\arguments{ + \item{plistextra}{output from ggplot2::ggplot_build(p)} +} +\value{ +list containing information for each legend +} +\description{ +Get legend information. +} + diff --git a/man/gg2list.Rd b/man/gg2list.Rd new file mode 100644 index 0000000000..43a26a8980 --- /dev/null +++ b/man/gg2list.Rd @@ -0,0 +1,16 @@ +\name{gg2list} +\alias{gg2list} +\title{Convert a ggplot to a list.} +\usage{ +gg2list(p) +} +\arguments{ + \item{p}{ggplot2 plot.} +} +\value{ +list representing a ggplot. +} +\description{ +Convert a ggplot to a list. +} + diff --git a/man/group2trace.Rd b/man/group2trace.Rd new file mode 100644 index 0000000000..8505309c56 --- /dev/null +++ b/man/group2trace.Rd @@ -0,0 +1,23 @@ +\name{group2trace} +\alias{group2trace} +\title{Convert 1 ggplot2 group to 1 plotly trace.} +\usage{ +group2trace(df, params, geom) +} +\arguments{ + \item{df}{data.frame.} + + \item{params}{list of defaults.} + + \item{geom}{length 1 character.} +} +\value{ +a list to be passed to plotly(). +} +\description{ +Convert 1 ggplot2 group to 1 plotly trace. +} +\author{ +Toby Dylan Hocking +} + diff --git a/man/layer2list.Rd b/man/layer2list.Rd new file mode 100644 index 0000000000..c25d67fda2 --- /dev/null +++ b/man/layer2list.Rd @@ -0,0 +1,22 @@ +\name{layer2list} +\alias{layer2list} +\title{Convert a layer to a list. Called from gg2list()} +\usage{ +layer2list(l, d, ranges) +} +\arguments{ + \item{l}{one layer of the ggplot object} + + \item{d}{one layer of calculated data from + ggplot2::ggplot_build(p)} + + \item{ranges}{axes ranges} +} +\value{ +list representing a layer, with corresponding aesthetics, +ranges, and groups. +} +\description{ +Convert a layer to a list. Called from gg2list() +} + diff --git a/man/lty2dash.Rd b/man/lty2dash.Rd new file mode 100644 index 0000000000..db13016a4a --- /dev/null +++ b/man/lty2dash.Rd @@ -0,0 +1,15 @@ +\docType{data} +\name{lty2dash} +\alias{lty2dash} +\title{Convert R lty line type codes to plotly "dash" codes.} +\format{\preformatted{ Named chr [1:25] "solid" "dash" "dot" "dashdot" "longdash" ... + - attr(*, "names")= chr [1:25] "1" "2" "3" "4" ... +}} +\usage{ +lty2dash +} +\description{ +Convert R lty line type codes to plotly "dash" codes. +} +\keyword{datasets} + diff --git a/man/merge_recurse.Rd b/man/merge_recurse.Rd new file mode 100644 index 0000000000..47e6ad3eb2 --- /dev/null +++ b/man/merge_recurse.Rd @@ -0,0 +1,19 @@ +\name{merge_recurse} +\alias{merge_recurse} +\title{Function to merge a list of data frames (from the reshape package)} +\usage{ +merge_recurse(dfs, ...) +} +\arguments{ + \item{dfs}{list of data frames} + + \item{...}{other arguments to merge} +} +\value{ +data frame of merged lists +} +\description{ +Function to merge a list of data frames (from the reshape +package) +} + diff --git a/man/pch2symbol.Rd b/man/pch2symbol.Rd new file mode 100644 index 0000000000..6caca6d688 --- /dev/null +++ b/man/pch2symbol.Rd @@ -0,0 +1,15 @@ +\docType{data} +\name{pch2symbol} +\alias{pch2symbol} +\title{Convert R pch point codes to plotly "symbol" codes.} +\format{\preformatted{ Named chr [1:20] "square" "circle" "triangle-up" "cross" "x" ... + - attr(*, "names")= chr [1:20] "0" "1" "2" "3" ... +}} +\usage{ +pch2symbol +} +\description{ +Convert R pch point codes to plotly "symbol" codes. +} +\keyword{datasets} + diff --git a/man/plotly-package.Rd b/man/plotly-package.Rd index 5c0e7ab60a..1d756d27b6 100644 --- a/man/plotly-package.Rd +++ b/man/plotly-package.Rd @@ -10,10 +10,10 @@ desktop R environment. } \details{ An example of an interactive graph made from the R API: -https://plot.ly/ ~chris/407/ +https://plot.ly/~chris/407/ \itemize{ \item Package: plotly \item Type: Package \item -Version: 0.3.4 \item Date: 2014-03-06 \item License: MIT } +Version: 0.3.4 \item Date: 2014-03-07 \item License: MIT } } \section{Authentication}{ There are a few different options. First, you can pass in diff --git a/man/plotly.Rd b/man/plotly.Rd index 1e80b5e74b..9cda7c4651 100644 --- a/man/plotly.Rd +++ b/man/plotly.Rd @@ -45,6 +45,17 @@ y <- c(10,11,12) response <- py$plot(x,y) response$url # view your plot at this URL browseURL(response$url) # use browseURL to go to the URL in your browser + +## Export ggplots directly to plot.ly. +ggiris <- qplot(Petal.Width, Sepal.Length, data=iris, color=Species) +py$ggplotly(ggiris) +data(canada.cities, package="maps") +viz <- ggplot(canada.cities, aes(long, lat))+ + borders(regions="canada", name="borders")+ + coord_equal()+ + geom_point(aes(text=name, size=pop), colour="red", + alpha=1/2, name="cities") + py$ggplotly(viz) } } \author{ diff --git a/man/toRGB.Rd b/man/toRGB.Rd new file mode 100644 index 0000000000..1b646e05b5 --- /dev/null +++ b/man/toRGB.Rd @@ -0,0 +1,17 @@ +\name{toRGB} +\alias{toRGB} +\title{Convert R colors to RGB hexadecimal color values} +\usage{ +toRGB(x) +} +\arguments{ + \item{x}{character} +} +\value{ +hexadecimal color value (if is.na(x), return "none" for +compatibility with JavaScript) +} +\description{ +Convert R colors to RGB hexadecimal color values +} + diff --git a/tests/ggplotly.R b/tests/ggplotly.R new file mode 100644 index 0000000000..0eccbbe1fa --- /dev/null +++ b/tests/ggplotly.R @@ -0,0 +1,182 @@ +library(ggplot2) +library(plotly) + +check <- function(gg, expected, name=NULL){ + if(is.null(name)){ + m <- match.call() + name <- as.character(m$gg) + } + list(ggplot=gg, expected=expected, name=name) +} +check.named <- function(expected, generated, trace){ + for(L in list(expected, generated)){ + stopifnot(is.list(L)) + stopifnot(!is.null(names(L))) + if(any(names(L) == "")){ + print(names(L)) + stop("un-named elements") + } + } + for(name in names(expected)){ + this.trace <- c(trace, name) + e <- expected[[name]] + g <- generated[[name]] + bad <- function(msg="did not generate what we expected"){ + if(missing(msg)){ + print(list(expected=e, generated=g)) + } + print(this.trace) + stop(msg) + } + if(is.list(e)){ + if(!is.list(g)){ + bad() + } + check.named(e, g, this.trace) + }else if(is.atomic(e)){ + if(!is.atomic(g) || length(g) != length(e)) { + bad() + } + if(is.numeric(e)){ + if(!is.numeric(g)){ + bad() + } + char.if.different <- all.equal(e, g) + if(is.character(char.if.different)){ + print(rbind(expected=e, generated=g)) + print(char.if.different) + bad("not numerically equal") + } + }else if(is.character(e) || is.factor(e)){ + if(any(e != g)){ + bad() + } + }else{ + print(e) + stop("do not know what to do with this expectation") + } + }else{ + print(e) + stop("do not know what to do with this expectation") + } + } +} + +## Generate lineplot data. +set.seed(1) +n.groups <- 20 +Groups <- data.frame(x=rep(1:10, times=n.groups), + group = rep(1:n.groups, each=10)) +Groups$lt <- c("even", "odd")[(Groups$group%%2+1)] # linetype +Groups$group <- as.factor(Groups$group) +Groups$y <- rnorm(length(Groups$x), Groups$x, .5) + + rep(rnorm(n.groups, 0, 2), each=10) +## Simple black lineplot. +AllBlack <- ggplot(Groups) + + geom_line(aes(x=x, y=y, group=group)) + + ggtitle("geom_line") +group.list <- split(Groups, Groups$group) +AllBlack.expected <- list() +for(group.i in seq_along(group.list)){ + g <- group.list[[group.i]] + AllBlack.expected[[group.i]] <- + list(x=g$x, y=g$y, type="scatter", mode="lines", + line=list(color="black")) +} +## A ggplot with 6 different automatic types should be converted to +## plotly's 6 types. +Types <- ggplot(subset(Groups, as.integer(group)<=6)) + + geom_line(aes(x=x, y=y, group=group, linetype=group))+ + ggtitle("geom_line + scale_linetype automatic") +Types.expected <- AllBlack.expected[1:6] +dash <- + c("solid", + "dash", + "dot", + "dashdot", + "longdash", + "longdashdot") +for(trace.i in seq_along(Types.expected)){ + Types.expected[[trace.i]]$line$dash <- dash[[trace.i]] +} + + +## Canada city population map. +library(maps) +data(canada.cities) +DefaultCities <- ggplot(canada.cities, aes(long, lat))+ + borders(regions="canada", name="borders")+ + coord_equal()+ + geom_point(aes(text=name, size=pop), colour="red", + alpha=1/2, name="cities") +b <- borders(regions="canada")$data +group.list <- split(b, b$group) +line.df <- data.frame() +for(group.i in seq_along(group.list)){ + g <- group.list[[group.i]] + line.df <- rbind(line.df, g, NA) +} +normalize <- function(x, m, M){ + x <- na.omit(x) + zero.one <- (x-min(x))/(max(x)-min(x)) + stopifnot(range(zero.one) == c(0,1)) + m.M <- zero.one*(M-m) + m + stopifnot(range(m.M) == c(m, M)) + m.M +} +DefaultCities.expected <- + list(list(x=line.df$long, y=line.df$lat, + type="scatter", mode="lines", name="borders", + line=list(dash="solid", color="grey50")), + with(canada.cities,{ + list(x=long, y=lat, text=name, type="scatter", mode="markers", + name="cities", + marker=list(opacity=1/2, color="red", size=pop)) + })) +## different ways to define the iris scatterplot, these should all +## give the same result. +iris.plots <- + list(global=ggplot(iris,aes(Petal.Width, Sepal.Width, color=Species))+ + geom_point(), + point=ggplot(iris)+ + geom_point(aes(Petal.Width, Sepal.Width, color=Species)), + qplot=qplot(Petal.Width, Sepal.Width, color=Species, data=iris)) +## different ways to define color, these should all give the same result. +color.synonyms <- + list(color=qplot(Petal.Width, Sepal.Width, color=Species, data=iris), + colour=qplot(Petal.Width, Sepal.Width, colour=Species, data=iris), + col=qplot(Petal.Width, Sepal.Width, col=Species, data=iris)) +igroups <- split(iris, iris$Sp) +iris.expected <- list() +colors3 <- c("rgb(0,186,56)","rgb(248,118,109)","rgb(97,156,255)") +inames <- c("versicolor", "setosa", "virginica") +for(species.i in seq_along(inames)){ + iname <- inames[[species.i]] + sp <- igroups[[iname]] + iris.expected[[species.i]] <- + list(x=sp$Petal.Width, y=sp$Sepal.Width, name=as.character(sp$Sp[1]), + type="scatter", mode="markers", + marker=list(color=rep(colors3[[species.i]], nrow(sp)))) +} +## Checklist. +to.check <- + list(check(AllBlack, AllBlack.expected), + check(DefaultCities, DefaultCities.expected), + check(Types, Types.expected)) +## TODO: check.unordered function! +for(name in names(iris.plots)){ + full.name <- sprintf("iris.%s", name) + to.check[[length(to.check)+1]] <- + check(iris.plots[[name]], iris.expected, full.name) +} + +for(L in to.check){ + generated <- gg2list(L$gg) + for(trace.i in seq_along(L$expected)){ + e <- L$exp[[trace.i]] + g <- generated[[trace.i]] + check.named(e, g, c(L$name, trace.i)) + } +} + + diff --git a/tests/testthat.R b/tests/testthat.R new file mode 100644 index 0000000000..1af593b42b --- /dev/null +++ b/tests/testthat.R @@ -0,0 +1,3 @@ +library(testthat) +library(plotly) +test_package("plotly")