diff --git a/R/ggplotly.R b/R/ggplotly.R index ba6acaef07..67622ba3ad 100644 --- a/R/ggplotly.R +++ b/R/ggplotly.R @@ -69,6 +69,7 @@ gg2list <- function(p){ } # Always use identity size scale so that plot.ly gets the real # units for the size variables. + original.p <- p p <- tryCatch({ # this will be an error for discrete variables. suppressMessages({ @@ -117,7 +118,8 @@ gg2list <- function(p){ ggsizemin <- min(unlist(sizerange)) ggsizemax <- max(unlist(sizerange)) } - + + layer.legends <- list() for(i in seq_along(built$plot$layers)){ # This is the layer from the original ggplot object. L <- p$layers[[i]] @@ -134,14 +136,14 @@ gg2list <- function(p){ # 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(a in c("fill", "colour", "x", "y", "size")){ for(data.type in c("continuous", "date", "datetime", "discrete")){ fun.name <- sprintf("scale_%s_%s", a, data.type) misc.name <- paste0("is.", data.type) misc[[misc.name]][[a]] <- tryCatch({ fun <- get(fun.name) suppressMessages({ - with.scale <- p+fun() + with.scale <- original.p + fun() }) ggplot_build(with.scale) TRUE @@ -152,13 +154,16 @@ gg2list <- function(p){ } # scales are needed for legend ordering. + misc$breaks <- list() for(sc in p$scales$scales){ - a <- sc$aesthetics - if(length(a) == 1){ + a.vec <- sc$aesthetics + default.breaks <- inherits(sc$breaks, "waiver") + if (length(a.vec) == 1 && (!default.breaks) ) { + # TODO: generalize for x/y scales too. br <- sc$breaks ranks <- seq_along(br) names(ranks) <- br - misc$breaks[[sc$aesthetics]] <- ranks + misc$breaks[[a.vec]] <- ranks } misc$trans[sc$aesthetics] <- sc$trans$name } @@ -202,6 +207,10 @@ gg2list <- function(p){ # This extracts essential info for this geom/layer. traces <- layer2traces(L, df, misc) + + possible.legends <- markLegends[[L$geom$objname]] + actual.legends <- possible.legends[possible.legends %in% names(L$mapping)] + layer.legends[[paste(i)]] <- actual.legends # Do we really need to coord_transform? # g$data <- ggplot2:::coord_transform(built$plot$coord, g$data, @@ -528,7 +537,27 @@ gg2list <- function(p){ if (any(names(layer.aes) %in% markUnique[markUnique != "x"]) == FALSE) layout$showlegend <- FALSE - if (layout$showlegend && length(p$data)) { + ## Legend hiding when guides(fill="none"). + legends.present <- unique(unlist(layer.legends)) + is.false <- function(x){ + is.logical(x) && length(x) == 1 && x == FALSE + } + is.none <- function(x){ + is.character(x) && length(x) == 1 && x == "none" + } + is.hidden <- function(x){ + is.false(x) || is.none(x) + } + for(a in legends.present){ + if(is.hidden(p$guides[[a]])){ + 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.title <- "" @@ -687,7 +716,13 @@ gg2list <- function(p){ } # Put the traces in correct order, according to any manually - # specified scales. + # specified scales. This seems to be repetitive with the trace$rank + # attribute in layer2traces (which is useful for sorting traces that + # get different legend entries but come from the same geom, as in + # test-ggplot-legend.R), but in fact this is better since it could + # be used for sorting traces that come from different geoms + # (currently we don't have a test for this). TODO: write such a + # test, delete the trace$rank code, and have it work here instead. trace.order <- unlist(trace.order.list) ordered.traces <- if(length(trace.order)){ trace.order.score <- seq_along(trace.order) diff --git a/R/trace_generation.R b/R/trace_generation.R index dc8a62d741..b698ef58a2 100644 --- a/R/trace_generation.R +++ b/R/trace_generation.R @@ -210,16 +210,24 @@ layer2traces <- function(l, d, misc) { } } name.names <- grep("[.]name$", names(data.params$params), value=TRUE) - if (length(name.names)) { - for(a.name in name.names){ + not.group <- grep("group", name.names, value=TRUE, invert=TRUE) + if (length(not.group)) { + for(a.name in not.group){ a <- sub("[.]name$", "", a.name) - a.value <- as.character(data.params$params[[a.name]]) - ranks <- misc$breaks[[a]] - if(length(ranks)){ - tr$sort[[a.name]] <- ranks[[a.value]] + tr$sort[[a.name]] <- if (a %in% names(misc$breaks)){ + # Custom breaks were specified. + a.value <- as.character(data.params$params[[a.name]]) + ranks <- misc$breaks[[a]] + if (a.value %in% names(ranks)){ + ranks[[a.value]] + } else { + Inf # sorts to the end, when there are less breaks than classes. + } + } else { # custom breaks were not specified. + 1 # sort them all the same. } } - name.list <- data.params$params[name.names] + name.list <- data.params$params[not.group] tr$name <- paste(unlist(name.list), collapse=".") if (length(unique(name.list)) < 2) tr$name <- as.character(name.list[[1]]) @@ -257,14 +265,25 @@ layer2traces <- function(l, d, misc) { 0 } }) - + ord <- order(sort.val) no.sort <- traces[ord] for(tr.i in seq_along(no.sort)){ + s <- no.sort[[tr.i]]$sort + no.sort[[tr.i]]$showlegend <- + if (is.numeric(s)) { + if (s == Inf){ + FALSE + } else { + TRUE + } + } else { # no legend. + FALSE + } no.sort[[tr.i]]$sort <- NULL } no.sort - } +}#layer2traces # Preprocess data and params. @@ -309,7 +328,10 @@ toBasic <- list( bar=function(g) { if (any(is.na(g$prestats.data$x))) g$prestats.data$x <- g$prestats.data$x.name - g$prestats.data$fill <- g$data$fill[match(g$prestats.data$group, g$data$group)] + for(a in c("fill", "colour")){ + g$prestats.data[[a]] <- + g$data[[a]][match(g$prestats.data$group, g$data$group)] + } g$params$xstart <- min(g$data$xmin) g$params$xend <- max(g$data$xmax) g$data <- g$prestats.data diff --git a/tests/testthat/test-ggplot-abline.R b/tests/testthat/test-ggplot-abline.R index 9a017f4a46..ebff610cd8 100644 --- a/tests/testthat/test-ggplot-abline.R +++ b/tests/testthat/test-ggplot-abline.R @@ -11,7 +11,7 @@ test_that("Second trace be the a-b line", { geom_abline(intercept=1.1, slope=0.9, colour="red", size=4) L <- gg2list(gg) - + expect_equal(length(L), 3) expect_true(L[[2]]$x[1] <= 0) expect_true(L[[2]]$x[2] >= 3.5) @@ -19,5 +19,8 @@ test_that("Second trace be the a-b line", { expect_identical(L[[2]]$line$shape, "linear") expect_equal(L[[2]]$line$width, 8) + expect_identical(L[[1]]$showlegend, FALSE) + expect_identical(L[[2]]$showlegend, FALSE) + save_outputs(gg, "abline") }) diff --git a/tests/testthat/test-ggplot-bar.R b/tests/testthat/test-ggplot-bar.R index 0f5525f6f0..eb8bd6176e 100644 --- a/tests/testthat/test-ggplot-bar.R +++ b/tests/testthat/test-ggplot-bar.R @@ -63,3 +63,122 @@ test_that("dates work well with bar charts", { save_outputs(gd, "bar-dates") }) + +expect_traces <- function(gg, n.traces, name){ + stopifnot(is.ggplot(gg)) + stopifnot(is.numeric(n.traces)) + save_outputs(gg, paste0("bar-", 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) +} + +## http://www.cookbook-r.com/Graphs/Bar_and_line_graphs_%28ggplot2%29/ +df <- data.frame(time = factor(c("Lunch","Dinner"), levels=c("Lunch","Dinner")), + total_bill = c(14.89, 17.23)) + +test_that("Very basic bar graph", { + gg <- ggplot(data=df, aes(x=time, y=total_bill)) + + geom_bar(stat="identity") + info <- expect_traces(gg, 1, "nocolor") + for(tr in info$traces){ + expect_null(tr$marker$color) + expect_null(tr$marker$line$color) + expect_null(tr$marker$line$width) + } + expect_null(info$kwargs$layout$annotations) + expect_false(info$kwargs$layout$showlegend) +}) + +test_that("Map the time of day to different fill colors", { + gg <- ggplot(data=df, aes(x=time, y=total_bill, fill=time)) + + geom_bar(stat="identity") + info <- expect_traces(gg, 2, "color") + for(tr in info$traces){ + expect_true(is.character(tr$marker$color)) + expect_null(tr$marker$line$color) + expect_null(tr$marker$line$width) + expect_true(tr$showlegend) + } + expect_match(info$kwargs$layout$annotations[[1]]$text, "time") + expect_true(info$kwargs$layout$showlegend) +}) + +test_that("Add a black outline", { + gg <- ggplot(data=df, aes(x=time, y=total_bill, fill=time)) + + geom_bar(colour="black", stat="identity") + info <- expect_traces(gg, 2, "black-outline") + for(tr in info$traces){ + expect_true(is.character(tr$marker$color)) + expect_identical(tr$marker$line$color, toRGB("black")) + expect_equal(tr$marker$line$width, 1) + expect_true(tr$showlegend) + } + expect_match(info$kwargs$layout$annotations[[1]]$text, "time") + expect_true(info$kwargs$layout$showlegend) +}) + +test_that("guides(fill=FALSE) hides fill legend", { + gg <- ggplot(data=df, aes(x=time, y=total_bill, fill=time)) + + geom_bar(colour="black", stat="identity") + + guides(fill=FALSE) + info <- expect_traces(gg, 2, "aes-fill-guides-fill-FALSE") + for(tr in info$traces){ + expect_true(is.character(tr$marker$color)) + expect_identical(tr$marker$line$color, toRGB("black")) + expect_equal(tr$marker$line$width, 1) + } + expect_null(info$kwargs$layout$annotations) + expect_false(info$kwargs$layout$showlegend) +}) + +test_that('guides(fill="none") hides fill legend', { + gg <- ggplot(data=df, aes(x=time, y=total_bill, fill=time)) + + geom_bar(colour="black", stat="identity") + + guides(fill="none") + info <- expect_traces(gg, 2, "aes-fill-guides-fill-none") + for(tr in info$traces){ + expect_true(is.character(tr$marker$color)) + expect_identical(tr$marker$line$color, toRGB("black")) + expect_equal(tr$marker$line$width, 1) + } + expect_null(info$kwargs$layout$annotations) + expect_false(info$kwargs$layout$showlegend) +}) + +test_that('guides(colour="none") does not affect fill legend', { + gg <- ggplot(data=df, aes(x=time, y=total_bill, fill=time)) + + geom_bar(color="black", stat="identity") + + guides(colour="none") + info <- expect_traces(gg, 2, "aes-fill-guides-color-none") + for(tr in info$traces){ + expect_true(is.character(tr$marker$color)) + expect_identical(tr$marker$line$color, toRGB("black")) + expect_equal(tr$marker$line$width, 1) + expect_true(tr$showlegend) + } + expect_match(info$kwargs$layout$annotations[[1]]$text, "time") + expect_true(info$kwargs$layout$showlegend) +}) + +test_that("guides(fill=FALSE) does not affect colour legend", { + gg <- ggplot(data=df, aes(x=time, y=total_bill, colour=time)) + + geom_bar(fill="grey", stat="identity") + + guides(fill=FALSE) + info <- expect_traces(gg, 2, "aes-colour-guides-fill-FALSE") + for(tr in info$traces){ + expect_identical(tr$marker$color, toRGB("grey")) + expect_true(is.character(tr$marker$line$color)) + expect_equal(tr$marker$line$width, 1) + expect_true(tr$showlegend) + } + expect_match(info$kwargs$layout$annotations[[1]]$text, "time") + expect_true(info$kwargs$layout$showlegend) +}) + diff --git a/tests/testthat/test-ggplot-legend.R b/tests/testthat/test-ggplot-legend.R index 54847244a1..56c26b590c 100644 --- a/tests/testthat/test-ggplot-legend.R +++ b/tests/testthat/test-ggplot-legend.R @@ -1,31 +1,87 @@ context("legends") +expect_traces <- function(gg, n.traces, name){ + stopifnot(is.ggplot(gg)) + stopifnot(is.numeric(n.traces)) + save_outputs(gg, paste0("legend-", 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) +} + test_that("legend can be hidden", { ggiris <- ggplot(iris)+ geom_point(aes(Petal.Width, Sepal.Width, color=Species))+ theme(legend.position="none") - info <- gg2list(ggiris) + info <- expect_traces(ggiris, 3, "iris-position-none") expect_identical(info$kwargs$layout$showlegend, FALSE) - - save_outputs(ggiris, "legend-hidden") }) +getnames <- function(traces){ + name.list <- lapply(traces, "[[", "name") + ## Not sapply, since that will result in a character vector with + ## "NULL" if one of the traces does not have an element "name" + do.call(c, name.list) +} + test_that("legend entries appear in the correct order", { ggiris <- ggplot(iris)+ geom_point(aes(Petal.Width, Sepal.Width, color=Species)) - getnames <- function(L){ - traces <- L[names(L)==""] - expect_equal(length(traces), 3) - as.character(sapply(traces, "[[", "name")) - } - info <- gg2list(ggiris) + info <- expect_traces(ggiris, 3, "iris-default") + computed.showlegend <- sapply(info$traces, "[[", "showlegend") + expected.showlegend <- rep(TRUE, 3) + expect_identical(as.logical(computed.showlegend), expected.showlegend) ## Default is the same as factor levels. - expect_identical(getnames(info), levels(iris$Species)) + expect_identical(getnames(info$traces), levels(iris$Species)) ## Custom breaks should be respected. breaks <- c("versicolor", "setosa", "virginica") ggbreaks <- ggiris+scale_color_discrete(breaks=breaks) - info.breaks <- gg2list(ggbreaks) - expect_identical(getnames(info.breaks), breaks) + info.breaks <- expect_traces(ggbreaks, 3, "iris-breaks") + expect_identical(getnames(info.breaks$traces), breaks) +}) - save_outputs(ggiris, "legend-order") +test_that("2 breaks -> 1 named trace with showlegend=FALSE", { + two.breaks <- c("setosa", "versicolor") + two.legend.entries <- ggplot(iris)+ + geom_point(aes(Petal.Width, Sepal.Width, color=Species))+ + scale_color_discrete(breaks=two.breaks) + info <- expect_traces(two.legend.entries, 3, "iris-trace-showlegend-FALSE") + expected.names <- levels(iris$Species) + expected.showlegend <- expected.names %in% two.breaks + expect_identical(getnames(info$traces), expected.names) + computed.showlegend <- sapply(info$traces, "[[", "showlegend") + expect_identical(as.logical(computed.showlegend), expected.showlegend) +}) + +test_that("1 break -> 2 traces with showlegend=FALSE", { + one.break <- c("setosa") + one.legend.entry <- ggplot(iris)+ + geom_point(aes(Petal.Width, Sepal.Width, color=Species))+ + scale_color_discrete(breaks=one.break) + info <- expect_traces(one.legend.entry, 3, "iris-2traces-showlegend-FALSE") + expected.names <- levels(iris$Species) + expected.showlegend <- expected.names %in% one.break + expect_identical(getnames(info$traces), expected.names) + computed.showlegend <- sapply(info$traces, "[[", "showlegend") + expect_identical(as.logical(computed.showlegend), expected.showlegend) +}) + +test_that("0 breaks -> 3 traces with showlegend=FALSE", { + no.breaks <- c() + no.legend.entries <- ggplot(iris)+ + geom_point(aes(Petal.Width, Sepal.Width, color=Species))+ + scale_color_discrete(breaks=no.breaks) + info <- expect_traces(no.legend.entries, 3, "iris-3traces-showlegend-FALSE") + expect_equal(length(info$kwargs$layout$annotations), 0) + expected.names <- levels(iris$Species) + expected.showlegend <- expected.names %in% no.breaks + expect_identical(getnames(info$traces), expected.names) + computed.showlegend <- sapply(info$traces, "[[", "showlegend") + expect_identical(as.logical(computed.showlegend), expected.showlegend) }) diff --git a/tests/testthat/test-ggplot-polygons.R b/tests/testthat/test-ggplot-polygons.R index 5f93251550..b159e2d10a 100644 --- a/tests/testthat/test-ggplot-polygons.R +++ b/tests/testthat/test-ggplot-polygons.R @@ -16,6 +16,9 @@ test_that("filled polygons become several traces", { expect_identical(info[[1]]$line$color, "transparent") expect_identical(info[[2]]$line$color, "transparent") + expect_identical(info[[1]]$showlegend, FALSE) + expect_identical(info[[2]]$showlegend, FALSE) + save_outputs(gg, "polygons-filled-polygons") first.color <- rgb(0.23, 0.45, 0.67) @@ -35,6 +38,9 @@ test_that("filled polygons become several traces", { expect_equal(info[[2]]$line$color, toRGB("springgreen3")) expect_equal(info[[2]]$name, "name2") + expect_identical(info[[1]]$showlegend, TRUE) + expect_identical(info[[2]]$showlegend, TRUE) + save_outputs(gg, "polygons-springgreen3") @@ -53,6 +59,9 @@ test_that("filled polygons become several traces", { expect_equal(info[[2]]$fillcolor, toRGB("springgreen3")) expect_equal(info[[2]]$name, "name2") + expect_identical(info[[1]]$showlegend, TRUE) + expect_identical(info[[2]]$showlegend, TRUE) + save_outputs(gg, "polygons-springgreen3-lab") @@ -74,6 +83,9 @@ test_that("filled polygons become several traces", { expect_equal(info[[2]]$line$dash, "dash") expect_equal(info[[2]]$name, "name2") + expect_identical(info[[1]]$showlegend, TRUE) + expect_identical(info[[2]]$showlegend, TRUE) + save_outputs(gg, "polygons-dashed") @@ -93,6 +105,8 @@ test_that("filled polygons become several traces", { expect_equal(info[[2]]$line$width, 6) expect_equal(info[[2]]$name, "name2") + expect_identical(info[[1]]$showlegend, TRUE) + expect_identical(info[[2]]$showlegend, TRUE) save_outputs(gg, "polygons-halloween") diff --git a/tests/testthat/test-ggplot-size.R b/tests/testthat/test-ggplot-size.R index 89c17b29e5..c688558e93 100644 --- a/tests/testthat/test-ggplot-size.R +++ b/tests/testthat/test-ggplot-size.R @@ -19,6 +19,8 @@ test_that("size is a vector if it is specified", { expect_that(m, is_a("list")) expect_true(length(m$size) > 1) + expect_identical(L[[1]]$showlegend, FALSE) + save_outputs(iplot, "size-is-a-vector") }) diff --git a/tests/testthat/test-ggplot-theme.R b/tests/testthat/test-ggplot-theme.R index 53968b1d41..f0f5692607 100644 --- a/tests/testthat/test-ggplot-theme.R +++ b/tests/testthat/test-ggplot-theme.R @@ -72,6 +72,7 @@ test_that("marker default shape is a circle", { info <- gg2list(gg) for (i in c(1:3)) { expect_identical(info[[i]]$marker$symbol, "circle") + expect_true(info[[i]]$showlegend) } save_outputs(gg, "theme-marker-default") diff --git a/tests/testthat/test-ggplot-ylim.R b/tests/testthat/test-ggplot-ylim.R index 3c749d861b..725787a621 100644 --- a/tests/testthat/test-ggplot-ylim.R +++ b/tests/testthat/test-ggplot-ylim.R @@ -32,4 +32,6 @@ test_that("ylim is respected for 1 trace", { info <- expect_traces(gg.ylim, 1, "one-trace") expected.ylim <- c(0, max(df$total_bill)) expect_equal(info$kwargs$layout$yaxis$range, expected.ylim) + + expect_identical(info$traces[[1]]$showlegend, FALSE) })