diff --git a/R/z_animint.R b/R/z_animint.R index 002cae49b..b214ea8fa 100644 --- a/R/z_animint.R +++ b/R/z_animint.R @@ -214,6 +214,607 @@ storeLayer <- function(meta, g, g.data.varied){ g } +#' Save a layer to disk, save and return meta-data. +#' @param l one layer of the ggplot object. +#' @param d one layer of calculated data from ggplot_build(p). +#' @param meta environment of meta-data. +#' @param layer_name name of layer +#' @param ggplot ggplot +#' @param built built list +#' @param AnimationInfo animation list +#' ID number starting from 1 +#' @return list representing a layer, with corresponding aesthetics, ranges, and groups. +#' @export +saveLayer <- function(l, d, meta, layer_name, ggplot, built, AnimationInfo){ + xminv <- y <- xmaxv <- chunks.for <- NULL + ## above to avoid NOTE on CRAN check. + # Set geom name and layer name + g <- list(geom=strsplit(layer_name, "_")[[1]][2]) + g$classed <- layer_name + + ranges <- built$panel$ranges + + ## needed for when group, etc. is an expression: + g$aes <- sapply(l$mapping, function(k) as.character(as.expression(k))) + + ## 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. + ## 'colour', 'size' etc. have been moved to aes_params + g$params <- getLayerParams(l) + + ## Make a list of variables to use for subsetting. subset_order is the + ## order in which these variables will be accessed in the recursive + ## JavaScript array structure. + + ## subset_order IS in fact useful with geom_segment! For example, in + ## the first plot in the breakpointError example, the geom_segment has + ## the following exported data in plot.json + + ## "subset_order": [ + ## "showSelected", + ## "showSelected2" + ## ], + + ## This information is used to parse the recursive array data structure + ## that allows efficient lookup of subsets of data in JavaScript. Look at + ## the Firebug DOM browser on + ## http://sugiyama-www.cs.titech.ac.jp/~toby/animint/breakpoints/index.html + ## and navigate to plot.Geoms.geom3.data. You will see that this is a + ## recursive array that can be accessed via + ## data[segments][bases.per.probe] which is an un-named array + ## e.g. [{row1},{row2},...] which will be bound to the elements by + ## D3. The key point is that the subset_order array stores the order of the + ## indices that will be used to select the current subset of data (in + ## this case showSelected=segments, showSelected2=bases.per.probe). The + ## currently selected values of these variables are stored in + ## plot.Selectors. + + ## Separate .variable/.value selectors + s.aes <- selectSSandCS(g$aes) + meta$selector.aes[[g$classed]] <- s.aes + + ## Do not copy group unless it is specified in aes, and do not copy + ## showSelected variables which are specified multiple times. + do.not.copy <- colsNotToCopy(g, s.aes) + copy.cols <- ! names(d) %in% do.not.copy + + g.data <- d[copy.cols] + + is.ss <- names(g$aes) %in% s.aes$showSelected$one + show.vars <- g$aes[is.ss] + pre.subset.order <- as.list(names(show.vars)) + + is.cs <- names(g$aes) %in% s.aes$clickSelects$one + update.vars <- g$aes[is.ss | is.cs] + + update.var.names <- if(0 < length(update.vars)){ + data.frame(variable=names(update.vars), value=NA) + } + + interactive.aes <- with(s.aes, { + rbind(clickSelects$several, showSelected$several, + update.var.names) + }) + + ## Construct the selector. + for(row.i in seq_along(interactive.aes$variable)){ + aes.row <- interactive.aes[row.i, ] + is.variable.value <- !is.na(aes.row$value) + selector.df <- if(is.variable.value){ + selector.vec <- g.data[[paste(aes.row$variable)]] + data.frame(value.col=aes.row$value, + selector.name=unique(paste(selector.vec))) + }else{ + value.col <- paste(aes.row$variable) + data.frame(value.col, + selector.name=update.vars[[value.col]]) + } + for(sel.i in 1:nrow(selector.df)){ + sel.row <- selector.df[sel.i,] + value.col <- paste(sel.row$value.col) + selector.name <- paste(sel.row$selector.name) + ## If this selector was defined by .variable .value aes, then we + ## will not generate selectize widgets. + meta$selectors[[selector.name]]$is.variable.value <- is.variable.value + ## If this selector has no defined type yet, we define it once + ## and for all here, so we can use it later for chunk + ## separation. + if(is.null(meta$selectors[[selector.name]]$type)){ + selector.type <- meta$selector.types[[selector.name]] + if(is.null(selector.type))selector.type <- "single" + stopifnot(is.character(selector.type)) + stopifnot(length(selector.type)==1) + stopifnot(selector.type %in% c("single", "multiple")) + meta$selectors[[selector.name]]$type <- selector.type + } + ## If this selector does not have any clickSelects then we show + ## the selectize widgets by default. + for(look.for in c("showSelected", "clickSelects")){ + if(grepl(look.for, aes.row$variable)){ + meta$selectors[[selector.name]][[look.for]] <- TRUE + } + } + ## We also store all the values of this selector in this layer, + ## so we can accurately set levels after all geoms have been + ## compiled. + value.vec <- unique(g.data[[value.col]]) + key <- paste(g$classed, row.i, sel.i) + meta$selector.values[[selector.name]][[key]] <- + list(values=paste(value.vec), update=g$classed) + } + } + + is.show <- grepl("showSelected", names(g$aes)) + has.show <- any(is.show) + ## Error if non-identity stat is used with showSelected, since + ## typically the stats will delete the showSelected column from the + ## built data set. For example geom_bar + stat_bin doesn't make + ## sense with clickSelects/showSelected, since two + ## clickSelects/showSelected values may show up in the same bin. + stat.type <- class(l$stat)[[1]] + checkForNonIdentityAndSS(stat.type, has.show, is.show, l, + g$classed, names(g.data), names(g$aes)) + + ## Warn if non-identity position is used with animint aes. + position.type <- class(l$position)[[1]] + if(has.show && position.type != "PositionIdentity"){ + print(l) + warning("showSelected only works with position=identity, problem: ", + g$classed) + } + + ##print("before pre-processing") + + ## Pre-process some complex 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. + if(g$geom=="abline"){ + ## loop through each set of slopes/intercepts + + ## TODO: vectorize this code! + for(i in 1:nrow(g.data)) { + + # "Trick" ggplot coord_transform into transforming the slope and intercept + g.data[i, "x"] <- ranges[[ g.data$PANEL[i] ]]$x.range[1] + g.data[i, "xend"] <- ranges[[ g.data$PANEL[i] ]]$x.range[2] + g.data[i, "y"] <- g.data$slope[i] * g.data$x[i] + g.data$intercept[i] + g.data[i, "yend"] <- g.data$slope[i] * g.data$xend[i] + g.data$intercept[i] + + # make sure that lines don't run off the graph + if(g.data$y[i] < ranges[[ g.data$PANEL[i] ]]$y.range[1] ) { + g.data$y[i] <- ranges[[ g.data$PANEL[i] ]]$y.range[1] + g.data$x[i] <- (g.data$y[i] - g.data$intercept[i]) / g.data$slope[i] + } + if(g.data$yend[i] > ranges[[ g.data$PANEL[i] ]]$y.range[2]) { + g.data$yend[i] <- ranges[[ g.data$PANEL[i] ]]$y.range[2] + g.data$xend[i] <- (g.data$yend[i] - g.data$intercept[i]) / g.data$slope[i] + } + } + ## ggplot2 defaults to adding a group aes for ablines! + ## Remove it since it is meaningless. + g$aes <- g$aes[names(g$aes)!="group"] + g.data <- g.data[! names(g.data) %in% c("slope", "intercept")] + g$geom <- "segment" + } else if(g$geom=="point"){ + # Fill set to match ggplot2 default of filled in circle. + # Check for fill in both data and params + fill.in.data <- ("fill" %in% names(g.data) && any(!is.na(g.data[["fill"]]))) + fill.in.params <- "fill" %in% names(g$params) + fill.specified <- fill.in.data || fill.in.params + if(!fill.specified & "colour" %in% names(g.data)){ + g.data[["fill"]] <- g.data[["colour"]] + } + } else if(g$geom=="text"){ + ## convert hjust to anchor. + hjustRemove <- function(df.or.list){ + df.or.list$anchor <- hjust2anchor(df.or.list$hjust) + df.or.list[names(df.or.list) != "hjust"] + } + vjustWarning <- function(vjust.vec){ + not.supported <- vjust.vec != 0 + if(any(not.supported)){ + bad.vjust <- unique(vjust.vec[not.supported]) + print(bad.vjust) + warning("animint only supports vjust=0") + } + } + if ("hjust" %in% names(g$params)) { + g$params <- hjustRemove(g$params) + } else if ("hjust" %in% names(g.data)) { + g.data <- hjustRemove(g.data) + } + if("vjust" %in% names(g$params)) { + vjustWarning(g$params$vjust) + } else if ("vjust" %in% names(g$aes)) { + vjustWarning(g.data$vjust) + } + } else if(g$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"]] + } + } else if(g$geom=="density" | g$geom=="area"){ + g$geom <- "ribbon" + } else if(g$geom=="tile" | g$geom=="raster" | g$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 + } + g$geom <- "rect" + } else if(g$geom=="bar"){ + is.xy <- names(g.data) %in% c("x", "y") + g.data <- g.data[!is.xy] + g$geom <- "rect" + } else if(g$geom=="bin2d"){ + stop("bin2d is not supported in animint. Try using geom_tile() and binning the data yourself.") + } else if(g$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... change so that they are specified + # as a single string which can then be parsed in JavaScript. + # there has got to be a better way to do this!! + } else if(g$geom=="violin"){ + g.data$xminv <- with(g.data, x - violinwidth * (x - xmin)) + g.data$xmaxv <- with(g.data, x + violinwidth * (xmax - x)) + newdata <- plyr::ddply(g.data, "group", function(df){ + rbind(plyr::arrange(transform(df, x=xminv), y), + plyr::arrange(transform(df, x=xmaxv), -y)) + }) + newdata <- plyr::ddply(newdata, "group", function(df) rbind(df, df[1,])) + g.data <- newdata + g$geom <- "polygon" + } else if(g$geom=="step"){ + datanames <- names(g.data) + g.data <- plyr::ddply(g.data, "group", function(df) stairstep(df)) + g$geom <- "path" + } else if(g$geom=="contour" | g$geom=="density2d"){ + g$aes[["group"]] <- "piece" + g$geom <- "path" + } else if(g$geom=="freqpoly"){ + g$geom <- "line" + } else if(g$geom=="quantile"){ + g$geom <- "path" + } else if(g$geom=="hex"){ + g$geom <- "polygon" + ## 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 <- resolution(g.data$x, FALSE) + dy <- resolution(g.data$y, FALSE) / sqrt(3) / 2 * 1.15 + hex <- as.data.frame(hexbin::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". + # CPS (07-24-14) what about this? -- + # http://tdhock.github.io/animint/geoms/polygon/index.html + newdata <- plyr::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 + } + } + + ## Some geoms need their data sorted before saving to tsv. + if(g$geom %in% c("ribbon", "line")){ + g.data <- g.data[order(g.data$x), ] + } + + ## 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(color.var %in% names(g$params)){ + g$params[[color.var]] <- toRGB(g$params[[color.var]]) + } + } + + has.no.fill <- g$geom %in% c("path", "line") + zero.size <- any(g.data$size == 0, na.rm=TRUE) + if(zero.size && has.no.fill){ + warning(sprintf("geom_%s with size=0 will be invisible",g$geom)) + } + ## 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. + + ## Flip axes in case of coord_flip + if(inherits(ggplot$coordinates, "CoordFlip")){ + names(g.data) <- switch_axes(names(g.data)) + } + + ## Output types + ## Check to see if character type is d3's rgb type. + g$types <- sapply(g.data, function(x) { + type <- paste(class(x), collapse="-") + if(type == "character"){ + if(sum(!is.rgb(x))==0){ + "rgb" + }else if(sum(!is.linetype(x))==0){ + "linetype" + }else { + "character" + } + }else{ + type + } + }) + g$types[["group"]] <- "character" + + ## convert ordered factors to unordered factors so javascript + ## doesn't flip out. + ordfactidx <- which(g$types=="ordered-factor") + for(i in ordfactidx){ + g.data[[i]] <- factor(as.character(g.data[[i]])) + g$types[[i]] <- "factor" + } + + ## Get unique values of time variable. + time.col <- NULL + if(is.list(AnimationInfo$time)){ # if this is an animation, + g.time.list <- list() + for(c.or.s in names(s.aes)){ + cs.info <- s.aes[[c.or.s]] + for(a in cs.info$one){ + if(g$aes[[a]] == AnimationInfo$time$var){ + g.time.list[[a]] <- g.data[[a]] + time.col <- a + } + } + for(row.i in seq_along(cs.info$several$value)){ + cs.row <- cs.info$several[row.i,] + c.name <- paste(cs.row$variable) + is.time <- g.data[[c.name]] == AnimationInfo$time$var + g.time.list[[c.name]] <- g.data[is.time, paste(cs.row$value)] + } + } + u.vals <- unique(unlist(g.time.list)) + if(length(u.vals)){ + AnimationInfo$timeValues[[paste(g$classed)]] <- sort(u.vals) + } + } + ## Make the time variable the first subset_order variable. + if(length(time.col)){ + pre.subset.order <- pre.subset.order[order(pre.subset.order != time.col)] + } + + ## Determine which showSelected values to use for breaking the data + ## into chunks. This is a list of variables which have the same + ## names as the selectors. E.g. if chunk_order=list("year") then + ## when year is clicked, we may need to download some new data for + ## this geom. + subset.vec <- unlist(pre.subset.order) + if("chunk_vars" %in% names(g$params)){ #designer-specified chunk vars. + designer.chunks <- g$params$chunk_vars + if(!is.character(designer.chunks)){ + stop("chunk_vars must be a character vector; ", + "use chunk_vars=character() to specify 1 chunk") + } + not.subset <- !designer.chunks %in% g$aes[subset.vec] + if(any(not.subset)){ + stop("invalid chunk_vars ", + paste(designer.chunks[not.subset], collapse=" "), + "; possible showSelected variables: ", + paste(g$aes[subset.vec], collapse=" ")) + } + is.chunk <- g$aes[subset.vec] %in% designer.chunks + chunk.cols <- subset.vec[is.chunk] + nest.cols <- subset.vec[!is.chunk] + }else{ #infer a default, either 0 or 1 chunk vars: + if(length(meta$selectors)==0){ + ## no selectors, just make 1 chunk. + nest.cols <- subset.vec + chunk.cols <- NULL + }else{ + selector.types <- sapply(meta$selectors, "[[", "type") + selector.names <- g$aes[subset.vec] + subset.types <- selector.types[selector.names] + can.chunk <- subset.types != "multiple" + names(can.chunk) <- subset.vec + ## Guess how big the chunk files will be, and reduce the number of + ## chunks if there are any that are too small. + tmp <- tempfile() + some.lines <- rbind(head(g.data), tail(g.data)) + write.table(some.lines, tmp, + col.names=FALSE, + quote=FALSE, row.names=FALSE, sep="\t") + bytes <- file.info(tmp)$size + bytes.per.line <- bytes/nrow(some.lines) + bad.chunk <- function(){ + if(all(!can.chunk))return(NULL) + can.chunk.cols <- subset.vec[can.chunk] + maybe.factors <- g.data[, can.chunk.cols, drop=FALSE] + for(N in names(maybe.factors)){ + maybe.factors[[N]] <- paste(maybe.factors[[N]]) + } + rows.per.chunk <- table(maybe.factors) + bytes.per.chunk <- rows.per.chunk * bytes.per.line + if(all(4096 < bytes.per.chunk))return(NULL) + ## If all of the tsv chunk files are greater than 4KB, then we + ## return NULL here to indicate that the current chunk + ## variables (indicated in can.chunk) are fine. + + ## In other words, the compiler will not break a geom into + ## chunks if any of the resulting chunk tsv files is estimated + ## to be less than 4KB (of course, if the layer has very few + ## data overall, the compiler creates 1 file which may be less + ## than 4KB, but that is fine). + dim.byte.list <- list() + if(length(can.chunk.cols) == 1){ + dim.byte.list[[can.chunk.cols]] <- sum(bytes.per.chunk) + }else{ + for(dim.i in seq_along(can.chunk.cols)){ + dim.name <- can.chunk.cols[[dim.i]] + dim.byte.list[[dim.name]] <- + apply(bytes.per.chunk, -dim.i, sum) + } + } + selector.df <- + data.frame(chunks.for=length(rows.per.chunk), + chunks.without=sapply(dim.byte.list, length), + min.bytes=sapply(dim.byte.list, min)) + ## chunks.for is the number of chunks you get if you split the + ## data set using just this column. If it is 1, then it is + ## fine to chunk on this variable (since we certainly won't + ## make more than 1 small tsv file) and in fact we want to + ## chunk on this variable, since then this layer's data won't + ## be downloaded at first if it is not needed. + not.one <- subset(selector.df, 1 < chunks.for) + if(nrow(not.one) == 0){ + NULL + }else{ + rownames(not.one)[[which.max(not.one$min.bytes)]] + } + } + while({ + bad <- bad.chunk() + !is.null(bad) + }){ + can.chunk[[bad]] <- FALSE + } + if(any(can.chunk)){ + nest.cols <- subset.vec[!can.chunk] + chunk.cols <- subset.vec[can.chunk] + }else{ + nest.cols <- subset.vec + chunk.cols <- NULL + } + } # meta$selectors > 0 + } + + # If there is only one PANEL, we don't need it anymore. + # g$PANEL <- unique(g.data[["PANEL"]]) + plot.has.panels <- nrow(built$panel$layout) > 1 + g.data <- removeUniquePanelValue(g.data, plot.has.panels) + + ## Also add pointers to these chunks from the related selectors. + if(length(chunk.cols)){ + selector.names <- as.character(g$aes[chunk.cols]) + chunk.name <- paste(selector.names, collapse="_") + g$chunk_order <- as.list(selector.names) + for(selector.name in selector.names){ + meta$selectors[[selector.name]]$chunks <- + unique(c(meta$selectors[[selector.name]]$chunks, chunk.name)) + } + }else{ + g$chunk_order <- list() + } + g$nest_order <- as.list(nest.cols) + names(g$chunk_order) <- NULL + names(g$nest_order) <- NULL + g$subset_order <- g$nest_order + + ## If this plot has more than one PANEL then add it to subset_order + ## and nest_order. + if(plot.has.panels){ + g$subset_order <- c(g$subset_order, "PANEL") + g$nest_order <- c(g$nest_order, "PANEL") + } + + ## nest_order should contain both .variable .value aesthetics, but + ## subset_order should contain only .variable. + if((nrow(s.aes$showSelected$several) > 0)){ + g$nest_order <- with(s.aes$showSelected$several, { + c(g$nest_order, paste(variable), paste(value)) + }) + g$subset_order <- + c(g$subset_order, paste(s.aes$showSelected$several$variable)) + } + + ## group should be the last thing in nest_order, if it is present. + data.object.geoms <- c("line", "path", "ribbon", "polygon") + if("group" %in% names(g$aes) && g$geom %in% data.object.geoms){ + g$nest_order <- c(g$nest_order, "group") + } + + ## Some geoms should be split into separate groups if there are NAs. + if(any(is.na(g.data)) && "group" %in% names(g$aes)){ + sp.cols <- unlist(c(chunk.cols, g$nest_order)) + order.args <- list() + for(sp.col in sp.cols){ + order.args[[sp.col]] <- g.data[[sp.col]] + } + ord <- do.call(order, order.args) + g.data <- g.data[ord,] + is.missing <- apply(is.na(g.data), 1, any) + diff.vec <- diff(is.missing) + new.group.vec <- c(FALSE, diff.vec == 1) + for(chunk.col in sp.cols){ + one.col <- g.data[[chunk.col]] + is.diff <- c(FALSE, one.col[-1] != one.col[-length(one.col)]) + new.group.vec[is.diff] <- TRUE + } + subgroup.vec <- cumsum(new.group.vec) + g.data$group <- subgroup.vec + } + + ## Find infinite values and replace with range min/max. + for(xy in c("x", "y")){ + range.name <- paste0(xy, ".range") + range.mat <- sapply(ranges, "[[", range.name) + xy.col.vec <- grep(paste0("^", xy), names(g.data), value=TRUE) + xy.col.df <- g.data[, xy.col.vec, drop=FALSE] + cmp.list <- list(`<`, `>`)#order is important here! + for(row.i in seq_along(cmp.list)){ + ## PANEL may be a factor so it is not good enough to do + ## if(is.numeric(g.data$PANEL)) + panel.vec <- if("PANEL" %in% names(g.data)){ + g.data$PANEL + }else{ + rep(1, nrow(g.data)) + } + extreme.vec <- range.mat[row.i, panel.vec] + cmp <- cmp.list[[row.i]] + to.rep <- cmp(xy.col.df, extreme.vec) & !is.na(xy.col.df) + row.vec <- row(to.rep)[to.rep] + xy.col.df[to.rep] <- extreme.vec[row.vec] + } + g.data[, xy.col.vec] <- xy.col.df + } + + ## Determine if there are any "common" data that can be saved + ## separately to reduce disk usage. + data.or.null <- getCommonChunk(g.data, chunk.cols, g$aes) + g.data.varied <- if(is.null(data.or.null)){ + split.x(na.omit(g.data), chunk.cols) + }else{ + g$columns$common <- as.list(names(data.or.null$common)) + tsv.name <- sprintf("%s_chunk_common.tsv", g$classed) + tsv.path <- file.path(meta$out.dir, tsv.name) + write.table(data.or.null$common, tsv.path, + quote = FALSE, row.names = FALSE, + sep = "\t") + data.or.null$varied + } + + list(g=g, g.data.varied=g.data.varied, timeValues=AnimationInfo$timeValues) +} + #' Compile and render an animint in a local directory. #' #' This function converts an animint plot.list into a directory of diff --git a/tests/testthat/test-renderer1-legends.R b/tests/testthat/test-renderer1-legends.R index a446fe27a..fc4bfa2f0 100644 --- a/tests/testthat/test-renderer1-legends.R +++ b/tests/testthat/test-renderer1-legends.R @@ -1,5 +1,4 @@ acontext("legends") - data(WorldBank, package="animint2") breaks <- 10^(4:9) viz <- @@ -69,17 +68,17 @@ test_that('hiding all legends works with theme(legend.position="none")',{ error.types <- data.frame(x=1:3, status=c("correct", "false positive", "false negative")) -gg <- +gg <- ggplot(error.types)+ geom_point(aes(x, x))+ geom_tallrect(aes(xmin=x, xmax=x+0.5, fill=x), color="black") -expected.legend.list <- +expected.legend.list <- list(increasing=1:3, default=seq(3, 1, by=-0.5), decreasing=3:1) - + test_that("renderer shows legend entries in correct order", { viz <- list(increasing=gg+ @@ -89,7 +88,7 @@ test_that("renderer shows legend entries in correct order", { default=gg) info <- animint2HTML(viz) ##sapply(info$plots, function(p)sapply(p$legend$x$entries, "[[", "label")) - + ## NOTE: it is important to test the renderer here (not the ## compiler) since maybe the order specified in the plot.json file ## is not the same as the order of appearance on the web page. @@ -112,3 +111,19 @@ test_that("renderer shows legend entries in correct order", { expect_equal(value.num, expected.entries) } }) + +gg <- ggplot()+ + geom_line(aes( + year, life.expectancy, group=country, colour=region), + clickSelects="country", + data=WorldBank, size=3, alpha=3/5) +viz <- list( + noLegends=gg+ + guides(color="none"), + zoom=gg+coord_cartesian(xlim=c(1970, 2000), ylim=c(30, 70))) +test_that("ok to have two plots based on a common plot", { + info <- animint2HTML(viz) + p1 <- getNodeSet(info$html, '//g[@class="geom1_line_noLegends"]//path') + p2 <- getNodeSet(info$html, '//g[@class="geom2_line_zoom"]//path') + expect_equal(length(p1), length(p2)) +})