diff --git a/DESCRIPTION b/DESCRIPTION index 18e50e1b92..02d7831a9a 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: plotly Type: Package Title: Interactive, publication-quality graphs online. -Version: 0.5.26 +Version: 0.5.27 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 d1d6bdca77..cefff8b5e7 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,7 @@ +0.5.27 -- 19 Mar 2015 + +Reimplement geom_ribbon as a basic polygon. Fix #191. Fix #192. + 0.5.26 -- 18 Mar 2015 Implemented geom_rect #178 diff --git a/R/ggplotly.R b/R/ggplotly.R index 08bf4af08f..798171bbde 100644 --- a/R/ggplotly.R +++ b/R/ggplotly.R @@ -598,13 +598,13 @@ gg2list <- function(p){ layout$showlegend <- FALSE } } - + # Only show a legend title if there is at least 1 trace with # showlegend=TRUE. trace.showlegend <- sapply(trace.list, "[[", "showlegend") if (any(trace.showlegend) && layout$showlegend && length(p$data)) { # Retrieve legend title - legend.elements <- sapply(traces, "[[", "name") + legend.elements <- unlist(sapply(traces, "[[", "name")) legend.title <- "" for (i in 1:ncol(p$data)) { if (all(legend.elements %in% unique(p$data[, i]))) @@ -760,6 +760,46 @@ gg2list <- function(p){ merged.traces[[length(merged.traces)+1]] <- tr } + # ------------------------------- + # avoid redundant legends entries + # ------------------------------- + # remove alpha from a color entry + rm_alpha <- function(x) { + if (length(x) == 0) return(x) + pat <- "^rgba\\(" + if (!grepl(pat, x)) return(x) + sub(",\\s*[0]?[.]?[0-9]+\\)$", ")", sub(pat, "rgb(", x)) + } + # convenient for extracting name/value of legend entries (ignoring alpha) + entries <- function(x, y) { + z <- try(x[[y]], silent = TRUE) + if (inherits(e, "try-error")) { + paste0(x$name, "-") + } else { + paste0(x$name, "-", rm_alpha(z)) + } + } + fill_set <- unlist(lapply(merged.traces, entries, "fillcolor")) + line_set <- unlist(lapply(merged.traces, entries, c("line", "color"))) + mark_set <- unlist(lapply(merged.traces, entries, c("marker", "color"))) + legend_intersect <- function(x, y) { + i <- intersect(x, y) + # restrict intersection to valid legend entries + i[grepl("-rgb[a]?\\(", i)] + } + # if there is a mark & line legend, get rid of line + t1 <- line_set %in% legend_intersect(mark_set, line_set) + # that is, unless the mode is 'lines+markers'... + t1 <- t1 & !(unlist(lapply(merged.traces, "[[", "mode")) %in% "lines+markers") + # if there is a mark & fill legend, get rid of fill + t2 <- fill_set %in% legend_intersect(mark_set, fill_set) + # if there is a line & fill legend, get rid of fill + t3 <- fill_set %in% legend_intersect(line_set, fill_set) + t <- t1 | t2 | t3 + for (m in seq_along(merged.traces)) + if (isTRUE(merged.traces[[m]]$showlegend && t[m])) + merged.traces[[m]]$showlegend <- FALSE + # Put the traces in correct order, according to any manually # specified scales. This seems to be repetitive with the trace$rank # attribute in layer2traces (which is useful for sorting traces that diff --git a/R/trace_generation.R b/R/trace_generation.R index 2181e0e3c4..06b5728dfc 100644 --- a/R/trace_generation.R +++ b/R/trace_generation.R @@ -26,13 +26,15 @@ layer2traces <- function(l, d, misc) { # geom_smooth() means geom_line() + geom_ribbon() # Note the line is always drawn, but ribbon is not if se = FALSE. if (g$geom == "smooth") { - # If smoothLine has been compiled already, consider smoothRibbon. + # If smoothLine has been compiled already, consider drawing the ribbon if (isTRUE(misc$smoothLine)) { misc$smoothLine <- FALSE if (isTRUE(l$stat_params$se == FALSE)) { return(NULL) } else { g$geom <- "smoothRibbon" + # disregard colour + g$data <- g$data[!grepl("^colour[.name]?", names(g$data))] } } else { misc$smoothLine <- TRUE @@ -248,7 +250,6 @@ layer2traces <- function(l, d, misc) { if (length(unique(name.list)) < 2) tr$name <- as.character(name.list[[1]]) } - dpd <- data.params$data if ("PANEL" %in% names(dpd) && nrow(dpd) > 0) { @@ -335,6 +336,11 @@ toBasic <- list( g$geom <- "polygon" g }, + ribbon=function(g) { + g$data <- ribbon_dat(g$data) + g$geom <- "polygon" + g + }, path=function(g) { group2NA(g, "path") }, @@ -406,12 +412,15 @@ toBasic <- list( g }, smoothLine=function(g) { - if (length(unique(g$data$group)) == 1) g$params$colour <- "#3366FF" + if (length(grep("^colour$", names(g$data))) == 0) + g$params$colour <- "#3366FF" group2NA(g, "path") }, smoothRibbon=function(g) { - if (is.null(g$params$alpha)) g$params$alpha <- 0.1 - group2NA(g, "ribbon") + if (is.null(g$params$alpha)) g$params$alpha <- 0.2 + g$data <- ribbon_dat(g$data) + g$geom <- "polygon" + g } ) @@ -493,6 +502,26 @@ make.errorbar <- function(data, params, xy){ tr } +# function to transform geom_ribbon data into format plotly likes +# (note this function is also used for geom_smooth) +ribbon_dat <- function(dat) { + n <- nrow(dat) + o <- order(dat$x) + o2 <- order(dat$x, decreasing = TRUE) + used <- c("x", "ymin", "ymax") + not_used <- setdiff(names(dat), used) + # top-half of ribbon + tmp <- dat[o, ] + others <- tmp[not_used] + dat1 <- cbind(x = tmp$x, y = tmp$ymax, others) + dat1[n+1, ] <- cbind(x = tmp$x[n], y = tmp$ymin[n], others[n, ]) + # bottom-half of ribbon + tmp2 <- dat[o2, ] + others2 <- tmp2[not_used] + dat2 <- cbind(x = tmp2$x, y = tmp2$ymin, others2) + rbind(dat1, dat2) +} + # Convert basic geoms to traces. geom2trace <- list( path=function(data, params) { @@ -515,7 +544,8 @@ geom2trace <- list( mode="lines", line=paramORdefault(params, aes2line, polygon.line.defaults), fill="tozerox", - fillcolor=toFill(params$fill)) + fillcolor=toFill(params$fill, ifelse(is.null(params$alpha), 1, + params$alpha))) }, point=function(data, params){ L <- list(x=data$x, @@ -667,15 +697,6 @@ geom2trace <- list( fillcolor=toFill(params$fill, ifelse(is.null(params$alpha), 1, params$alpha))) }, - ribbon=function(data, params) { - list(x=c(data$x[1], data$x, rev(data$x)), - y=c(data$ymin[1], data$ymax, rev(data$ymin)), - type="scatter", - line=paramORdefault(params, aes2line, ribbon.line.defaults), - fill="tonexty", - fillcolor=toFill(params$fill, ifelse(is.null(params$alpha), 1, - params$alpha))) - }, abline=function(data, params) { list(x=c(params$xstart, params$xend), y=c(params$intercept + params$xstart * params$slope, diff --git a/tests/testthat/test-ggplot-ribbon.R b/tests/testthat/test-ggplot-ribbon.R index aca320f0bd..7ec354a8aa 100644 --- a/tests/testthat/test-ggplot-ribbon.R +++ b/tests/testthat/test-ggplot-ribbon.R @@ -1,26 +1,53 @@ context("ribbon") -huron <- data.frame(year=1875:1972, level=as.vector(LakeHuron)) +expect_traces <- function(gg, n.traces, name){ + stopifnot(is.ggplot(gg)) + stopifnot(is.numeric(n.traces)) + save_outputs(gg, paste0("ribbon-", 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) +} -rb <- ggplot(huron, aes(x=year)) + geom_ribbon(aes(ymin=level-1, ymax=level+1)) -L <- gg2list(rb) +huron <- data.frame(year = 1875:1972, level = as.vector(LakeHuron)) +huron$decade <- with(huron, round(year/10) * 10) +huron$diff <- huron$year - huron$decade -test_that("sanity check for geom_ribbon", { - expect_equal(length(L), 2) - expect_identical(L[[1]]$type, "scatter") - expect_equal(L[[1]]$x, c(huron$year[1], huron$year, rev(huron$year))) - expect_equal(L[[1]]$y, c(huron$level[1]-1, huron$level+1, rev(huron$level-1))) - expect_identical(L[[1]]$line$color, "transparent") +p1 <- ggplot(data = huron) + + geom_ribbon(aes(x = year, ymin = level-1, ymax = level+1), + alpha = 0.1) + +test_that("geom_ribbon() creates 1 trace & respects alpha transparency", { + info <- expect_traces(p1, 1, "alpha") + tr <- info$traces[[1]] + expect_match(tr$fillcolor, "0.1)", fixed=TRUE) }) -save_outputs(rb, "ribbon") +p2 <- ggplot(data = huron, aes(group = factor(decade))) + + geom_ribbon(aes(x = diff, ymin = level-0.1, ymax = level+0.1)) + +test_that("geom_ribbon() with group aesthetic produces 1 trace", { + info <- expect_traces(p2, 1, "group") +}) -rb2 <- ggplot(huron, aes(x=year)) + - geom_ribbon(aes(ymin=level-1, ymax=level+1), alpha = 0.1) -L2 <- gg2list(rb2) +p3 <- ggplot(data = huron, aes(colour = factor(decade))) + + geom_ribbon(aes(x = diff, ymin = level-0.1, ymax = level+0.1)) -test_that("geom_ribbon respects alpha transparency", { - expect_match(L2[[1]]$fillcolor, "0.1)", fixed=TRUE) +test_that("geom_ribbon() with colour aesthetic produces multiple traces", { + # 10 traces -- one for each decade + info <- expect_traces(p3, 10, "colour") }) -save_outputs(rb2, "ribbon-alpha") +p4 <- ggplot(data = huron, aes(fill = factor(decade))) + + geom_ribbon(aes(x = diff, ymin = level-0.1, ymax = level+0.1)) + +test_that("geom_ribbon() with fill aesthetic produces multiple traces", { + # 10 traces -- one for each decade + info <- expect_traces(p4, 10, "fill") +}) diff --git a/tests/testthat/test-ggplot-smooth.R b/tests/testthat/test-ggplot-smooth.R index 7e5cbdb168..cc1b9064bf 100644 --- a/tests/testthat/test-ggplot-smooth.R +++ b/tests/testthat/test-ggplot-smooth.R @@ -1,18 +1,69 @@ context("smooth") +expect_traces <- function(gg, n.traces, name){ + stopifnot(is.ggplot(gg)) + stopifnot(is.numeric(n.traces)) + save_outputs(gg, paste0("smooth-", 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) +} + p <- ggplot(mtcars, aes(mpg, wt)) + geom_point() + geom_smooth() test_that("geom_point() + geom_smooth() produces 3 traces", { - info <- gg2list(p) - expect_true(sum(names(info) == "") == 3) - save_outputs(p, "smooth") + expect_traces(p, 3, "basic") }) -p2 <- ggplot(mtcars, aes(mpg, wt)) + geom_point() + geom_smooth(se = FALSE) +p2 <- ggplot(mtcars, aes(mpg, wt)) + geom_point() + + geom_smooth(se = FALSE) test_that("geom_point() + geom_smooth(se = FALSE) produces 2 traces", { - info2 <- gg2list(p2) - expect_true(sum(names(info2) == "") == 2) - save_outputs(p2, "smooth-se-false") + expect_traces(p2, 2, "se-false") +}) + +d <- diamonds[sample(nrow(diamonds), 1000), ] +p3 <- qplot(carat, price, group = cut, data = d) + geom_smooth() + +test_that("geom_smooth() respects group aesthetic", { + info <- expect_traces(p3, 3, "group") +}) + +p4 <- qplot(carat, price, colour = cut, data = d) + geom_smooth() +p5 <- qplot(carat, price, data = d) + geom_smooth(aes(colour = cut)) + +test_that("geom_smooth() respects colour aesthetic", { + info <- expect_traces(p4, 11, "colour") + # number of showlegends should equal the number of factor levels + n <- sum(unlist(sapply(info$traces, "[[", "showlegend"))) + expect_equal(n, nlevels(d$cut)) + info <- expect_traces(p5, 7, "colour2") + n <- sum(unlist(sapply(info$traces, "[[", "showlegend"))) + expect_equal(n, nlevels(d$cut)) +}) + +p7 <- qplot(carat, price, data = d) + geom_smooth(aes(fill = cut)) + +test_that("geom_smooth() respects fill aesthetic", { + info <- expect_traces(p7, 7, "fill2") + n <- sum(unlist(sapply(info$traces, "[[", "showlegend"))) + expect_equal(n, nlevels(d$cut)) +}) + +# ensure legend is drawn when needed +p8 <- qplot(carat, price, data = d) + facet_wrap(~cut) + + geom_smooth(aes(colour = cut, fill = cut)) + +test_that("geom_smooth() works with facets", { + # 3 traces for each panel + info <- expect_traces(p8, 15, "facet") + n <- sum(unlist(sapply(info$traces, "[[", "showlegend"))) + expect_equal(n, nlevels(d$cut)) })