diff --git a/NEWS b/NEWS index 4e644d0dcb..e6c24311f1 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,11 @@ +NEW FEATURE: + +Fixed coordinates (i.e., coord_fixed()/coord_equal()) are now supported. + +BUGFIX: + +Long legend titles will no longer run off the screen, and legend titles with line breaks (\n) are now translated correctly. + 3.4.13 -- 6 Apr 2016 BUGFIX: diff --git a/R/ggplotly.R b/R/ggplotly.R index 7670ec5489..2aec910a5f 100644 --- a/R/ggplotly.R +++ b/R/ggplotly.R @@ -112,6 +112,7 @@ gg2list <- function(p, width = NULL, height = NULL, tooltip = "all", source = "A # ------------------------------------------------------------------------ # end of ggplot_build() # ------------------------------------------------------------------------ + # initiate plotly.js layout with some plot-wide theming stuff theme <- ggfun("plot_theme")(p) elements <- names(which(sapply(theme, inherits, "element"))) @@ -126,21 +127,78 @@ gg2list <- function(p, width = NULL, height = NULL, tooltip = "all", source = "A paper_bgcolor = toRGB(theme$plot.background$fill), font = text2font(theme$text) ) + # ensure there's enough space for the modebar (this is based on a height of 1em) + # https://github.com/plotly/plotly.js/blob/dd1547/src/components/modebar/index.js#L171 + gglayout$margin$t <- gglayout$margin$t + 16 # main plot title + # TODO: implement subtitle? https://github.com/hadley/ggplot2/pull/1582 if (nchar(p$labels$title %||% "") > 0) { gglayout$title <- faced(p$labels$title, theme$plot.title$face) gglayout$titlefont <- text2font(theme$plot.title) gglayout$margin$t <- gglayout$margin$t + gglayout$titlefont$size } - # ensure there's enough space for the modebar (this is based on a height of 1em) - # https://github.com/plotly/plotly.js/blob/dd1547/src/components/modebar/index.js#L171 - gglayout$margin$t <- gglayout$margin$t + 16 - - # important stuff like panel$ranges is already flipped, but - # p$scales/p$labels/data aren't. We flip x/y trace data at the very end - # and scales in the axis loop below. - if (inherits(p$coordinates, "CoordFlip")) { - p$labels[c("x", "y")] <- p$labels[c("y", "x")] + + # account for exterior facet strips in `layout.margin` + if (has_facet(p)) { + stripTextX <- theme$strip.text.x %||% theme$strip.text + gglayout$margin$t <- gglayout$margin$t + + unitConvert(stripTextX, "pixels", "height") + if (inherits(p$facet, "grid")) { + stripTextY <- theme$strip.text.y %||% theme$strip.text + gglayout$margin$r <- gglayout$margin$r + + unitConvert(stripTextY, "pixels", "width") + } + } + + # create xaxis/yaxis "templates" + # here we also account for ticks, tick text, and axis titles in `layout.margin` + # (the _entire_ plot margin must be known before we compute axis domains) + for (xy in c("x", "y")) { + # find axis specific theme elements that inherit from their parent + theme_el <- function(el) { + theme[[paste0(el, ".", xy)]] %||% theme[[el]] + } + axisTicks <- theme_el("axis.ticks") + axisText <- theme_el("axis.text") + axisTitle <- theme_el("axis.title") + axisLine <- theme_el("axis.line") + panelGrid <- theme_el("panel.grid.major") + stripText <- theme_el("strip.text") + # panel$ranges are flipped, but p$labels/p$scales aren't + if (inherits(p$coordinates, "CoordFlip")) xy <- setdiff(c("x", "y"), xy) + axisTitleText <- scales$get_scales(xy)$name %||% p$labels[[xy]] %||% "" + # type of unit conversion + type <- if (xy == "x") "height" else "width" + # all these axis properties are "global" + gglayout[[paste0(xy, "axis")]] <- axisObj <- list( + # titles are really drawn as annotations, but we'll need this later + title = if (is_blank(axisTitle)) "" else axisTitleText, + type = "linear", + autorange = FALSE, + tickmode = "array", + ticks = if (is_blank(axisTicks)) "" else "outside", + tickcolor = toRGB(axisTicks$colour), + ticklen = unitConvert(theme$axis.ticks.length, "pixels", type), + tickwidth = unitConvert(axisTicks, "pixels", type), + showticklabels = !is_blank(axisText), + tickfont = text2font(axisText, "height"), + tickangle = -(axisText$angle %||% 0), + showline = !is_blank(axisLine), + linecolor = toRGB(axisLine$colour), + linewidth = unitConvert(axisLine, "pixels", type), + showgrid = !is_blank(panelGrid), + gridcolor = toRGB(panelGrid$colour), + gridwidth = unitConvert(panelGrid, "pixels", type), + zeroline = FALSE + ) + # account for the _maximum_ tick text length + axisTickText <- unlist(lapply(panel$ranges, "[[", paste0(xy, ".labels"))) + axisTickText <- axisTickText[which.max(nchar(axisTickText))] + # (apparently ggplot2 doesn't support axis.title/axis.text margins) + side <- if (xy == "x") "b" else "l" + gglayout$margin[[side]] <- gglayout$margin[[side]] + axisObj$ticklen + + bbox(axisTickText, axisObj$tickangle, axisObj$tickfont$size)[[type]] + + bbox(axisObj$title, axisTitle$angle, unitConvert(axisTitle, "pixels", type))[[type]] } # important panel summary stats @@ -249,19 +307,146 @@ gg2list <- function(p, width = NULL, height = NULL, tooltip = "all", source = "A tr$hoverinfo <- tr$hoverinfo %||%"text" tr }) - # show only one legend entry per legendgroup + # only one legend entry per legendgroup grps <- sapply(traces, "[[", "legendgroup") traces <- Map(function(x, y) { x$showlegend <- isTRUE(x$showlegend) && y x }, traces, !duplicated(grps)) - + + # If a trace isn't named, it shouldn't have additional hoverinfo (i.e., trace0) + traces <- lapply(compact(traces), function(x) { x$name <- x$name %||% ""; x }) + # ------------------------------------------------------------------------ - # axis/facet/margin conversion + # guide conversion + # Strategy: Obtain and translate the output of ggplot2:::guides_train(). + # To do so, we borrow some of the body of ggplot2:::guides_build(). # ------------------------------------------------------------------------ + + # is there be a legend? + gglayout$showlegend <- sum(unlist(lapply(traces, "[[", "showlegend"))) > 1 + # legend styling + gglayout$legend <- list( + bgcolor = toRGB(theme$legend.background$fill), + bordercolor = toRGB(theme$legend.background$colour), + borderwidth = unitConvert(theme$legend.background$size, "pixels", "width"), + font = text2font(theme$legend.text) + ) + # if theme(legend.position = "none") is used, don't show a legend _or_ guide + if (npscales$n() == 0 || identical(theme$legend.position, "none")) { + gglayout$showlegend <- FALSE + } else { + # by default, guide boxes are vertically aligned + theme$legend.box <- theme$legend.box %||% "vertical" + + # size of key (also used for bar in colorbar guide) + theme$legend.key.width <- theme$legend.key.width %||% theme$legend.key.size + theme$legend.key.height <- theme$legend.key.height %||% theme$legend.key.size + + # legend direction must be vertical + theme$legend.direction <- theme$legend.direction %||% "vertical" + if (!identical(theme$legend.direction, "vertical")) { + warning( + "plotly.js does not (yet) support horizontal legend items \n", + "You can track progress here: \n", + "https://github.com/plotly/plotly.js/issues/53 \n", + call. = FALSE + ) + theme$legend.direction <- "vertical" + } + + # justification of legend boxes + theme$legend.box.just <- theme$legend.box.just %||% c("center", "center") + # scales -> data for guides + gdefs <- ggfun("guides_train")(scales, theme, p$guides, p$labels) + if (length(gdefs) > 0) { + gdefs <- ggfun("guides_merge")(gdefs) + gdefs <- ggfun("guides_geom")(gdefs, layers, p$mapping) + } + + # colourbar -> plotly.js colorbar + colorbar <- compact(lapply(gdefs, gdef2trace, theme, gglayout)) + nguides <- length(colorbar) + gglayout$showlegend + # If we have 2 or more guides, set x/y positions accordingly + if (nguides >= 2) { + # place legend at the bottom + gglayout$legend$y <- 1 / nguides + gglayout$legend$yanchor <- "top" + # adjust colorbar position(s) + for (i in seq_along(colorbar)) { + colorbar[[i]]$marker$colorbar$yanchor <- "top" + colorbar[[i]]$marker$colorbar$len <- 1 / nguides + colorbar[[i]]$marker$colorbar$y <- 1 - (i - 1) * (1 / nguides) + } + } + traces <- c(traces, colorbar) + + # legend title annotation - https://github.com/plotly/plotly.js/issues/276 + legendTitles <- unlist(lapply(gdefs, function(g) { + if (inherits(g, "legend")) strsplit(g$title, "\\\n") else NULL + })) + legendTitle <- paste(legendTitles, collapse = "
") + titleAnnotation <- make_label( + legendTitle, + x = gglayout$legend$x %||% 1.02, + y = gglayout$legend$y %||% 1, + theme$legend.title, + xanchor = "left", + yanchor = "top", + align = "left" + ) + gglayout$annotations <- c(gglayout$annotations, titleAnnotation) + # adjust the height of the legend to accomodate for the title + # this assumes the legend always appears below colorbars + gglayout$legend$y <- (gglayout$legend$y %||% 1) - + length(legendTitles) * unitConvert(theme$legend.title$size, "npc", "height") + } + + # legend width is required to estimate the graphing area + # (which is required to do fixed coordinates, among other things) + showLegend <- sapply(traces, "[[", "showlegend") + legendWidth <- if (gglayout$showlegend) { + # plotly.js accomodates for long legend entry names + # so if we have a long legend title, but short entry names, add whitespace! + titleAnnotation <- gglayout$annotations[[length(gglayout$annotations)]] + nCharTitle <- if (nchar(titleAnnotation$text %||% "") > 0) max(nchar(legendTitles)) else 0 + legendEntries <- sapply(traces, "[[", "name")[showLegend] + nCharEntry <- max(nchar(legendEntries)) + # trace with the longest _shown_ name + idx <- which(showLegend)[which.max(nchar(legendEntries))] + m <- gglayout$legend$font$size / (titleAnnotation$font$size %||% gglayout$legend$font$size) + if (nCharTitle * m > nCharEntry) { + leftOver <- round(nCharTitle * m) - nCharEntry + buffer <- paste(rep(" ", leftOver), collapse = "") + traces[[idx]]$name <- paste0(traces[[idx]]$name, buffer) + } + bbox(traces[[idx]]$name, 0, gglayout$legend$font$size)[["width"]] + } else { + 0 + } + + # plotting/graphing area in pixels (if user doesn't supply height/width, we + # approximate them using the graphics device) + plotSize <- list( + height = height %||% unitConvert(grid::unit(1, "npc"), "pixels", "height"), + width = width %||% unitConvert(grid::unit(1, "npc"), "pixels", "width") + ) + graphSize <- list( + height = plotSize$height - (gglayout$margin$t + gglayout$margin$b), + width = plotSize$width - (gglayout$margin$l + gglayout$margin$r) - legendWidth + ) + + # obtain ratio for fixed coordinates + ratio <- if (inherits(p$coordinates, "CoordFixed") && + !isTRUE(Reduce(`&&`, p$facet$free))) { + rng <- panel$ranges[[1]] + aspectRatio <- diff(rng$y.range)/diff(rng$x.range) * p$coordinates$ratio + aspectRatio * with(graphSize, width / height) + } else { + NULL + } - # panel margins must be computed before panel/axis loops - # (in order to use get_domains()) + # spacing between panels panelMarginX <- unitConvert( theme[["panel.margin.x"]] %||% theme[["panel.margin"]], "npc", "width" @@ -309,130 +494,33 @@ gg2list <- function(p, width = NULL, height = NULL, tooltip = "all", source = "A rep(panelMarginY, 2) ) - doms <- get_domains(nPanels, nRows, margins) + doms <- get_domains(nPanels, nRows, margins, ratio) for (i in seq_len(nPanels)) { lay <- panel$layout[i, ] for (xy in c("x", "y")) { - # find axis specific theme elements that inherit from their parent - theme_el <- function(el) { - theme[[paste0(el, ".", xy)]] %||% theme[[el]] - } - axisTicks <- theme_el("axis.ticks") - axisText <- theme_el("axis.text") - axisTitle <- theme_el("axis.title") - axisLine <- theme_el("axis.line") - panelGrid <- theme_el("panel.grid.major") - stripText <- theme_el("strip.text") - axisName <- lay[, paste0(xy, "axis")] - anchor <- lay[, paste0(xy, "anchor")] rng <- panel$ranges[[i]] - # stuff like panel$ranges is already flipped, but scales aren't - sc <- if (inherits(p$coordinates, "CoordFlip")) { - scales$get_scales(setdiff(c("x", "y"), xy)) - } else { - scales$get_scales(xy) - } - # type of unit conversion - type <- if (xy == "x") "height" else "width" - # https://plot.ly/r/reference/#layout-xaxis - axisObj <- list( - type = "linear", - autorange = FALSE, - tickmode = "array", - range = rng[[paste0(xy, ".range")]], - ticktext = rng[[paste0(xy, ".labels")]], - # TODO: implement minor grid lines with another axis object - # and _always_ hide ticks/text? - tickvals = rng[[paste0(xy, ".major")]], - ticks = if (is_blank(axisTicks)) "" else "outside", - tickcolor = toRGB(axisTicks$colour), - ticklen = unitConvert(theme$axis.ticks.length, "pixels", type), - tickwidth = unitConvert(axisTicks, "pixels", type), - showticklabels = !is_blank(axisText), - tickfont = text2font(axisText, "height"), - tickangle = - (axisText$angle %||% 0), - showline = !is_blank(axisLine), - linecolor = toRGB(axisLine$colour), - linewidth = unitConvert(axisLine, "pixels", type), - showgrid = !is_blank(panelGrid), - domain = sort(as.numeric(doms[i, paste0(xy, c("start", "end"))])), - gridcolor = toRGB(panelGrid$colour), - gridwidth = unitConvert(panelGrid, "pixels", type), - zeroline = FALSE, - anchor = anchor - ) - # convert dates to milliseconds (86400000 = 24 * 60 * 60 * 1000) - # this way both dates/datetimes are on same scale - # hopefully scale_name doesn't go away -- https://github.com/hadley/ggplot2/issues/1312 - if (identical("date", sc$scale_name)) { - axisObj$range <- axisObj$range * 86400000 - if (i == 1) { - traces <- lapply(traces, function(z) { z[[xy]] <- z[[xy]] * 86400000; z }) - } - } - # tickvals are currently on 0-1 scale, but we want them on data scale - axisObj$tickvals <- scales::rescale( - axisObj$tickvals, to = axisObj$range, from = c(0, 1) + gglayout[[axisName]] <- modifyList( + gglayout[[paste0(xy, "axis")]], + list( + anchor = lay[, paste0(xy, "anchor")], + range = rng[[paste0(xy, ".range")]], + ticktext = rng[[paste0(xy, ".labels")]], + # tickvals come on 0-1 scale, but we want them on data scale + tickvals = scales::rescale( + rng[[paste0(xy, ".major")]], rng[[paste0(xy, ".range")]], from = c(0, 1) + ), + domain = sort(as.numeric(doms[i, paste0(xy, c("start", "end"))])) + ) ) - # attach axis object to the layout - gglayout[[axisName]] <- axisObj - - # do some stuff that should be done once for the entire plot - if (i == 1) { - # add space for exterior facet strips in `layout.margin` - if (has_facet(p)) { - stripSize <- unitConvert(stripText, "pixels", type) - if (xy == "x") { - gglayout$margin$t <- gglayout$margin$t + stripSize - } - if (xy == "y" && inherits(p$facet, "grid")) { - gglayout$margin$r <- gglayout$margin$r + stripSize - } - } - axisTitleText <- sc$name %||% p$labels[[xy]] %||% "" - if (is_blank(axisTitle)) axisTitleText <- "" - axisTickText <- axisObj$ticktext[which.max(nchar(axisObj$ticktext))] - side <- if (xy == "x") "b" else "l" - # account for axis ticks, ticks text, and titles in plot margins - # (apparently ggplot2 doesn't support axis.title/axis.text margins) - gglayout$margin[[side]] <- gglayout$margin[[side]] + axisObj$ticklen + - bbox(axisTickText, axisObj$tickangle, axisObj$tickfont$size)[[type]] + - bbox(axisTitleText, axisTitle$angle, unitConvert(axisTitle, "pixels", type))[[type]] - # draw axis titles as annotations - # (plotly.js axis titles aren't smart enough to dodge ticks & text) - if (nchar(axisTitleText) > 0) { - axisTextSize <- unitConvert(axisText, "npc", type) - axisTitleSize <- unitConvert(axisTitle, "npc", type) - offset <- - (0 - - bbox(axisTickText, axisText$angle, axisTextSize)[[type]] - - bbox(axisTitleText, axisTitle$angle, axisTitleSize)[[type]] / 2 - - unitConvert(theme$axis.ticks.length, "npc", type)) - # npc is on a 0-1 scale of the _entire_ device, - # but these units _should_ be wrt to the plotting region - # multiplying the offset by 2 seems to work, but this is a terrible hack - offset <- 1.75 * offset - x <- if (xy == "x") 0.5 else offset - y <- if (xy == "x") offset else 0.5 - gglayout$annotations <- c( - gglayout$annotations, - make_label( - faced(axisTitleText, axisTitle$face), x, y, el = axisTitle, - xanchor = "center", yanchor = "middle" - ) - ) - } - } - } # end of axis loop - + # panel border is a element_rect() xdom <- gglayout[[lay[, "xaxis"]]]$domain ydom <- gglayout[[lay[, "yaxis"]]]$domain border <- make_panel_border(xdom, ydom, theme) gglayout$shapes <- c(gglayout$shapes, border) - + # facet strips -> plotly annotations if (!is_blank(theme[["strip.text.x"]]) && (inherits(p$facet, "wrap") || inherits(p$facet, "grid") && lay$ROW == 1)) { @@ -463,91 +551,56 @@ gg2list <- function(p, width = NULL, height = NULL, tooltip = "all", source = "A strip <- make_strip_rect(xdom, ydom, theme, "right") gglayout$shapes <- c(gglayout$shapes, strip) } - } # end of panel loop - - # ------------------------------------------------------------------------ - # guide conversion - # Strategy: Obtain and translate the output of ggplot2:::guides_train(). - # To do so, we borrow some of the body of ggplot2:::guides_build(). - # ------------------------------------------------------------------------ - # will there be a legend? - gglayout$showlegend <- sum(unlist(lapply(traces, "[[", "showlegend"))) > 1 - - # legend styling - gglayout$legend <- list( - bgcolor = toRGB(theme$legend.background$fill), - bordercolor = toRGB(theme$legend.background$colour), - borderwidth = unitConvert(theme$legend.background$size, "pixels", "width"), - font = text2font(theme$legend.text) - ) - - # if theme(legend.position = "none") is used, don't show a legend _or_ guide - if (npscales$n() == 0 || identical(theme$legend.position, "none")) { - gglayout$showlegend <- FALSE - } else { - # by default, guide boxes are vertically aligned - theme$legend.box <- theme$legend.box %||% "vertical" - - # size of key (also used for bar in colorbar guide) - theme$legend.key.width <- theme$legend.key.width %||% theme$legend.key.size - theme$legend.key.height <- theme$legend.key.height %||% theme$legend.key.size - - # legend direction must be vertical - theme$legend.direction <- theme$legend.direction %||% "vertical" - if (!identical(theme$legend.direction, "vertical")) { - warning( - "plotly.js does not (yet) support horizontal legend items \n", - "You can track progress here: \n", - "https://github.com/plotly/plotly.js/issues/53 \n", - call. = FALSE - ) - theme$legend.direction <- "vertical" + + # draw axis titles as annotations + # (plotly.js axis titles aren't smart enough to dodge ticks & text) + xAxes <- gglayout[grep("^xaxis", names(gglayout))] + yAxes <- gglayout[grep("^yaxis", names(gglayout))] + xDomain <- range(unlist(lapply(xAxes, "[[", "domain"))) + yDomain <- range(unlist(lapply(yAxes, "[[", "domain"))) + + for (ax in c("xaxis", "yaxis")) { + if (nchar(gglayout[[ax]]$title %||% "") == 0) next + x <- if (ax == "xaxis") mean(xDomain) else min(xDomain) + y <- if (ax == "xaxis") min(yDomain) else mean(yDomain) + tickText <- gglayout[[ax]]$ticktext + tickText <- tickText[which.max(nchar(tickText))] + type <- if (ax == "xaxis") "height" else "width" + # scale tickfont size to graphing region + fontSize <- unitConvert(grid::unit(1, "npc"), "pixels", type) / graphSize[[type]] * gglayout[[ax]]$tickfont$size + tickSize <- gglayout[[ax]]$ticklen + + bbox(tickText, gglayout[[ax]]$tickangle, fontSize)[[type]] + # estimate the offset required to dodge ticks/ticktext + if (ax == "xaxis") { + axisTitle <- theme$axis.title %||% theme$axis.title.x + y <- y - + unitConvert(axisTitle, "npc", "height") - + # the multiplier is for a little cushion + 1.5 * (tickSize) / (graphSize$height) } - - # justification of legend boxes - theme$legend.box.just <- theme$legend.box.just %||% c("center", "center") - # scales -> data for guides - gdefs <- ggfun("guides_train")(scales, theme, p$guides, p$labels) - if (length(gdefs) > 0) { - gdefs <- ggfun("guides_merge")(gdefs) - gdefs <- ggfun("guides_geom")(gdefs, layers, p$mapping) + if (ax == "yaxis") { + axisTitle <- theme$axis.title %||% theme$axis.title.y + x <- x - + unitConvert(axisTitle, "npc", "width") - + 1.6 * (tickSize) / (graphSize$width) } - - # colourbar -> plotly.js colorbar - colorbar <- compact(lapply(gdefs, gdef2trace, theme, gglayout)) - nguides <- length(colorbar) + gglayout$showlegend - # If we have 2 or more guides, set x/y positions accordingly - if (nguides >= 2) { - # place legend at the bottom - gglayout$legend$y <- 1 / nguides - gglayout$legend$yanchor <- "top" - # adjust colorbar position(s) - for (i in seq_along(colorbar)) { - colorbar[[i]]$marker$colorbar$yanchor <- "top" - colorbar[[i]]$marker$colorbar$len <- 1 / nguides - colorbar[[i]]$marker$colorbar$y <- 1 - (i - 1) * (1 / nguides) - } - } - traces <- c(traces, colorbar) - - # legend title annotation - https://github.com/plotly/plotly.js/issues/276 - legendTitles <- compact(lapply(gdefs, function(g) if (inherits(g, "legend")) g$title else NULL)) - legendTitle <- paste(legendTitles, collapse = "
") - titleAnnotation <- make_label( - legendTitle, - x = gglayout$legend$x %||% 1.02, - y = gglayout$legend$y %||% 1, - theme$legend.title, - xanchor = "left", - yanchor = "top" + gglayout$annotations <- c( + gglayout$annotations, + make_label( + txt = faced(gglayout[[ax]]$title, gglayout[[ax]]$face), + x = x, y = y, el = axisTitle, + xanchor = "center", yanchor = "middle" + ) ) - gglayout$annotations <- c(gglayout$annotations, titleAnnotation) - # adjust the height of the legend to accomodate for the title - # this assumes the legend always appears below colorbars - gglayout$legend$y <- (gglayout$legend$y %||% 1) - - length(legendTitles) * unitConvert(theme$legend.title$size, "npc", "height") } + + idx <- grepl("^xaxis", names(gglayout)) + gglayout[idx] <- lapply(gglayout[idx], function(x) {x$title <- NULL; x}) + idy <- grepl("^yaxis", names(gglayout)) + gglayout[idy] <- lapply(gglayout[idy], function(x) {x$title <- NULL; x}) + + # geom_bar() hacks geoms <- sapply(layers, ggtype, "geom") @@ -633,10 +686,8 @@ gg2list <- function(p, width = NULL, height = NULL, tooltip = "all", source = "A for (i in ax) { gglayout[[i]]$hoverformat <- ".2f" } - # If a trace isn't named, it shouldn't have additional hoverinfo - traces <- lapply(compact(traces), function(x) { x$name <- x$name %||% ""; x }) - l <- list(data = setNames(traces, NULL), layout = compact(gglayout)) + l <- list(data = setNames(compact(traces), NULL), layout = compact(gglayout)) # ensure properties are boxed correctly l <- add_boxed(rm_asis(l)) l$width <- width @@ -694,15 +745,16 @@ mm2pixels <- function(u) { } verifyUnit <- function(u) { - # the default unit in ggplot2 is millimeters (unless it's element_text()) - if (is.null(attr(u, "unit"))) { - u <- if (inherits(u, "element")) { - grid::unit(u$size %||% 0, "points") - } else { - grid::unit(u %||% 0, "mm") - } - } - u + # in ggplot2, data size is commonly in mm, while elements are in points + if (inherits(u, "unit")) return(u) + if (inherits(u, "element")) return(grid::unit(u$size %||% 0, "points")) + grid::unit(u %||% 0, "mm") +} + +# approximate the height/width ratio of the device +device_ratio <- function() { + unitConvert(grid::unit(1, "npc"), "pixels", "height") / + unitConvert(grid::unit(1, "npc"), "pixels", "width") } # detect a blank theme element @@ -859,8 +911,8 @@ gdef2trace <- function(gdef, theme, gglayout) { gdef$bar$value <- scales::rescale(gdef$bar$value, from = rng) gdef$key$.value <- scales::rescale(gdef$key$.value, from = rng) list( - x = gglayout$xaxis$range, - y = gglayout$yaxis$range, + x = NA, + y = NA, # esentially to prevent this getting merged at a later point name = gdef$hash, type = "scatter", @@ -898,3 +950,5 @@ gdef2trace <- function(gdef, theme, gglayout) { NULL } } + + diff --git a/R/subplots.R b/R/subplots.R index 50b1794855..d466796e3a 100644 --- a/R/subplots.R +++ b/R/subplots.R @@ -139,17 +139,25 @@ subplot <- function(..., nrows = 1, which_layout = "merge", margin = 0.02) { hash_plot(data.frame(), p) } - -get_domains <- function(nplots = 1, nrows = 1, margins = 0.01) { +# aspect ratio (y / x) +get_domains <- function(nplots = 1, nrows = 1, margins = 0.01, aspect = NULL) { if (length(margins) == 1) margins <- rep(margins, 4) if (length(margins) != 4) stop("margins must be length 1 or 4", call. = FALSE) ncols <- ceiling(nplots / nrows) + cushion <- list(x = 0, y = 0) + if (!is.null(aspect) && aspect < 1) { + cushion <- list(x = 0, y = (1 - aspect) / 2) + } + if (!is.null(aspect) && aspect >= 1) { + cushion <- list(x = (1 - 1 / aspect) / 2, y = 0) + } + xs <- vector("list", ncols) for (i in seq_len(ncols)) { xs[[i]] <- c( - xstart = ((i - 1) / ncols) + ifelse(i == 1, 0, margins[1]), - xend = (i / ncols) - ifelse(i == ncols, 0, margins[2]) + xstart = ((i - 1) / ncols) + ifelse(i == 1, 0 + cushion$x, margins[1]), + xend = (i / ncols) - ifelse(i == ncols, 0 + cushion$x, margins[2]) ) } xz <- rep_len(xs, nplots) @@ -158,8 +166,8 @@ get_domains <- function(nplots = 1, nrows = 1, margins = 0.01) { for (i in seq_len(nplots)) { j <- ceiling(i / ncols) ys[[i]] <- c( - ystart = 1 - ((j - 1) / nrows) - ifelse(j == 1, 0, margins[3]), - yend = 1 - (j / nrows) + ifelse(j == nrows, 0, margins[4]) + ystart = 1 - ((j - 1) / nrows) - ifelse(j == 1, 0 + cushion$y, margins[3]), + yend = 1 - (j / nrows) + ifelse(j == nrows, 0 + cushion$y, margins[4]) ) } list2df(Map(c, xz, ys)) diff --git a/tests/testthat/test-ggplot-bar.R b/tests/testthat/test-ggplot-bar.R index 9f507df0f0..c616c15e22 100644 --- a/tests/testthat/test-ggplot-bar.R +++ b/tests/testthat/test-ggplot-bar.R @@ -48,7 +48,7 @@ test_that("dates work well with bar charts", { info <- expect_traces(gd, 2, "dates") trs <- info$data # plotly likes time in milliseconds - t <- as.numeric(unique(researchers$month)) * 24 * 60 * 60 * 1000 + t <- as.numeric(unique(researchers$month)) expect_equal(trs[[1]]$x, t) }) diff --git a/tests/testthat/test-ggplot-coord.R b/tests/testthat/test-ggplot-coord.R new file mode 100644 index 0000000000..7d0894d50b --- /dev/null +++ b/tests/testthat/test-ggplot-coord.R @@ -0,0 +1,50 @@ +context("coord") + +base <- qplot(mpg, wt, data = mtcars) +p <- base + coord_fixed() + +test_that("simple fixed coordinates", { + l <- save_outputs(p, "coord-fixed") + # I don't think there's a good way to test ratios explictly... + # we'll rely on visual testing for now +}) + +base2 <- qplot(wt, mpg, data = mtcars) +p <- base2 + coord_fixed() + +test_that("simple fixed coordinates", { + l <- save_outputs(p, "coord-fixed2") + # I don't think there's a good way to test ratios explictly... + # we'll rely on visual testing for now +}) + + +p <- base + + facet_grid(vs ~ am, labeller = label_both) + + coord_fixed() + ylim(0, 5) + +test_that("fixed coordinates with facets", { + l <- save_outputs(p, "coord-fixed-facet") +}) + +p <- base2 + + facet_grid(vs ~ am, labeller = label_both) + + coord_fixed() + +test_that("fixed coordinates with facets", { + l <- save_outputs(p, "coord-fixed-facet2") +}) + +p <- qplot(1:10, rep(1:2, 5), colour = sapply(11:20, function(x) paste(rep("a", x), collapse = ""))) + + coord_fixed() + scale_color_discrete("aslk") + +test_that("fixed coordinates with long legend entries", { + l <- save_outputs(p, "coord-fixed-long-entries") +}) + +p <- qplot(1:10, rep(1:2, 5), colour = factor(1:10)) + + coord_fixed() + scale_color_discrete("aslkdsadklnasn\nsa;mkdas;dm") + +test_that("fixed coordinates with long legend title", { + l <- save_outputs(p, "coord-fixed-long-title") +}) diff --git a/tests/testthat/test-ggplot-density.R b/tests/testthat/test-ggplot-density.R index df23b36d3b..adca009d05 100644 --- a/tests/testthat/test-ggplot-density.R +++ b/tests/testthat/test-ggplot-density.R @@ -68,7 +68,7 @@ p <- ggplot(data = mtcars, aes(x = mpg, fill = factor(cyl))) + test_that("traces are ordered correctly in geom_density", { info <- expect_traces(p, 3, "traces_order") - nms <- as.character(sapply(info$data, "[[", "name")) + nms <- sub("\\s+$", "", as.character(sapply(info$data, "[[", "name"))) expect_identical(nms, c("4", "6", "8")) }) diff --git a/tests/testthat/test-ggplot-density2d.R b/tests/testthat/test-ggplot-density2d.R index 60dce56040..ecc32c6c3a 100644 --- a/tests/testthat/test-ggplot-density2d.R +++ b/tests/testthat/test-ggplot-density2d.R @@ -60,13 +60,5 @@ test_that("StatDensity2d with GeomPolygon translates to filled path(s)", { #test some properties that shouldn't be sensitive to ggplot2 defaults expect_true(colorbar$marker$colorbar$title == "level") - # are the hidden colorbar markers on the correct range? - for (xy in c("x", "y")) { - rng <- L$layout[[paste0(xy, "axis")]]$range - expect_true( - all(min(rng) <= colorbar[[xy]] & colorbar[[xy]] <= max(rng)) - ) - } - }) diff --git a/tests/testthat/test-ggplot-legend.R b/tests/testthat/test-ggplot-legend.R index 958799dc1f..105af534a1 100644 --- a/tests/testthat/test-ggplot-legend.R +++ b/tests/testthat/test-ggplot-legend.R @@ -23,16 +23,16 @@ test_that("Discrete colour and shape get merged into one legend", { # 5 legend entries expect_equal(sum(sapply(info$data, "[[", "showlegend")), 5) # verify entries are sorted correctly - nms <- sapply(info$data, "[[", "name") + nms <- sub("\\s+$", "", sapply(info$data, "[[", "name")) d <- unique(mtcars[c("vs", "cyl")]) d <- d[order(d$vs, d$cyl), ] expect_identical( nms, paste0("(", d$vs, ",", d$cyl, ")") ) a <- info$layout$annotations - expect_match(a[[3]]$text, "^factor\\(vs\\)") - expect_match(a[[3]]$text, "factor\\(cyl\\)$") - expect_true(a[[3]]$y > info$layout$legend$y) + expect_match(a[[1]]$text, "^factor\\(vs\\)") + expect_match(a[[1]]$text, "factor\\(cyl\\)$") + expect_true(a[[1]]$y > info$layout$legend$y) }) diff --git a/vignettes/Untitled.Rmd b/vignettes/Untitled.Rmd new file mode 100644 index 0000000000..d423af70a8 --- /dev/null +++ b/vignettes/Untitled.Rmd @@ -0,0 +1,88 @@ +--- +title: "ggplotly: various examples" +author: "Carson Sievert" +output: + flexdashboard::flex_dashboard: + orientation: rows + social: menu + source_code: embed +--- + +```{r setup, include=FALSE} +library(plotly) +library(maps) +knitr::opts_chunk$set(message = FALSE) +``` + +Row {data-height=600} +------------------------------------------------------------------------------ + +### Unemployment + +```{r} +# This example modifies code from Hadley Wickham (https://gist.github.com/hadley/233134) +# It also uses data from Nathan Yau's flowingdata site (http://flowingdata.com/) +unemp <- read.csv("http://datasets.flowingdata.com/unemployment09.csv") +names(unemp) <- c("id", "state_fips", "county_fips", "name", "year", + "?", "?", "?", "rate") +unemp$county <- tolower(gsub(" County, [A-Z]{2}", "", unemp$name)) +unemp$state <- gsub("^.*([A-Z]{2}).*$", "\\1", unemp$name) +county_df <- map_data("county") +names(county_df) <- c("long", "lat", "group", "order", "state_name", "county") +county_df$state <- state.abb[match(county_df$state_name, tolower(state.name))] +county_df$state_name <- NULL +state_df <- map_data("state") +choropleth <- merge(county_df, unemp, by = c("state", "county")) +choropleth <- choropleth[order(choropleth$order), ] +choropleth$rate_d <- cut(choropleth$rate, breaks = c(seq(0, 10, by = 2), 35)) + +# provide a custom tooltip to plotly with the county name and actual rate +choropleth$text <- with(choropleth, paste0("County: ", name, "
Rate: ", rate)) +p <- ggplot(choropleth, aes(long, lat, group = group)) + + geom_polygon(aes(fill = rate_d, text = text), + colour = alpha("white", 1/2), size = 0.2) + + geom_polygon(data = state_df, colour = "white", fill = NA) + + scale_fill_brewer(palette = "PuRd") + theme_void() +# just show the text aesthetic in the tooltip +ggplotly(p, tooltip = "text") +``` + +### Crimes + +```{r} +crimes <- data.frame(state = tolower(rownames(USArrests)), USArrests) +crimesm <- tidyr::gather(crimes, variable, value, -state) +states_map <- map_data("state") +g <- ggplot(crimesm, aes(map_id = state)) + + geom_map(aes(fill = value), map = states_map) + + expand_limits(x = states_map$long, y = states_map$lat) + + facet_wrap( ~ variable) + theme_void() +ggplotly(g) +``` + +Row {data-height=400} +------------------------------------------------------------------------------ + +### Faithful Eruptions + +```{r} +m <- ggplot(faithful, aes(x = eruptions, y = waiting)) + + stat_density_2d() + xlim(0.5, 6) + ylim(40, 110) +ggplotly(m) +``` + +### Faithful Eruptions (polygon) + +```{r} +m <- ggplot(faithful, aes(x = eruptions, y = waiting)) + + stat_density_2d(aes(fill = ..level..), geom = "polygon") + + xlim(0.5, 6) + ylim(40, 110) +ggplotly(m) +``` + +### Faithful Eruptions (hex) + +```{r} +m <- ggplot(faithful, aes(x = eruptions, y = waiting)) + geom_hex() +ggplotly(m) +``` diff --git a/vignettes/intro.Rmd b/vignettes/intro.Rmd index d3d5ffe61d..a11e46fce2 100644 --- a/vignettes/intro.Rmd +++ b/vignettes/intro.Rmd @@ -36,7 +36,7 @@ You can manually add a trace to an existing plot with `add_trace()`. In that cas ```{r} m <- loess(unemploy / pop ~ as.numeric(date), data = economics) p <- plot_ly(economics, x = date, y = unemploy / pop, name = "raw") -add_trace(p, y = fitted(m), name = "loess") +add_trace(p, x = date, y = fitted(m), name = "loess") ``` __plotly__ was designed with a [pure, predictable, and pipeable interface](https://dl.dropboxusercontent.com/u/41902/pipe-dsls.pdf) in mind, so you can also use the `%>%` operator to create a visualization pipeline: @@ -44,7 +44,7 @@ __plotly__ was designed with a [pure, predictable, and pipeable interface](https ```{r} economics %>% plot_ly(x = date, y = unemploy / pop) %>% - add_trace(y = fitted(m)) %>% + add_trace(x = date, y = fitted(m)) %>% layout(showlegend = F) ```