diff --git a/DESCRIPTION b/DESCRIPTION index b1f9c1db81..c6ec3ebd62 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: plotly Type: Package Title: Interactive, publication-quality graphs online. -Version: 0.5.20 +Version: 0.5.21 Authors@R: c(person("Chris", "Parmer", role = c("aut", "cre"), email = "chris@plot.ly"), person("Scott", "Chamberlain", role = "aut", diff --git a/NEWS b/NEWS index 4a5c9c3b12..c5d1e6d708 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,7 @@ +0.5.21 -- 23 February 2015. + +Fixes for error bars and tick marks. + 0.5.20 -- 9 February 2015. Add alpha transparency to fill conversion. diff --git a/R/corresp_one_one.R b/R/corresp_one_one.R index 30499350d0..967ddbfcb9 100644 --- a/R/corresp_one_one.R +++ b/R/corresp_one_one.R @@ -81,8 +81,12 @@ coded.lty <- c("22"="dash", "224282F2"="dash", "F1"="dash") +# Convert R lty line type codes to plotly "dash" codes. +lty2dash <- c(numeric.lty, named.lty, coded.lty) + # Convert ggplot2 aes to line parameters. aes2line <- c(linetype="dash", colour="color", size="width", direction="shape") + diff --git a/R/ggplotly.R b/R/ggplotly.R index 868122718f..3b2cf9e378 100644 --- a/R/ggplotly.R +++ b/R/ggplotly.R @@ -1,4 +1,4 @@ -## calc. the epoch +# calc. the epoch now <- Sys.time() the.epoch <- now - as.numeric(now) @@ -26,9 +26,6 @@ ribbon.line.defaults$colour <- NA polygon.line.defaults <- line.defaults polygon.line.defaults$colour <- NA -# Convert R lty line type codes to plotly "dash" codes. -lty2dash <- c(numeric.lty, named.lty, coded.lty) - aesConverters <- list(linetype=function(lty) { lty2dash[as.character(lty)] }, @@ -45,13 +42,15 @@ aesConverters <- list(linetype=function(lty) { direction=identity) markLegends <- - ## NOTE: Do we also want to split on size? - ## Legends based on sizes not implemented yet in Plotly - ## list(point=c("colour", "fill", "shape", "size"), + # NOTE: Do we also want to split on size? + # Legends based on sizes not implemented yet in Plotly + # list(point=c("colour", "fill", "shape", "size"), list(point=c("colour", "fill", "shape"), path=c("linetype", "size", "colour", "shape"), polygon=c("colour", "fill", "linetype", "size", "group"), bar=c("colour", "fill"), + errorbar=c("colour", "linetype"), + errorbarh=c("colour", "linetype"), area=c("colour", "fill"), step=c("linetype", "size", "colour"), boxplot=c("x"), @@ -68,10 +67,10 @@ gg2list <- function(p){ if(length(p$layers) == 0) { stop("No layers in plot") } - ## Always use identity size scale so that plot.ly gets the real - ## units for the size variables. + # Always use identity size scale so that plot.ly gets the real + # units for the size variables. p <- tryCatch({ - ## this will be an error for discrete variables. + # this will be an error for discrete variables. suppressMessages({ ggplot_build(p+scale_size_continuous()) p+scale_size_identity() @@ -82,10 +81,10 @@ gg2list <- function(p){ layout <- list() trace.list <- list() - ## Before building the ggplot, we would like to add aes(name) to - ## figure out what the object group is later. This also copies any - ## needed global aes/data values to each layer, so we do not have to - ## worry about combining global and layer-specific aes/data later. + # Before building the ggplot, we would like to add aes(name) to + # figure out what the object group is later. This also copies any + # needed global aes/data values to each layer, so we do not have to + # worry about combining global and layer-specific aes/data later. for(layer.i in seq_along(p$layers)) { layer.aes <- p$layers[[layer.i]]$mapping to.copy <- names(p$mapping)[!names(p$mapping) %in% names(layer.aes)] @@ -99,9 +98,8 @@ gg2list <- function(p){ } } - ## Extract data from built ggplots + # Extract data from built ggplots built <- ggplot_build2(p) - # Get global x-range now because we need some of its info in layer2traces ggranges <- built$panel$ranges # Extract x.range @@ -121,20 +119,20 @@ gg2list <- function(p){ } for(i in seq_along(built$plot$layers)){ - ## This is the layer from the original ggplot object. + # This is the layer from the original ggplot object. L <- p$layers[[i]] - ## for each layer, there is a correpsonding data.frame which - ## evaluates the aesthetic mapping. + # for each layer, there is a correpsonding data.frame which + # evaluates the aesthetic mapping. df <- built$data[[i]] - ## Test fill and color to see if they encode a quantitative - ## variable. This may be useful for several reasons: (1) it is - ## sometimes possible to plot several different colors in the same - ## trace (e.g. points), and that is faster for large numbers of - ## data points and colors; (2) factors on x or y axes should be - ## sent to plotly as characters, not as numeric data (which is - ## what ggplot_build gives us). + # Test fill and color to see if they encode a quantitative + # variable. This may be useful for several reasons: (1) it is + # sometimes possible to plot several different colors in the same + # trace (e.g. points), and that is faster for large numbers of + # data points and colors; (2) factors on x or y axes should be + # sent to plotly as characters, not as numeric data (which is + # what ggplot_build gives us). misc <- list() for(a in c("fill", "colour", "x", "y")){ for(data.type in c("continuous", "date", "datetime", "discrete")){ @@ -153,7 +151,7 @@ gg2list <- function(p){ } } - ## scales are needed for legend ordering. + # scales are needed for legend ordering. for(sc in p$scales$scales){ a <- sc$aesthetics if(length(a) == 1){ @@ -162,22 +160,32 @@ gg2list <- function(p){ names(ranks) <- br misc$breaks[[sc$aesthetics]] <- ranks } + misc$trans[sc$aesthetics] <- sc$trans$name } + reverse.aes <- names(misc$trans)[misc$trans=="reverse"] - ## get gglayout now because we need some of its info in layer2traces + # get gglayout now because we need some of its info in layer2traces gglayout <- built$panel$layout - ## invert rows so that plotly and ggplot2 show panels in the same order + # invert rows so that plotly and ggplot2 show panels in the same order gglayout$plotly.row <- max(gglayout$ROW) - gglayout$ROW + 1 - ## Add ROW and COL to df: needed to link axes to traces; keep df's - ## original ordering while merging. + # Add ROW and COL to df: needed to link axes to traces; keep df's + # original ordering while merging. df$order <- seq_len(nrow(df)) df <- merge(df, gglayout[, c("PANEL", "plotly.row", "COL")]) df <- df[order(df$order),] df$order <- NULL - misc$prestats.data <- merge(built$prestats.data[[i]], - gglayout[, c("PANEL", "plotly.row", "COL")]) + prestats <- built$prestats.data[[i]] + # scale_reverse multiples x/y data by -1, so here we undo that so + # that the actual data can be uploaded to plotly. + replace.aes <- intersect(names(prestats), reverse.aes) + for (a in replace.aes) { + prestats[[a]] <- -1 * prestats[[a]] + } + misc$prestats.data <- + merge(prestats, + gglayout[, c("PANEL", "plotly.row", "COL")]) # Add global x-range info misc$prestats.data$globxmin <- ggxmin @@ -192,36 +200,17 @@ gg2list <- function(p){ misc$prestats.data$globsizemax <- ggsizemax } - ## This extracts essential info for this geom/layer. + # This extracts essential info for this geom/layer. traces <- layer2traces(L, df, misc) - # Associate error bars with previous traces - if (grepl("errorbar", L$geom$objname)) { - for (j in 1:length(trace.list)) { - temp <- list() - ind <- traces[[1]]$x %in% trace.list[[j]]$x - only_ind <- function(x) x[ind] - if ("errorbarh" %in% L$geom$objname) { - temp <- lapply(traces[[1]]$error_x, only_ind) - # Colour of error bar has to be one string - if (length(temp$color) > 1) temp$color <- temp$color[1] - trace.list[[j]]["error_x"] <- list(temp) - } else { - temp <- lapply(traces[[1]]$error_y, only_ind) - if (length(temp$color) > 1) temp$color <- temp$color[1] - trace.list[[j]]["error_y"] <- list(temp) - } - } - } else { - # Do we really need to coord_transform? - # g$data <- ggplot2:::coord_transform(built$plot$coord, g$data, - # built$panel$ranges[[1]]) - trace.list <- c(trace.list, traces) - } + # Do we really need to coord_transform? + # g$data <- ggplot2:::coord_transform(built$plot$coord, g$data, + # built$panel$ranges[[1]]) + trace.list <- c(trace.list, traces) } - ## for barcharts, verify that all traces have the same barmode; we don't - ## support different barmodes on the same plot yet. + # for barcharts, verify that all traces have the same barmode; we don't + # support different barmodes on the same plot yet. barmodes <- do.call(c, lapply(trace.list, function (x) x$barmode)) barmodes <- barmodes[!is.null(barmodes)] if (length(barmodes) > 0) { @@ -245,36 +234,38 @@ gg2list <- function(p){ } } - ## 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. + # 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 <- built$panel$ranges[[1]] - ## if("flip"%in%attr(built$plot$coordinates, "class")){ - ## temp <- built$plot$labels$x - ## built$plot$labels$x <- built$plot$labels$y - ## built$plot$labels$y <- temp - ## } + # 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 <- built$panel$ranges[[1]] + # if("flip"%in%attr(built$plot$coordinates, "class")){ + # temp <- built$plot$labels$x + # built$plot$labels$x <- built$plot$labels$y + # built$plot$labels$y <- temp + # } e <- function(el.name){ ggplot2::calc_element(el.name, p$theme) } is.blank <- function(el.name, null.is.blank=FALSE) { - ## NULL shows ticks and hides borders + # NULL shows ticks and hides borders cls <- attr(e(el.name),"class") "element_blank" %in% cls || null.is.blank && is.null(cls) } + trace.order.list <- list() + trace.name.map <- c() for(xy in c("x","y")){ ax.list <- list() s <- function(tmp)sprintf(tmp, xy) ax.list$tickcolor <- toRGB(theme.pars$axis.ticks$colour) - ## When gridlines are dotted or dashed: + # When gridlines are dotted or dashed: grid <- theme.pars$panel.grid grid.major <- theme.pars$panel.grid.major if ((!is.null(grid$linetype) || !is.null(grid.major$linetype)) && @@ -287,9 +278,9 @@ gg2list <- function(p){ } ax.list$showgrid <- !is.blank(s("panel.grid.major.%s")) - ## These numeric length variables are not easily convertible. - ##ax.list$gridwidth <- as.numeric(theme.pars$panel.grid.major$size) - ##ax.list$ticklen <- as.numeric(theme.pars$axis.ticks.length) + # These numeric length variables are not easily convertible. + #ax.list$gridwidth <- as.numeric(theme.pars$panel.grid.major$size) + #ax.list$ticklen <- as.numeric(theme.pars$axis.ticks.length) theme2font <- function(text){ if(!is.null(text)){ @@ -315,10 +306,37 @@ gg2list <- function(p){ } ax.list$tickfont <- theme2font(tick.text) - ## Translate axes labels. + # Translate axes labels. scale.i <- which(p$scales$find(xy)) ax.list$title <- if(length(scale.i)){ sc <- p$scales$scales[[scale.i]] + trace.order.list[[xy]] <- sc$limits + trace.name.map[sc$breaks] <- sc$labels + if (is.null(sc$breaks)) { + ax.list$showticklabels <- FALSE + ax.list$showgrid <- FALSE + ax.list$ticks <- "" + } + if (is.numeric(sc$breaks)) { + dticks <- diff(sc$breaks) + dt <- dticks[1] + if(all(dticks == dt)){ + ax.list$dtick <- dt + ax.list$autotick <- FALSE + } + } + ax.list$range <- if(!is.null(sc$limits)){ + sc$limits + }else{ + if(misc$is.continuous[[xy]]){ + ggranges[[1]][[s("%s.range")]] #TODO: facets! + }else{ # for a discrete scale, range should be NULL. + NULL + } + } + if(is.character(sc$trans$name) && sc$trans$name == "reverse"){ + ax.list$range <- sort(-ax.list$range, decreasing = TRUE) + } if(!is.null(sc$name)){ sc$name }else{ @@ -327,6 +345,7 @@ gg2list <- function(p){ }else{ p$labels[[xy]] } + title.text <- e(s("axis.title.%s")) ax.list$titlefont <- theme2font(title.text) ax.list$type <- if(misc$is.continuous[[xy]]){ @@ -344,17 +363,16 @@ gg2list <- function(p){ ax.list$showline <- !is.blank("panel.border", TRUE) ax.list$linecolor <- toRGB(theme.pars$panel.border$colour) ax.list$linewidth <- theme.pars$panel.border$size - ## Some other params that we used in animint but we don't yet - ## translate to plotly: + # Some other params that we used in animint but we don't yet + # translate to plotly: !is.blank(s("axis.line.%s")) layout[[s("%saxis")]] <- ax.list } - - ## copy [x/y]axis to [x/y]axisN and set domain, range, etc. for each + # copy [x/y]axis to [x/y]axisN and set domain, range, etc. for each xaxis.title <- layout$xaxis$title yaxis.title <- layout$yaxis$title - inner.margin <- 0.01 ## between facets - outer.margin <- 0.05 ## to put titles outside of the plots + inner.margin <- 0.01 # between facets + outer.margin <- 0.05 # to put titles outside of the plots orig.xaxis <- layout$xaxis orig.yaxis <- layout$yaxis if (nrow(gglayout) > 1) @@ -398,7 +416,7 @@ gg2list <- function(p){ } } - ## add panel titles as annotations + # add panel titles as annotations annotations <- list() nann <- 1 make.label <- function(text, x, y, xanchor="auto", yanchor="auto", textangle=0) @@ -442,8 +460,8 @@ gg2list <- function(p){ } } - ## add empty traces everywhere so that the background shows even if there - ## is no data for a facet + # add empty traces everywhere so that the background shows even if there + # is no data for a facet for (r in seq_len(max(gglayout$ROW))) for (c in seq_len(max(gglayout$COL))) trace.list <- c(trace.list, list(list(xaxis=paste0("x", c), yaxis=paste0("y", r), showlegend=FALSE))) @@ -468,7 +486,7 @@ gg2list <- function(p){ } } - ## axes titles + # axes titles annotations[[nann]] <- make.label(xaxis.title, 0.5, -outer.margin, @@ -482,17 +500,17 @@ gg2list <- function(p){ layout$annotations <- annotations } - ## Remove legend if theme has no legend position + # Remove legend if theme has no legend position layout$showlegend <- !(theme.pars$legend.position=="none") - ## Main plot title. + # Main plot title. layout$title <- built$plot$labels$title - ## Background color. + # Background color. layout$plot_bgcolor <- toRGB(theme.pars$panel.background$fill) layout$paper_bgcolor <- toRGB(theme.pars$plot.background$fill) - ## Legend. + # Legend. layout$margin$r <- 10 if (exists("increase_margin_r")) { layout$margin$r <- 60 @@ -533,23 +551,23 @@ gg2list <- function(p){ layout$annotations <- annotations } - ## Family font for text + # Family font for text if (!is.null(theme.pars$text$family)) { layout$titlefont$family <- theme.pars$text$family layout$legend$font$family <- theme.pars$text$family } - ## Family font for title + # Family font for title if (!is.null(theme.pars$plot.title$family)) { layout$titlefont$family <- theme.pars$plot.title$family } - ## Family font for legend + # Family font for legend if (!is.null(theme.pars$legend.text$family)) { layout$legend$font$family <- theme.pars$legend.text$family } - ## Bold, italic and bold.italic face for text + # Bold, italic and bold.italic face for text text_face <- theme.pars$text$face if (!is.null(text_face)) { if (text_face=="bold") { @@ -567,7 +585,7 @@ gg2list <- function(p){ } } - ## Bold, italic and bold.italic face for title + # Bold, italic and bold.italic face for title title_face <- theme.pars$plot.title$face if (!is.null(title_face)) { if (title_face=="bold") { @@ -579,7 +597,7 @@ gg2list <- function(p){ } } - ## Bold, italic, and bold.italic face for axis title + # Bold, italic, and bold.italic face for axis title title_face <- list(theme.pars$axis.title.y$face, theme.pars$axis.title.x$face) sub_elem <- c("yaxis", "xaxis") @@ -613,63 +631,103 @@ gg2list <- function(p){ layout$legend$bgcolor <- toRGB(s(rect_fill)) } - trace.list$kwargs <- list(layout=layout) - - if (length(trace.list) < 2) { + if (length(trace.list) == 0) { stop("No exportable traces") } - - if (length(trace.list) > 2) { - # Maybe some traces should be merged. - nr <- length(trace.list) - 1 - comp <- data.frame(matrix(ncol=2, nrow=nr)) - colnames(comp) <- c("name", "mode") - - for (j in 1:nr) { - # Use lapply to be elegant? - for (d in colnames(comp)) { - try(comp[[d]][j] <- trace.list[[j]][[d]], silent=TRUE) - # "names" might be NULL in trace.list + + mode.mat <- matrix(NA, 3, 3) + rownames(mode.mat) <- colnames(mode.mat) <- c("markers", "lines", "none") + mode.mat["markers", "lines"] <- + mode.mat["lines", "markers"] <- "lines+markers" + mode.mat["markers", "none"] <- mode.mat["none", "markers"] <- "markers" + mode.mat["lines", "none"] <- mode.mat["none", "lines"] <- "lines" + merged.traces <- list() + not.merged <- trace.list + while(length(not.merged)){ + tr <- not.merged[[1]] + not.merged <- not.merged[-1] + # Are there any traces that have not yet been merged, and can be + # merged with tr? + can.merge <- rep(FALSE, l=length(not.merged)) + for(other.i in seq_along(not.merged)){ + other <- not.merged[[other.i]] + criteria <- c() + for(must.be.equal in c("x", "y", "xaxis", "yaxis")){ + other.attr <- other[[must.be.equal]] + tr.attr <- tr[[must.be.equal]] + criteria[[must.be.equal]] <- isTRUE(all.equal(other.attr, tr.attr)) + } + if(all(criteria)){ + can.merge[[other.i]] <- TRUE } } - # Compare the "name"s of the traces (so far naively inherited from layers) - layernames <- unique(comp$name) - if (length(layernames) < nr) { - # Some traces (layers at this stage) have the same "name"s. - for (j in 1:length(layernames)) { - lind <- which(layernames[j] == comp$name) - lmod <- c("lines", "markers") %in% comp$mode[lind] - # Is there one with "mode": "lines" and another with "mode": "markers"? - if (all(lmod)) { - # Data comparison - xcomp <- (trace.list[[lind[1]]]$x == trace.list[[lind[2]]]$x) - ycomp <- (trace.list[[lind[1]]]$y == trace.list[[lind[2]]]$y) - if (all(xcomp) && all(ycomp)) { - # Union of the two traces - keys <- unique(c(names(trace.list[[lind[1]]]), - names(trace.list[[lind[2]]]))) - temp <- setNames(mapply(c, trace.list[[lind[1]]][keys], - trace.list[[lind[2]]][keys]), keys) - # Info is duplicated in fields which are in common - temp <- lapply(temp, unique) - # But unique() is detrimental to line or marker sublist - temp$line <- trace.list[[lind[1]]]$line - temp$marker <- trace.list[[lind[2]]]$marker - # Overwrite x and y to be safe - temp$x <- trace.list[[lind[1]]]$x - temp$y <- trace.list[[lind[1]]]$y - # Specify new one mode - temp$mode <- "lines+markers" - # Keep one trace and remove the other one - trace.list[[lind[1]]] <- temp - trace.list <- trace.list[-lind[2]] - # Update comparison table - comp <- comp[-lind[2], ] - } + to.merge <- not.merged[can.merge] + not.merged <- not.merged[!can.merge] + for(other in to.merge){ + new.mode <- tryCatch({ + mode.mat[tr$mode, other$mode] + }, error=function(e){ + NA + }) + if(is.character(new.mode) && !is.na(new.mode)){ + tr$mode <- new.mode + } + attrs <- c("error_x", "error_y", "marker", "line") + for(attr in attrs){ + if(!is.null(other[[attr]]) && is.null(tr[[attr]])){ + tr[[attr]] <- other[[attr]] } } } + merged.traces[[length(merged.traces)+1]] <- tr + } + + # Put the traces in correct order, according to any manually + # specified scales. + trace.order <- unlist(trace.order.list) + ordered.traces <- if(length(trace.order)){ + trace.order.score <- seq_along(trace.order) + names(trace.order.score) <- trace.order + trace.name <- sapply(merged.traces, "[[", "name") + trace.score <- trace.order.score[trace.name] + merged.traces[order(trace.score)] + }else{ + merged.traces + } + + # Translate scale(labels) to trace name. + named.traces <- ordered.traces + for(trace.i in seq_along(named.traces)){ + tr.name <- named.traces[[trace.i]][["name"]] + new.name <- trace.name.map[[tr.name]] + if(!is.null(new.name)){ + named.traces[[trace.i]][["name"]] <- new.name + } + } + + # If coord_flip is defined, then flip x/y in each trace, and in + # each axis. + flipped.traces <- named.traces + flipped.layout <- layout + if("flip" %in% attr(built$plot$coordinates, "class")){ + if(!inherits(p$facet, "null")){ + stop("coord_flip + facet conversion not supported") + } + for(trace.i in seq_along(flipped.traces)){ + tr <- flipped.traces[[trace.i]] + x <- tr[["x"]] + y <- tr[["y"]] + tr[["y"]] <- x + tr[["x"]] <- y + flipped.traces[[trace.i]] <- tr + } + x <- layout[["xaxis"]] + y <- layout[["yaxis"]] + flipped.layout[["xaxis"]] <- y + flipped.layout[["yaxis"]] <- x } + + flipped.traces$kwargs <- list(layout=flipped.layout) - trace.list + flipped.traces } diff --git a/R/marker_conversion.R b/R/marker_conversion.R index 9a50be51be..bc5af2f073 100644 --- a/R/marker_conversion.R +++ b/R/marker_conversion.R @@ -1,10 +1,10 @@ -##' Convert ggplot params to plotly. -##' @param params named list ggplot names -> values. -##' @param aesVec vector mapping ggplot names to plotly names. -##' @param defaults named list ggplot names -> values. -##' @export -##' @return named list. -##' @author Toby Dylan Hocking +#' Convert ggplot params to plotly. +#' @param params named list ggplot names -> values. +#' @param aesVec vector mapping ggplot names to plotly names. +#' @param defaults named list ggplot names -> values. +#' @export +#' @return named list. +#' @author Toby Dylan Hocking paramORdefault <- function(params, aesVec, defaults) { marker <- list() for (ggplot.name in names(aesVec)) { diff --git a/R/plotly.R b/R/plotly.R index 7e86ed3db3..50c73631b8 100644 --- a/R/plotly.R +++ b/R/plotly.R @@ -230,7 +230,7 @@ For more help, see https://plot.ly/R or contact .") cat(kwargs) return(pub$makecall(args = args, kwargs = kwargs, origin = "style")) } - ## wrap up the object + # wrap up the object pub <- list2env(pub) class(pub) <- "PlotlyClass" return(pub) diff --git a/R/tools.R b/R/tools.R index 5a97fe2564..6330659b98 100644 --- a/R/tools.R +++ b/R/tools.R @@ -20,7 +20,7 @@ ensure_file_exist <- function(abspath) { } -### Credentials Tools ### +# Credentials Tools ### #' Read Plotly credentials file (which is a JSON) #' @param args Character vector of keys you are looking up @@ -88,7 +88,7 @@ set_credentials_file <- function(username="", api_key="", } -### Config Tools ### +# Config Tools ### #' Read Plotly config file (which is a JSON) and create one if nonexistent #' @param args Character vector of keys you are looking up diff --git a/R/trace_generation.R b/R/trace_generation.R index 3457c78b40..dc8a62d741 100644 --- a/R/trace_generation.R +++ b/R/trace_generation.R @@ -5,10 +5,15 @@ #' @return list representing a layer, with corresponding aesthetics, ranges, and groups. #' @export layer2traces <- function(l, d, misc) { + not.na <- function(df){ + na.mat <- is.na(df) + to.exclude <- apply(na.mat, 1, any) + df[!to.exclude, ] + } g <- list(geom=l$geom$objname, - data=d, - prestats.data=misc$prestats.data) - ## needed for when group, etc. is an expression. + data=not.na(d), + prestats.data=not.na(misc$prestats.data)) + # needed for when group, etc. is an expression. g$aes <- sapply(l$mapping, function(k) as.character(as.expression(k))) # Partial conversion for geom_violin (Plotly does not offer KDE yet) if (g$geom == "violin") { @@ -17,7 +22,7 @@ layer2traces <- function(l, d, misc) { probability density estimation is not supported in Plotly yet.") } - ## Barmode and bargap + # Barmode and bargap barmode <- "group" if (g$geom == "bar" || g$geom == "histogram") { if (l$stat$objname == "bin") { @@ -39,9 +44,9 @@ layer2traces <- function(l, d, misc) { bargap <- 0 } - ## For non-numeric data on the axes, we should take the values from - ## the original data. - for (axis.name in c("x", "y")) { + # For non-numeric data on the axes, we should take the values from + # the original data. + for (axis.name in c("x", "y")) { if (!misc$is.continuous[[axis.name]]) { aes.names <- paste0(axis.name, c("", "end", "min", "max")) aes.used <- aes.names[aes.names %in% names(g$aes)] @@ -61,22 +66,22 @@ layer2traces <- function(l, d, misc) { } # For some plot types, we overwrite `data` with `prestats.data`. - pdata.vec <- misc$prestats.data[[a]] + pdata.vec <- g$prestats.data[[a]] if (inherits(data.vec, "POSIXt")) { - ## Re-create dates from nb seconds + # Re-create dates from nb seconds data.vec <- try(strftime(as.POSIXlt(g$data[[a]], origin=the.epoch), "%Y-%m-%d %H:%M:%S"), silent=TRUE) pdata.vec <- strftime(as.POSIXlt(g$prestats.data[[a]], origin=the.epoch), "%Y-%m-%d %H:%M:%S") } else if (inherits(data.vec, "Date")) { - ## Re-create dates from nb days + # Re-create dates from nb days data.vec <- try(strftime(as.Date(g$data[[a]], origin=the.epoch), "%Y-%m-%d %H:%M:%S"), silent=TRUE) pdata.vec <- strftime(as.Date(g$prestats.data[[a]], origin=the.epoch), "%Y-%m-%d %H:%M:%S") } else if (inherits(data.vec, "factor")) { - ## Re-order data so that Plotly gets it right from ggplot2. + # Re-order data so that Plotly gets it right from ggplot2. g$data <- g$data[order(g$data[[a]]), ] data.vec <- data.vec[match(g$data[[a]], as.numeric(data.vec))] g$prestats.data <- g$prestats.data[order(g$prestats.data[[a]]), ] @@ -92,60 +97,60 @@ layer2traces <- function(l, d, misc) { } } } - ## 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. + # 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. + # non-ggplot2 params like name are useful for plot.ly and ggplot2 + # places them into stat_params. for(p.name in names(g$params)){ - ## c("foo") is translated to "foo" in JSON, so instead we use - ## list("foo") which becomes ["foo"]. However we need to make sure - ## that the list does not have names since list(bar="foo") becomes - ## {"bar":"foo"} + # c("foo") is translated to "foo" in JSON, so instead we use + # list("foo") which becomes ["foo"]. However we need to make sure + # that the list does not have names since list(bar="foo") becomes + # {"bar":"foo"} 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. + # 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 + # 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. + # 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) + # 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 + # symbol=circle,square,diamond,cross,x, + # triangle-up,triangle-down,triangle-left,triangle-right - ## First convert to a "basic" geom, e.g. segments become lines. + # First convert to a "basic" geom, e.g. segments become lines. convert <- toBasic[[g$geom]] basic <- if(is.null(convert)){ g }else{ convert(g) } - ## Then split on visual characteristics that will get different - ## legend entries. + # Then split on visual characteristics that will get different + # legend entries. data.list <- if (basic$geom %in% names(markLegends)) { mark.names <- markLegends[[basic$geom]] - ## However, continuously colored points are an exception: they do - ## not need a legend entry, and they can be efficiently rendered - ## using just 1 trace. + # However, continuously colored points are an exception: they do + # not need a legend entry, and they can be efficiently rendered + # using just 1 trace. - ## Maybe it is nice to show a legend for continuous points? - ## if(basic$geom == "point"){ - ## to.erase <- names(misc$is.continuous)[misc$is.continuous] - ## mark.names <- mark.names[!mark.names %in% to.erase] - ## } + # Maybe it is nice to show a legend for continuous points? + # if(basic$geom == "point"){ + # to.erase <- names(misc$is.continuous)[misc$is.continuous] + # mark.names <- mark.names[!mark.names %in% to.erase] + # } name.names <- sprintf("%s.name", mark.names) - ## split on 'PANEL' to support facets + # split on 'PANEL' to support facets is.split <- names(basic$data) %in% c(name.names, "PANEL") if(any(is.split)){ data.i <- which(is.split) @@ -177,7 +182,7 @@ layer2traces <- function(l, d, misc) { }) } - ## case of no legend, if either of the two ifs above failed. + # case of no legend, if either of the two ifs above failed. if(is.null(data.list)){ data.list <- structure(list(list(data=basic$data, params=basic$params)), names=basic$params$name) @@ -265,8 +270,8 @@ layer2traces <- function(l, d, misc) { # Preprocess data and params. toBasic <- list( segment=function(g){ - ## Every row is one segment, we convert to a line with several - ## groups which can be efficiently drawn by adding NA rows. + # Every row is one segment, we convert to a line with several + # groups which can be efficiently drawn by adding NA rows. g$data$group <- 1:nrow(g$data) used <- c("x", "y", "xend", "yend") others <- g$data[!names(g$data) %in% used] @@ -378,6 +383,35 @@ group2NA <- function(g, geom) { g } +# Make a trace for geom_errorbar -> error_y or geom_errorbarh -> +# error_x. +make.errorbar <- function(data, params, xy){ + tr <- + list(x=data$x, + y=data$y, + type="scatter", + mode="none") + err.name <- paste0("error_", xy) + min.name <- paste0(xy, "min") + max.name <- paste0(xy, "max") + e <- + list(array=data[[max.name]]-data[[xy]], + type="data", + width=params$width, + symmetric=TRUE, + color=if(!is.null(params$colour)){ + toRGB(params$colour) + }else{ + toRGB(data$colour) + }) + arrayminus <- data[[xy]]-data[[min.name]] + if(!isTRUE(all.equal(e$array, arrayminus))){ + e$arrayminus <- arrayminus + e$symmetric <- FALSE + } + tr[[err.name]] <- e + tr +} # Convert basic geoms to traces. geom2trace <- list( @@ -412,7 +446,7 @@ geom2trace <- list( if("size" %in% names(data)){ L$text <- paste("size:", data$size) L$marker$sizeref <- default.marker.sizeref - ## Make sure sizes are passed as a list even when there is only one element. + # Make sure sizes are passed as a list even when there is only one element. s <- data$size marker.size <- 5 * (s - params$sizemin)/(params$sizemax - params$sizemin) + 0.25 marker.size <- marker.size * marker.size.mult @@ -536,18 +570,10 @@ geom2trace <- list( L }, errorbar=function(data, params) { - list(x=data$x, - y=data$y, - error_y=list(arrayminus=data$y-data$ymin, - array=data$ymax-data$y, - color=toRGB(data$colour))) + make.errorbar(data, params, "y") }, errorbarh=function(data, params) { - list(x=data$x, - y=data$y, - error_x=list(arrayminus=data$x-data$xmin, - array=data$xmax-data$x, - color=toRGB(data$colour))) + make.errorbar(data, params, "x") }, area=function(data, params) { list(x=c(data$x[1], data$x, tail(data$x, n=1)), diff --git a/tests/testthat/test-ggplot-errorbar-horizontal.R b/tests/testthat/test-ggplot-errorbar-horizontal.R new file mode 100644 index 0000000000..917fc2d38b --- /dev/null +++ b/tests/testthat/test-ggplot-errorbar-horizontal.R @@ -0,0 +1,27 @@ +context("geom_errorbarh") + +test_that("geom_errorbarh gives horizontal errorbars", { + + df <- data.frame( + trt = factor(c(1, 1, 2, 2)), + resp = c(1, 5, 3, 4), + group = factor(c(1, 2, 1, 2)), + se = c(0.1, 0.3, 0.3, 0.4) + ) + g <- ggplot(df, aes(resp, trt, colour=group)) + geom_point() + # Define the limits of the horizontal errorbars + g <- g + geom_errorbarh(aes(xmax = resp + se, xmin = resp - se)) + + L <- gg2list(g) + + # Expect 2 traces + expect_equal(length(L), 3) + # Expect scatter plot and its error bars to have the same color + expect_identical(L[[1]]$marker$color, L[[1]]$error_x$color) + expect_identical(L[[2]]$marker$color, L[[2]]$error_x$color) + # Expect given errorbar values + expect_equal(L[[1]]$error_x$array, c(0.1, 0.3)) + expect_true(L[[1]]$error_x$symmetric) + + save_outputs(g, "errorbar-horizontal") +}) diff --git a/tests/testthat/test-ggplot-errorbar.R b/tests/testthat/test-ggplot-errorbar.R index 323dab85d4..62f9f61b26 100644 --- a/tests/testthat/test-ggplot-errorbar.R +++ b/tests/testthat/test-ggplot-errorbar.R @@ -19,28 +19,3 @@ test_that("geom_errorbar gives errorbars", { save_outputs(g, "errorbar") }) -test_that("geom_errorbarh gives horizontal errorbars", { - - df <- data.frame( - trt = factor(c(1, 1, 2, 2)), - resp = c(1, 5, 3, 4), - group = factor(c(1, 2, 1, 2)), - se = c(0.1, 0.3, 0.3, 0.4) - ) - g <- ggplot(df, aes(resp, trt, colour=group)) + geom_point() - # Define the limits of the horizontal errorbars - g <- g + geom_errorbarh(aes(xmax = resp + se, xmin = resp - se)) - - L <- gg2list(g) - - # Expect 2 traces - expect_equal(length(L), 3) - # Expect scatter plot and its error bars to have the same color - expect_identical(L[[1]]$marker$color, L[[1]]$error_x$color) - expect_identical(L[[2]]$marker$color, L[[2]]$error_x$color) - # Expect given errorbar values - expect_equal(L[[1]]$error_x$arrayminus, c(0.1, 0.3)) - expect_equal(L[[1]]$error_x$array, L[[1]]$error_x$arrayminus) - - save_outputs(g, "errorbar-horizontal") -}) diff --git a/tests/testthat/test-ggplot-heatmap.R b/tests/testthat/test-ggplot-heatmap.R index 870d94bfdd..1dc941507c 100644 --- a/tests/testthat/test-ggplot-heatmap.R +++ b/tests/testthat/test-ggplot-heatmap.R @@ -4,10 +4,12 @@ wdays <- c("Monday", "Tuesday", "Wednesday", "Thursday", "Friday") dtimes <- c("Morning", "Afternoon", "Evening") workweek <- matrix(c(1, 20, 30, 20, 1, 60, 30, 60, 1, 50, 80, -10, 1, 30, 20), nrow=5, ncol=3, byrow=TRUE, - dimnames=list(wdays, dtimes)) + dimnames=list(day=wdays, time=dtimes)) ww <- reshape2::melt(workweek) +ww$day <- factor(ww$day, wdays) +ww$time <- factor(ww$time, dtimes) # Plot a heatmap using geom_tile -hm <- ggplot(ww) + geom_tile(aes(x=Var1, y=Var2, fill=value)) +hm <- ggplot(ww) + geom_tile(aes(x=day, y=time, fill=value)) test_that("geom_tile is translated to type=heatmap", { L <- gg2list(hm) diff --git a/tests/testthat/test-ggplot-theme.R b/tests/testthat/test-ggplot-theme.R index 74a4720e4c..53968b1d41 100644 --- a/tests/testthat/test-ggplot-theme.R +++ b/tests/testthat/test-ggplot-theme.R @@ -1,6 +1,8 @@ context("ggplot themes") -iris.base <- ggplot(iris) + geom_point(aes(Petal.Width, Sepal.Width)) +iris.base <- ggplot(iris) + + geom_point(aes(Petal.Width, Sepal.Width)) + + theme_grey() test_that("background translated correctly",{ ggiris <- iris.base + theme(panel.background=element_rect(fill="blue")) + @@ -54,10 +56,9 @@ test_that("dotted/dashed grid translated as line with alpha=0.1",{ for (xy in c("x", "y")) { ax.list <- info$kwargs$layout[[paste0(xy, "axis")]] expect_identical(ax.list$gridcolor, toRGB("white", 0.1)) - expect_identical(ax.list$gridcolor, "rgba(255,255,255,0.1)") } - save_outputs(ggiris, "theme-dashed-grid-lines") + save_outputs(ggiris, "theme-dashed-grid-lines") }) countrypop <- data.frame(country=c("Paraguay", "Peru", "Philippines"), @@ -87,8 +88,9 @@ test_that("plot panel border is translated correctly", { save_outputs(ggiris, "theme-panel-border-1") red <- ggplot(iris) + + theme_grey()+ geom_point(aes(Petal.Width, Sepal.Width)) + - theme(panel.border=element_rect(colour="red")) + theme(panel.border=element_rect(colour="red", fill=NA)) info <- gg2list(red) for (xy in c("x", "y")) { ax.list <- info$kwargs$layout[[paste0(xy, "axis")]] diff --git a/tests/testthat/test-ggplot-ticks.R b/tests/testthat/test-ggplot-ticks.R new file mode 100644 index 0000000000..1383af37d8 --- /dev/null +++ b/tests/testthat/test-ggplot-ticks.R @@ -0,0 +1,348 @@ +context("ggplot ticks") + +PlantGrowth$type <- + ifelse(PlantGrowth$group=="ctrl", "control", "treatment") +boxes <- ggplot(PlantGrowth, aes(x=group, y=weight)) + geom_boxplot() + +expect_traces <- function(gg, n.traces, name){ + stopifnot(is.ggplot(gg)) + stopifnot(is.numeric(n.traces)) + save_outputs(gg, paste0("ticks-", name)) + L <- gg2list(gg) + is.trace <- names(L) == "" + all.traces <- L[is.trace] + no.data <- sapply(all.traces, function(tr) { + is.null(tr[["x"]]) && is.null(tr[["y"]]) + }) + has.data <- all.traces[!no.data] + expect_equal(length(has.data), n.traces) + list(traces=has.data, kwargs=L$kwargs) +} + +plant.list <- split(PlantGrowth, PlantGrowth$group) +weight.range <- range(PlantGrowth$weight) + +test_that("boxes without coord_flip()", { + info <- expect_traces(boxes, 3, "boxes") + for(tr in info$traces){ + expect_true(is.null(tr[["x"]])) + expected <- plant.list[[tr$name]]$weight + computed <- tr[["y"]] + expect_equal(computed, expected) + } +}) + +test_that("boxes with facet_grid", { + facets <- boxes + facet_grid(. ~ type) + info <- expect_traces(facets, 3, "boxes-facet-grid") + ## TODO: expect boxes of equal size. + + ## TODO: expect empty space. + for(tr in info$traces){ + expect_true(is.null(tr[["x"]])) + expected <- plant.list[[tr$name]]$weight + computed <- tr[["y"]] + expect_equal(computed, expected) + } +}) + +test_that('boxes with facet_grid(scales="free")', { + facets.scales <- boxes + facet_grid(. ~ type, scales="free") + info <- expect_traces(facets.scales, 3, "boxes-scales-free") + ## TODO: expect boxes of unequal size. + + ## TODO: expect no empty space. + for(tr in info$traces){ + expect_true(is.null(tr[["x"]])) + expected <- plant.list[[tr$name]]$weight + computed <- tr[["y"]] + expect_equal(computed, expected) + } +}) + +test_that('boxes with facet_grid(scales="free", space="free")', { + facets.space <- boxes + facet_grid(. ~ type, scales="free", space="free") + info <- expect_traces(facets.space, 3, "boxes-space-free") + ## TODO: expect boxes of equal size. + + ## TODO: expect no empty space. + for(tr in info$traces){ + expect_true(is.null(tr[["x"]])) + expected <- plant.list[[tr$name]]$weight + computed <- tr[["y"]] + expect_equal(computed, expected) + } +}) + +flipped <- boxes + coord_flip() + +test_that("boxes with coord_flip()", { + info <- expect_traces(flipped, 3, "flip") + for(tr in info$traces){ + expect_true(is.null(tr[["y"]])) + expected <- plant.list[[tr$name]]$weight + computed <- tr[["x"]] + expect_equal(computed, expected) + } +}) + +## coord_flip + facets are not really even supported in ggplot2, so +## these tests are disabled for now. + +test_that("boxes with coord_flip()+facet_grid()", { + flip.facet <- flipped + facet_grid(type ~ .) + ##info <- expect_traces(flip.facet, 3) + ## for(tr in info$traces){ + ## expect_true(is.null(tr[["y"]])) + ## expected <- plant.list[[tr$name]]$weight + ## computed <- tr[["x"]] + ## expect_equal(computed, expected) + ## } +}) + +test_that('boxes with coord_flip()+facet_grid(scales="free")', { + flip.facet.scales <- flipped + facet_grid(type ~ ., scales="free") + ##info <- expect_traces(flip.facet.scales, 3) + ## for(tr in info$traces){ + ## expect_true(is.null(tr[["y"]])) + ## expected <- plant.list[[tr$name]]$weight + ## computed <- tr[["x"]] + ## expect_equal(computed, expected) + ## } +}) + +test_that('boxes+coord_flip()+facet_grid(scales="free", space="free")', { + flip.facet.space <- flipped + + facet_grid(type ~ ., scales="free", space="free") + ## BUG in ggplot2! +}) + +test_that('boxes+facet_grid(scales="free", space="free")+coord_flip()', { + flip.facet.space <- boxes + + facet_grid(type ~ ., scales="free", space="free")+ + coord_flip() + ## BUG in ggplot2! +}) + +test_that("Manually set the order of a discrete-valued axis", { + expected.order <- c("trt1", "ctrl", "trt2") + boxes.limits <- boxes + scale_x_discrete(limits=expected.order) + info <- expect_traces(boxes.limits, 3, "discrete-order") + computed.order <- sapply(info$traces, "[[", "name") + expect_identical(as.character(computed.order), expected.order) +}) + +test_that("limits can hide data", { + expected.order <- c("trt1", "ctrl") + boxes.limits <- boxes + scale_x_discrete(limits=expected.order) + info <- expect_traces(boxes.limits, 2, "limits-hide") + computed.order <- sapply(info$traces, "[[", "name") + expect_identical(as.character(computed.order), expected.order) +}) + +test_that("limits can create a gap", { + expected.order <- c("trt1", "trt2", "GAP", "ctrl") + boxes.limits <- boxes + scale_x_discrete(limits=expected.order) + info <- expect_traces(boxes.limits, 3, "limits-gap") + computed.order <- sapply(info$traces, "[[", "name") + ##expect_identical(as.character(computed.order), expected.order) + + ## TODO: can we make this in plotly? +}) + +boxes.breaks <- boxes + + scale_x_discrete(breaks=c("trt1", "ctrl", "trt2")) + +test_that("setting breaks does not change order", { + info <- expect_traces(boxes.breaks, 3, "breaks-nochange") + computed.labels <- sapply(info$traces, "[[", "name") + expect_identical(as.character(computed.labels), c("ctrl", "trt1", "trt2")) + ## For some reason plotly does not render the third box if range is + ## not NULL. + expect_identical(info$kwargs$layout$xaxis$range, NULL) +}) + +boxes.more <- boxes + + scale_x_discrete(breaks=c("trt1", "ctrl", "trt2", "FOO")) + +test_that("more breaks is fine", { + info <- expect_traces(boxes.more, 3, "breaks-more") + computed.labels <- sapply(info$traces, "[[", "name") + expect_identical(as.character(computed.labels), c("ctrl", "trt1", "trt2")) + ## For some reason plotly does not render the third box if range is + ## not NULL. + expect_identical(info$kwargs$layout$xaxis$range, NULL) +}) + +boxes.less <- boxes + + scale_x_discrete(breaks=c("trt1", "ctrl")) + +test_that("less breaks is fine", { + ## L <- gg2list(boxes.less) + ## sendJSON(L) # 2 boxes + ## sendJSON(L[1:3]) # 3 boxes + ## no.xaxis <- L + ## no.xaxis$kwargs$layout$xaxis <- NULL + ## sendJSON(no.xaxis) # 3 boxes + ## no.xrange <- L + ## no.xrange$kwargs$layout$xaxis$range <- NULL + ## sendJSON(no.xrange) # 3 boxes + info <- expect_traces(boxes.less, 3, "breaks-less") + computed.labels <- sapply(info$traces, "[[", "name") + expect_identical(as.character(computed.labels), c("ctrl", "trt1", "trt2")) + ## For some reason plotly does not render the third box if range is + ## not NULL. + expect_identical(info$kwargs$layout$xaxis$range, NULL) + + ## TODO: as of 20 Feb 2015 it is not possible to make this in + ## plotly. (no boxes but only 2 tick labels) +}) + +boxes.labels <- boxes + + scale_x_discrete(breaks=c("trt1", "ctrl", "trt2"), + labels=c("Treatment 1", "Control", "Treatment 2")) + +test_that("scale(labels) changes trace names", { + info <- expect_traces(boxes.labels, 3, "scale-labels") + computed.labels <- sapply(info$traces, "[[", "name") + expect_identical(as.character(computed.labels), + c("Control", "Treatment 1", "Treatment 2")) + ## For some reason plotly does not render the third box if range is + ## not NULL. + expect_identical(info$kwargs$layout$xaxis$range, NULL) +}) + +no.breaks <- boxes + scale_x_discrete(breaks=NULL) + +test_that("hide x ticks, lines, and labels", { + info <- expect_traces(no.breaks, 3, "hide-ticks-lines-labels") + x <- info$kwargs$layout$xaxis + expect_identical(x[["showticklabels"]], FALSE) + ##expect_identical(x[["showline"]], FALSE) #irrelevant. + expect_identical(x[["showgrid"]], FALSE) + + ## ticks ('' | 'inside' | 'outside') Sets the format of the ticks on + ## this axis. For hidden ticks, link 'ticks' to an empty string. + expect_identical(x[["ticks"]], "") + + ## xaxis has parameter autotick (a boolean: TRUE | FALSE) Toggle + ## whether or not the axis ticks parameters are picked automatically + ## by Plotly. Once 'autotick' is set to FALSE, the axis ticks + ## parameters can be declared with 'ticks', 'tick0', 'dtick0' and + ## other tick-related key in this axis object. + ##expect_identical(x[["autotick"]], FALSE) #not necessary + + ## For some reason plotly does not render the third box if range is + ## not NULL. + expect_identical(info$kwargs$layout$xaxis$range, NULL) +}) + +test_that("Hide X ticks and labels, but keep the gridlines", { + boxes.grid <- boxes + + theme(axis.ticks = element_blank(), axis.text.x = element_blank()) + info <- expect_traces(boxes.grid, 3, "hide-ticks-labels") + x <- info$kwargs$layout$xaxis + expect_identical(x[["showticklabels"]], FALSE) + expect_identical(x[["showgrid"]], TRUE) + expect_identical(x[["ticks"]], "") +}) + +test_that("scale_y_continuous(limits) means yaxis$ranges", { + boxes.range <- boxes + scale_y_continuous(limits=c(0,8)) + info <- expect_traces(boxes.range, 3, "ycontinuous-ranges") + y.axis <- info$kwargs$layout$yaxis + expect_equal(y.axis$range, c(0, 8)) +}) + +test_that("ylim() means yaxis$ranges", { + boxes.range <- boxes + ylim(0,8) + info <- expect_traces(boxes.range, 3, "ylim-ranges") + y.axis <- info$kwargs$layout$yaxis + expect_equal(y.axis$range, c(0, 8)) + ## ensure correct positive values without reverse scale. + for(tr in info$traces){ + expect_true(is.null(tr[["x"]])) + expected <- plant.list[[tr$name]]$weight + computed <- tr[["y"]] + expect_equal(computed, expected) + } +}) + +test_that("scale_y_reverse() -> yaxis$ranges reversed", { + boxes.reverse <- boxes + scale_y_reverse() + info <- expect_traces(boxes.reverse, 3, "yreverse-ranges") + y.axis <- info$kwargs$layout$yaxis + expect_that(y.axis$range[2], is_less_than(y.axis$range[1])) + ## ensure correct positive values, despite the reverse scale. + for(tr in info$traces){ + expect_true(is.null(tr[["x"]])) + expected <- plant.list[[tr$name]]$weight + computed <- tr[["y"]] + expect_equal(computed, expected) + } +}) + +test_that("scale_y_reverse(limits) -> yaxis$ranges reversed", { + y.lim <- c(10, -2) + boxes.reverse <- boxes + scale_y_reverse(limits=y.lim) + info <- expect_traces(boxes.reverse, 3, "yreverse-limits-ranges") + y.axis <- info$kwargs$layout$yaxis + expect_equal(y.axis$range, y.lim) + ## ensure correct positive values, despite the reverse scale. + for(tr in info$traces){ + expect_true(is.null(tr[["x"]])) + expected <- plant.list[[tr$name]]$weight + computed <- tr[["y"]] + expect_equal(computed, expected) + } +}) + +test_that("ylim(reversed) -> yaxis$ranges reversed", { + boxes.reverse <- boxes + ylim(7.5, -1) + info <- expect_traces(boxes.reverse, 3, "ylim-reversed-ranges") + y.axis <- info$kwargs$layout$yaxis + expect_equal(y.axis$range, c(7.5, -1)) + ## ensure correct positive values, despite the reverse scale. + for(tr in info$traces){ + expect_true(is.null(tr[["x"]])) + expected <- plant.list[[tr$name]]$weight + computed <- tr[["y"]] + expect_equal(computed, expected) + } +}) + +test_that("Set the X tick mark locations", { + ## This will show tick marks on every 0.25 from 1 to 10. The scale will + ## show only the ones that are within range (3.50-6.25 in this case) + boxes.ticks <- boxes + scale_y_continuous(breaks=seq(1,10,1/4)) + info <- expect_traces(boxes.ticks, 3, "evenly-spaced-ticks") + y.axis <- info$kwargs$layout$yaxis + expect_equal(y.axis$dtick, 0.25) + expect_identical(y.axis$autotick, FALSE) +}) + +test_that("The breaks can be spaced unevenly", { + boxes.uneven <- boxes + + scale_y_continuous(breaks=c(4, 4.25, 4.5, 5, 6,8)) + ##TODO: is this possible in plotly? + ## https://plot.ly/python/reference/#YAxis +}) + +test_that("hide y ticks, lines, and labels", { + no.breaks <- boxes + scale_y_continuous(breaks=NULL) + info <- expect_traces(no.breaks, 3, "hide-y") + y.axis <- info$kwargs$layout$yaxis + expect_identical(y.axis[["showgrid"]], FALSE) + expect_identical(y.axis[["ticks"]], "") + expect_identical(y.axis[["showticklabels"]], FALSE) +}) + +test_that("hide y ticks and labels, but keep the gridlines", { + boxes.ygrid <- boxes + + theme(axis.ticks = element_blank(), axis.text.y = element_blank()) + info <- expect_traces(boxes.ygrid, 3, "hide-y-keep-grid") + y.axis <- info$kwargs$layout$yaxis + expect_identical(y.axis[["showgrid"]], TRUE) + expect_identical(y.axis[["ticks"]], "") + expect_identical(y.axis[["showticklabels"]], FALSE) +}) diff --git a/tests/testthat/test-mean-error-bars.R b/tests/testthat/test-mean-error-bars.R new file mode 100644 index 0000000000..45dff776ae --- /dev/null +++ b/tests/testthat/test-mean-error-bars.R @@ -0,0 +1,231 @@ +context("means and error bars") + +one.line.df <- + data.frame( + x = c(1, 2, 3, 4), + y = c(2, 1, 3, 4), + array = c(0.1, 0.2, 0.1, 0.1), + arrayminus = c(0.2, 0.4, 1, 0.2)) + +none.json <- list( + list( + x = c(1, 2, 3, 4), + y = c(2, 1, 3, 4), + error_y = list( + type = "data", + symmetric = FALSE, + array = c(0.1, 0.2, 0.1, 0.1), + arrayminus = c(0.2, 0.4, 1, 0.2) + ), + type = "scatter", + mode = "none" + ) +) + +test_that("only asymmetric error bars", { + error.gg <- ggplot(one.line.df, aes(x, y))+ + geom_errorbar(aes(ymin=y-arrayminus, ymax=y+array)) + generated.json <- gg2list(error.gg) + is.trace <- names(generated.json) == "" + traces <- generated.json[is.trace] + expect_identical(length(traces), 1L) + tr <- traces[[1]] + expect_identical(tr$mode, "none") + expect_identical(tr$type, "scatter") + ey <- tr$error_y + expect_identical(ey$type, "data") + expect_identical(ey$symmetric, FALSE) + expect_equal(ey$array, c(0.1, 0.2, 0.1, 0.1)) + expect_equal(ey$arrayminus, c(0.2, 0.4, 1, 0.2)) +}) + +one.line.json <- list( + list( + x = c(1, 2, 3, 4), + y = c(2, 1, 3, 4), + error_y = list( + type = "data", + symmetric = FALSE, + array = c(0.1, 0.2, 0.1, 0.1), + arrayminus = c(0.2, 0.4, 1, 0.2) + ), + type = "scatter" + ) +) + +test_that("asymmetric error bars, geom_errorbar last", { + one.line.gg <- ggplot(one.line.df, aes(x, y))+ + geom_line()+ + geom_point()+ + geom_errorbar(aes(ymin=y-arrayminus, ymax=y+array)) + generated.json <- gg2list(one.line.gg) + ## when there is 1 trace with error bars, lines, and markers, plotly + ## shows error bars in the background, lines in the middle and + ## markers in front. + is.trace <- names(generated.json) == "" + traces <- generated.json[is.trace] + expect_identical(length(traces), 1L) + tr <- traces[[1]] + expect_identical(tr$mode, "lines+markers") + expect_identical(tr$type, "scatter") + ey <- tr$error_y + expect_identical(ey$type, "data") + expect_identical(ey$symmetric, FALSE) + expect_equal(ey$array, c(0.1, 0.2, 0.1, 0.1)) + expect_equal(ey$arrayminus, c(0.2, 0.4, 1, 0.2)) +}) + +test_that("asymmetric error bars, geom_errorbar first", { + one.line.gg <- ggplot(one.line.df, aes(x, y))+ + geom_errorbar(aes(ymin=y-arrayminus, ymax=y+array))+ + geom_line()+ + geom_point() + generated.json <- gg2list(one.line.gg) + is.trace <- names(generated.json) == "" + traces <- generated.json[is.trace] + expect_identical(length(traces), 1L) + tr <- traces[[1]] + expect_identical(tr$mode, "lines+markers") + expect_identical(tr$type, "scatter") + ey <- tr$error_y + expect_identical(ey$type, "data") + expect_identical(ey$symmetric, FALSE) + expect_equal(ey$array, c(0.1, 0.2, 0.1, 0.1)) + expect_equal(ey$arrayminus, c(0.2, 0.4, 1, 0.2)) +}) + +colors.json <- list( + list( + x = c(1, 2, 3, 4), + y = c(2, 1, 3, 4), + error_y = list( + type = "data", + symmetric = FALSE, + array = c(0.1, 0.2, 0.1, 0.1), + arrayminus = c(0.2, 0.4, 1, 0.2), + color="red" + ), + type = "scatter", + marker=list(color="blue", size=14), + line=list(color="violet") + ) +) + +test_that("different colors for error bars, points, and lines", { + one.line.gg <- ggplot(one.line.df, aes(x, y))+ + geom_errorbar(aes(ymin=y-arrayminus, ymax=y+array), color="red")+ + geom_line(color="violet")+ + geom_point(color="blue", size=14) + generated.json <- gg2list(one.line.gg) + is.trace <- names(generated.json) == "" + traces <- generated.json[is.trace] + expect_identical(length(traces), 1L) + tr <- traces[[1]] + expect_identical(tr$mode, "lines+markers") + expect_identical(tr$type, "scatter") + expect_identical(tr$marker$color, toRGB("blue")) + expect_identical(tr$marker$size, 14) + expect_identical(tr$line$color, toRGB("violet")) + ey <- tr$error_y + expect_identical(ey$type, "data") + expect_identical(ey$color, toRGB("red")) + expect_identical(ey$symmetric, FALSE) + expect_equal(ey$array, c(0.1, 0.2, 0.1, 0.1)) + expect_equal(ey$arrayminus, c(0.2, 0.4, 1, 0.2)) +}) + +## from https://github.com/chriddyp/ggplot2-plotly-cookbook/blob/a45f2c70b7adf484e0b0eb8810a1e59e018adbb8/means_and_error_bars.R#L162-L191 +df <- ToothGrowth +## Summarizes data. +## Gives count, mean, standard deviation, standard error of the mean, and confidence interval (default 95%). +## data: a data frame. +## measurevar: the name of a column that contains the variable to be summariezed +## groupvars: a vector containing names of columns that contain grouping variables +## na.rm: a boolean that indicates whether to ignore NA's +## conf.interval: the percent range of the confidence interval (default is 95%) +summarySE <- function(data=NULL, measurevar, groupvars=NULL, na.rm=FALSE, + conf.interval=.95, .drop=TRUE) { + require(plyr) + length2 <- function (x, na.rm=FALSE) { + if (na.rm) sum(!is.na(x)) + else length(x) + } + datac <- ddply(data, groupvars, .drop=.drop, + .fun = function(xx, col) { + c(N = length2(xx[[col]], na.rm=na.rm), + mean = mean (xx[[col]], na.rm=na.rm), + sd = sd (xx[[col]], na.rm=na.rm) + ) + }, + measurevar + ) + datac <- rename(datac, c("mean" = measurevar)) + datac$se <- datac$sd / sqrt(datac$N) # Calculate standard error of the mean + ciMult <- qt(conf.interval/2 + .5, datac$N-1) + datac$ci <- datac$se * ciMult + return(datac) +} + +dfc <- summarySE(df, measurevar="len", groupvars=c("supp","dose")) +color.code <- c(OJ="orange", VC="violet") +supp.list <- split(dfc, dfc$supp) + +test_that("errorbar(aes(color)) + other geoms", { + before <- + ggplot(dfc, aes(x=dose, y=len, colour=supp)) + + geom_errorbar(aes(ymin=len-se, ymax=len+se), width=.1) + + geom_line() + + scale_color_manual(values=color.code)+ + geom_point() + + before.json <- gg2list(before) + is.trace <- names(before.json) == "" + traces <- before.json[is.trace] + + expect_identical(length(traces), 2L) + for(tr in traces){ + expected.color <- toRGB(color.code[[tr$name]]) + expected.data <- supp.list[[tr$name]] + expect_identical(tr$mode, "lines+markers") + expect_identical(tr$type, "scatter") + expect_identical(tr$marker$color, expected.color) + expect_identical(tr$line$color, expected.color) + ey <- tr$error_y + expect_identical(ey$type, "data") + expect_identical(ey$color, expected.color) + expect_equal(ey$width, .1) + expect_identical(ey$symmetric, TRUE) + expect_equal(ey$array, expected.data$se) + } +}) + +test_that("other geoms + errorbar(aes(color))", { + after <- + ggplot(dfc, aes(x=dose, y=len, colour=supp)) + + geom_line() + + geom_errorbar(aes(ymin=len-se, ymax=len+se), width=.1) + + geom_point()+ + scale_color_manual(values=color.code) + + after.json <- gg2list(after) + is.trace <- names(after.json) == "" + traces <- after.json[is.trace] + + expect_identical(length(traces), 2L) + for(tr in traces){ + expected.color <- toRGB(color.code[[tr$name]]) + expected.data <- supp.list[[tr$name]] + expect_identical(tr$mode, "lines+markers") + expect_identical(tr$type, "scatter") + expect_identical(tr$marker$color, expected.color) + expect_identical(tr$line$color, expected.color) + ey <- tr$error_y + expect_identical(ey$type, "data") + expect_identical(ey$color, expected.color) + expect_equal(ey$width, .1) + expect_identical(ey$symmetric, TRUE) + expect_equal(ey$array, expected.data$se) + } +}) + + diff --git a/tests/testthat/test-rotated-ticks.R b/tests/testthat/test-rotated-ticks.R new file mode 100644 index 0000000000..92c3b8bfce --- /dev/null +++ b/tests/testthat/test-rotated-ticks.R @@ -0,0 +1,72 @@ +context("rotated ticks") + +ss <- data.frame(State=paste("some long text", c("CA", "NY", "TX")), + Prop.Inv=c(0, 1, 0.7), + Year=c(1984, 2015, 1999)) + +fg <- ggplot() + + geom_point(aes(x=State, y=Prop.Inv), data=ss) + + xlab("STATE SOME REALLY REALLY LONG TEXT THAT MAY OVERLAP TICKS") + +## TODO: change the details of getTicks and expect_rotate_anchor to +## test plotly web pages. +getTicks <- function(html, p.name){ + xp <- sprintf('//svg[@id="%s"]//g[@id="xaxis"]//text', p.name) + nodes <- getNodeSet(html, xp) + stopifnot(length(nodes) > 0) + sapply(nodes, xmlAttrs) +} +expect_rotate_anchor <- function(info, rotate, anchor){ + return()#TODO:remove. + not <- getTicks(info$html, 'not') + expect_match(not["style", ], "text-anchor: middle", fixed=TRUE) + expect_match(not["transform", ], "rotate(0", fixed=TRUE) + rotated <- getTicks(info$html, 'rotated') + expect_match(rotated["style", ], paste("text-anchor:", anchor), fixed=TRUE) + expect_match(rotated["transform", ], paste0("rotate(", rotate), fixed=TRUE) + + e.axis <- remDr$findElement(using="css selector", "g#xaxis") + e.text <- e.axis$findChildElement("css selector", "text") + tick.loc <- e.text$getElementLocation() + tick.size <- e.text$getElementSize() + ## Subtract a magic number that lets the test pass for un-rotated + ## labels in firefox. + tick.bottom.y <- tick.loc$y + tick.size$height - 6 + e.title <- remDr$findElement("css selector", "text#xtitle") + title.loc <- e.title$getElementLocation() + expect_true(tick.bottom.y < title.loc$y) +} + +## TODO: implement renderHTML which should upload and plot the data, +## then download the rendered HTML using RSelenium to control a +## headless browser. +renderHTML <- function(gg){ + gg2list(gg) +} + +test_that('no axis rotation is fine', { + info <- renderHTML(fg) + expect_rotate_anchor(info, "0", "middle") +}) + +test_that('axis.text.x=element_text(angle=90)"', { + rotated <- fg+theme(axis.text.x=element_text(angle=90)) + info <- renderHTML(rotated) + expect_rotate_anchor(info, "-90", "end") +}) + +test_that('axis.text.x=element_text(angle=70) means transform="rotate(-70)"', { + rotated <- fg+theme(axis.text.x=element_text(angle=70)) + info <- renderHTML(rotated) + expect_rotate_anchor(info, "-70", "end") +}) + +## test_that('hjust=0.75 is an error', { +## problem <- fg+theme(axis.text.x=element_text(hjust=0.75) +## expect_error({ +## info <- renderHTML(problem) +## }, "ggplotly only supports hjust values 0, 0.5, 1") +## }) + + +