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)
```