diff --git a/DESCRIPTION b/DESCRIPTION
index 02d5d246b4..c44e822b8b 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -1,6 +1,6 @@
Package: plotly
Title: Create Interactive Web Graphics via 'plotly.js'
-Version: 3.4.3
+Version: 3.4.4
Authors@R: c(person("Carson", "Sievert", role = c("aut", "cre"),
email = "cpsievert1@gmail.com"),
person("Chris", "Parmer", role = c("aut", "cph"),
diff --git a/NEWS b/NEWS
index a1c68c8b93..22f4b857c8 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,9 @@
+3.4.4 -- 17 Mar 2016
+
+BUGFIX:
+
+Show discrete positional values in tooltip (see #515); better GeomTile conversion; pass plot object into layers2traces.
+
3.4.3 -- 14 Mar 2016
BUGFIX:
diff --git a/R/ggplotly.R b/R/ggplotly.R
index 62e50e820a..7670ec5489 100644
--- a/R/ggplotly.R
+++ b/R/ggplotly.R
@@ -73,6 +73,13 @@ gg2list <- function(p, width = NULL, height = NULL, tooltip = "all", source = "A
scale_x <- function() scales$get_scales("x")
scale_y <- function() scales$get_scales("y")
panel <- ggfun("train_position")(panel, data, scale_x(), scale_y())
+ # Before mapping x/y position, save the domain (for discrete scales)
+ # to display in tooltip.
+ data <- lapply(data, function(d) {
+ if (!is.null(scale_x()) && scale_x()$is_discrete()) d$x_plotlyDomain <- d$x
+ if (!is.null(scale_y()) && scale_y()$is_discrete()) d$y_plotlyDomain <- d$y
+ d
+ })
data <- ggfun("map_position")(panel, data, scale_x(), scale_y())
# for some geoms (e.g. boxplots) plotly.js needs the "pre-statistics" data
prestats_data <- data
@@ -187,21 +194,18 @@ gg2list <- function(p, width = NULL, height = NULL, tooltip = "all", source = "A
# stat specific mappings
grep("^\\.\\.", as.character(x$stat$default_aes), value = TRUE)
)
- # remove leading/trailing dots in "hidden" stat aes
- map <- sub("^\\.\\.", "", sub("\\.\\.$", "", map))
+ # "hidden" names should be taken verbatim
+ idx <- grepl("^\\.\\.", map) & grepl("\\.\\.$", map)
+ hiddenMap <- sub("^\\.\\.", "", sub("\\.\\.$", "", map))
+ map[idx] <- hiddenMap[idx]
+ names(map)[idx] <- hiddenMap[idx]
if (!identical(tooltip, "all")) {
map <- map[tooltip]
}
- # tooltips for discrete positional scales are misleading
- if (scales$get_scales("x")$is_discrete()) {
- map <- map[!names(map) %in% "x"]
- }
- if (scales$get_scales("y")$is_discrete()) {
- map <- map[!names(map) %in% "y"]
- }
map
})
+
# attach a new column (hovertext) to each layer of data that should get mapped
# to the text trace property
data <- Map(function(x, y) {
@@ -234,10 +238,11 @@ gg2list <- function(p, width = NULL, height = NULL, tooltip = "all", source = "A
x
}, data, aesMap)
+
+
# layers -> plotly.js traces
- traces <- layers2traces(
- data, prestats_data, layers, panel$layout, scales, p$labels
- )
+ traces <- layers2traces(data, prestats_data, panel$layout, p)
+
# default to just the text in hover info, mainly because of this
# https://github.com/plotly/plotly.js/issues/320
traces <- lapply(traces, function(tr) {
diff --git a/R/layers2traces.R b/R/layers2traces.R
index 899e941886..0ea3b0d53f 100644
--- a/R/layers2traces.R
+++ b/R/layers2traces.R
@@ -1,14 +1,14 @@
# layer -> trace conversion
-layers2traces <- function(data, prestats_data, layers, layout, scales, labels) {
+layers2traces <- function(data, prestats_data, layout, p) {
# Attach a "geom class" to each layer of data for method dispatch
- data <- Map(function(x, y) prefix_class(x, class(y$geom)[1]), data, layers)
+ data <- Map(function(x, y) prefix_class(x, class(y$geom)[1]), data, p$layers)
# Extract parameters for each layer
- params <- lapply(layers, function(x) {
+ params <- lapply(p$layers, function(x) {
c(x$geom_params, x$stat_params, x$aes_params, position = ggtype(x, "position"))
})
# we draw legends only for discrete scales
discreteScales <- list()
- for (sc in scales$non_position_scales()$scales) {
+ for (sc in p$scales$non_position_scales()$scales) {
if (sc$is_discrete()) {
discreteScales[[sc$aesthetics]] <- sc
}
@@ -22,7 +22,7 @@ layers2traces <- function(data, prestats_data, layers, layout, scales, labels) {
for (i in seq_along(data)) {
# This has to be done in a loop, since some layers are really two layers,
# (and we need to replicate the data/params in those cases)
- d <- to_basic(data[[i]], prestats_data[[i]], layout, params[[i]])
+ d <- to_basic(data[[i]], prestats_data[[i]], layout, params[[i]], p)
if (is.data.frame(d)) d <- list(d)
for (j in seq_along(d)) {
datz <- c(datz, d[j])
@@ -52,7 +52,7 @@ layers2traces <- function(data, prestats_data, layers, layout, scales, labels) {
if (all(is.na(fac))) fac <- 1
dl <- split(d, fac, drop = TRUE)
# list of traces for this layer
- trs <- Map(geom2trace, dl, paramz[i])
+ trs <- Map(geom2trace, dl, paramz[i], list(p))
# are we splitting by a discrete scale on this layer?
# if so, set name/legendgroup/showlegend
isDiscrete <- names(d) %in% paste0(names(discreteScales), "_plotlyDomain")
@@ -104,14 +104,15 @@ layers2traces <- function(data, prestats_data, layers, layout, scales, labels) {
#' @param prestats_data the data before statistics are computed.
#' @param layout the panel layout.
#' @param params parameters for the geom, statistic, and 'constant' aesthetics
+#' @param p a ggplot2 object (the conversion may depend on scales, for instance).
#' @param ... currently ignored
#' @export
-to_basic <- function(data, prestats_data, layout, params, ...) {
+to_basic <- function(data, prestats_data, layout, params, p, ...) {
UseMethod("to_basic")
}
#' @export
-to_basic.GeomViolin <- function(data, prestats_data, layout, params, ...) {
+to_basic.GeomViolin <- function(data, prestats_data, layout, params, p, ...) {
n <- nrow(data)
revData <- data[order(data$y, decreasing = TRUE), ]
idx <- !names(data) %in% c("x", "xmin", "xmax")
@@ -125,7 +126,7 @@ to_basic.GeomViolin <- function(data, prestats_data, layout, params, ...) {
}
#' @export
-to_basic.GeomBoxplot <- function(data, prestats_data, layout, params, ...) {
+to_basic.GeomBoxplot <- function(data, prestats_data, layout, params, p, ...) {
aez <- names(GeomBoxplot$default_aes)
for (i in aez) {
prestats_data[[i]] <- NULL
@@ -138,7 +139,7 @@ to_basic.GeomBoxplot <- function(data, prestats_data, layout, params, ...) {
}
#' @export
-to_basic.GeomSmooth <- function(data, prestats_data, layout, params, ...) {
+to_basic.GeomSmooth <- function(data, prestats_data, layout, params, p, ...) {
dat <- prefix_class(data, "GeomPath")
dat$alpha <- NULL
if (!identical(params$se, FALSE)) {
@@ -150,33 +151,33 @@ to_basic.GeomSmooth <- function(data, prestats_data, layout, params, ...) {
}
#' @export
-to_basic.GeomRibbon <- function(data, prestats_data, layout, params, ...) {
+to_basic.GeomRibbon <- function(data, prestats_data, layout, params, p, ...) {
prefix_class(ribbon_dat(data), "GeomPolygon")
}
#' @export
-to_basic.GeomArea <- function(data, prestats_data, layout, params, ...) {
+to_basic.GeomArea <- function(data, prestats_data, layout, params, p, ...) {
prefix_class(ribbon_dat(data), "GeomPolygon")
}
#' @export
-to_basic.GeomDensity <- function(data, prestats_data, layout, params, ...) {
+to_basic.GeomDensity <- function(data, prestats_data, layout, params, p, ...) {
prefix_class(ribbon_dat(data), "GeomPolygon")
}
#' @export
-to_basic.GeomLine <- function(data, prestats_data, layout, params, ...) {
+to_basic.GeomLine <- function(data, prestats_data, layout, params, p, ...) {
data <- data[order(data$x), ]
prefix_class(data, "GeomPath")
}
#' @export
-to_basic.GeomStep <- function(data, prestats_data, layout, params, ...) {
+to_basic.GeomStep <- function(data, prestats_data, layout, params, p, ...) {
prefix_class(data, "GeomPath")
}
#' @export
-to_basic.GeomSegment <- function(data, prestats_data, layout, params, ...) {
+to_basic.GeomSegment <- function(data, prestats_data, layout, params, p, ...) {
# Every row is one segment, we convert to a line with several
# groups which can be efficiently drawn by adding NA rows.
data$group <- seq_len(nrow(data))
@@ -189,7 +190,7 @@ to_basic.GeomSegment <- function(data, prestats_data, layout, params, ...) {
}
#' @export
-to_basic.GeomRect <- function(data, prestats_data, layout, params, ...) {
+to_basic.GeomRect <- function(data, prestats_data, layout, params, p, ...) {
data$group <- seq_len(nrow(data))
others <- data[!names(data) %in% c("xmin", "ymin", "xmax", "ymax")]
data <- with(data, {
@@ -202,7 +203,7 @@ to_basic.GeomRect <- function(data, prestats_data, layout, params, ...) {
}
#' @export
-to_basic.GeomMap <- function(data, prestats_data, layout, params, ...) {
+to_basic.GeomMap <- function(data, prestats_data, layout, params, p, ...) {
common <- intersect(data$map_id, params$map$id)
data <- data[data$map_id %in% common, , drop = FALSE]
map <- params$map[params$map$id %in% common, , drop = FALSE]
@@ -216,26 +217,24 @@ to_basic.GeomMap <- function(data, prestats_data, layout, params, ...) {
}
#' @export
-to_basic.GeomRaster <- function(data, prestats_data, layout, params, ...) {
+to_basic.GeomRaster <- function(data, prestats_data, layout, params, p, ...) {
data <- prefix_class(data, "GeomTile")
to_basic(data, prestats_data, layout, params)
}
#' @export
-to_basic.GeomTile <- function(data, prestats_data, layout, params, ...) {
- # geom2trace.GeomTile is a heatmap, which requires continuous fill and
- # a complete grid
- g <- expand.grid(unique(data$x), unique(data$y))
- if (nrow(g) != nrow(data) || is.discrete(prestats_data$fill)) {
+to_basic.GeomTile <- function(data, prestats_data, layout, params, p, ...) {
+ # geom2trace.GeomTile is a heatmap, which requires continuous fill
+ if (is.discrete(prestats_data$fill)) {
data <- prefix_class(data, "GeomRect")
- to_basic(data, prestats_data, layout, params)
+ to_basic(data, prestats_data, layout, params, p)
} else {
data
}
}
#' @export
-to_basic.GeomHex <- function(data, prestats_data, layout, params, ...) {
+to_basic.GeomHex <- function(data, prestats_data, layout, params, p, ...) {
# see ggplot2:::hexGrob
dx <- resolution(data$x, FALSE)
dy <- resolution(data$y, FALSE)/sqrt(3)/2 * 1.15
@@ -252,13 +251,13 @@ to_basic.GeomHex <- function(data, prestats_data, layout, params, ...) {
}
#' @export
-to_basic.GeomContour <- function(data, prestats_data, layout, params, ...) {
+to_basic.GeomContour <- function(data, prestats_data, layout, params, p, ...) {
if (!"fill" %in% names(data)) data$fill <- NA
prefix_class(data, "GeomPath")
}
#' @export
-to_basic.GeomDensity2d <- function(data, prestats_data, layout, params, ...) {
+to_basic.GeomDensity2d <- function(data, prestats_data, layout, params, p, ...) {
if ("hovertext" %in% names(data)) {
data$hovertext <- paste0(data$hovertext, "
")
}
@@ -268,7 +267,7 @@ to_basic.GeomDensity2d <- function(data, prestats_data, layout, params, ...) {
}
#' @export
-to_basic.GeomAbline <- function(data, prestats_data, layout, params, ...) {
+to_basic.GeomAbline <- function(data, prestats_data, layout, params, p, ...) {
# ugh, we can't trust the group here
data$group <- interaction(
data[!grepl("group", names(data)) & !vapply(data, anyNA, logical(1))]
@@ -280,7 +279,7 @@ to_basic.GeomAbline <- function(data, prestats_data, layout, params, ...) {
}
#' @export
-to_basic.GeomHline <- function(data, prestats_data, layout, params, ...) {
+to_basic.GeomHline <- function(data, prestats_data, layout, params, p, ...) {
# ugh, we can't trust the group here
data$group <- interaction(
data[!grepl("group", names(data)) & !vapply(data, anyNA, logical(1))]
@@ -292,7 +291,7 @@ to_basic.GeomHline <- function(data, prestats_data, layout, params, ...) {
}
#' @export
-to_basic.GeomVline <- function(data, prestats_data, layout, params, ...) {
+to_basic.GeomVline <- function(data, prestats_data, layout, params, p, ...) {
# ugh, we can't trust the group here
data$group <- interaction(
data[!grepl("group", names(data)) & !vapply(data, anyNA, logical(1))]
@@ -304,13 +303,13 @@ to_basic.GeomVline <- function(data, prestats_data, layout, params, ...) {
}
#' @export
-to_basic.GeomJitter <- function(data, prestats_data, layout, params, ...) {
+to_basic.GeomJitter <- function(data, prestats_data, layout, params, p, ...) {
prefix_class(data, "GeomPoint")
}
#' @export
-to_basic.GeomErrorbar <- function(data, prestats_data, layout, params, ...) {
+to_basic.GeomErrorbar <- function(data, prestats_data, layout, params, p, ...) {
# width for ggplot2 means size of the entire bar, on the data scale
# (plotly.js wants half, in pixels)
data <- merge(data, layout, by = "PANEL", sort = FALSE)
@@ -320,7 +319,7 @@ to_basic.GeomErrorbar <- function(data, prestats_data, layout, params, ...) {
}
#' @export
-to_basic.GeomErrorbarh <- function(data, prestats_data, layout, params, ...) {
+to_basic.GeomErrorbarh <- function(data, prestats_data, layout, params, p, ...) {
# height for ggplot2 means size of the entire bar, on the data scale
# (plotly.js wants half, in pixels)
data <- merge(data, layout, by = "PANEL", sort = FALSE)
@@ -330,13 +329,13 @@ to_basic.GeomErrorbarh <- function(data, prestats_data, layout, params, ...) {
}
#' @export
-to_basic.GeomLinerange <- function(data, prestats_data, layout, params, ...) {
+to_basic.GeomLinerange <- function(data, prestats_data, layout, params, p, ...) {
data$width <- 0
prefix_class(data, "GeomErrorbar")
}
#' @export
-to_basic.GeomPointrange <- function(data, prestats_data, layout, params, ...) {
+to_basic.GeomPointrange <- function(data, prestats_data, layout, params, p, ...) {
data$width <- 0
list(
prefix_class(data, "GeomErrorbar"),
@@ -345,7 +344,7 @@ to_basic.GeomPointrange <- function(data, prestats_data, layout, params, ...) {
}
#' @export
-to_basic.default <- function(data, prestats_data, layout, params, ...) {
+to_basic.default <- function(data, prestats_data, layout, params, p, ...) {
data
}
@@ -358,18 +357,19 @@ to_basic.default <- function(data, prestats_data, layout, params, ...) {
#'
#' @param data the data returned by \code{plotly::to_basic}.
#' @param params parameters for the geom, statistic, and 'constant' aesthetics
+#' @param p a ggplot2 object (the conversion may depend on scales, for instance).
#' @export
-geom2trace <- function(data, params) {
+geom2trace <- function(data, params, p) {
UseMethod("geom2trace")
}
#' @export
-geom2trace.GeomBlank <- function(data, params) {
+geom2trace.GeomBlank <- function(data, params, p) {
list()
}
#' @export
-geom2trace.GeomPath <- function(data, params) {
+geom2trace.GeomPath <- function(data, params, p) {
data <- group2NA(data)
L <- list(
x = data$x,
@@ -393,7 +393,7 @@ geom2trace.GeomPath <- function(data, params) {
}
#' @export
-geom2trace.GeomPoint <- function(data, params) {
+geom2trace.GeomPoint <- function(data, params, p) {
shape <- aes2plotly(data, params, "shape")
L <- list(
x = data$x,
@@ -424,7 +424,7 @@ geom2trace.GeomPoint <- function(data, params) {
}
#' @export
-geom2trace.GeomBar <- function(data, params) {
+geom2trace.GeomBar <- function(data, params, p) {
data$y <- data$ymax - data$ymin
# TODO: use xmin/xmax once plotly.js allows explicit bar widths
# https://github.com/plotly/plotly.js/issues/80
@@ -448,7 +448,7 @@ geom2trace.GeomBar <- function(data, params) {
}
#' @export
-geom2trace.GeomPolygon <- function(data, params) {
+geom2trace.GeomPolygon <- function(data, params, p) {
data <- group2NA(data)
L <- list(
x = data$x,
@@ -476,7 +476,7 @@ geom2trace.GeomPolygon <- function(data, params) {
}
#' @export
-geom2trace.GeomBoxplot <- function(data, params) {
+geom2trace.GeomBoxplot <- function(data, params, p) {
list(
x = data$x,
y = data$y,
@@ -506,7 +506,7 @@ geom2trace.GeomBoxplot <- function(data, params) {
#' @export
-geom2trace.GeomText <- function(data, params) {
+geom2trace.GeomText <- function(data, params, p) {
list(
x = data$x,
y = data$y,
@@ -525,26 +525,27 @@ geom2trace.GeomText <- function(data, params) {
}
#' @export
-geom2trace.GeomTile <- function(data, params) {
- # make sure order of value make sense before throwing z in matrix
- data <- data[order(order(data$x), data$y), ]
+geom2trace.GeomTile <- function(data, params, p) {
x <- sort(unique(data$x))
y <- sort(unique(data$y))
- fill <- scales::rescale(data$fill_plotlyDomain)
- txt <- data$hovertext
- # create the colorscale, which should ignore NAs
- data <- data[!is.na(fill), ]
- o <- data[order(data$fill_plotlyDomain), "fill"]
- n <- length(o)
- qs <- seq(0, 1, length.out = min(n, 100))
- idx <- o[pmax(1, round(n * qs))]
- colorscale <- cbind(qs, idx)
+ # make sure we're dealing with a complete grid
+ g <- expand.grid(x = x, y = y)
+ g$order <- seq_len(nrow(g))
+ g <- merge(g, data, by = c("x", "y"), all.x = TRUE)
+ g <- g[order(g$order), ]
+ # put fill domain on 0-1 scale for colorscale purposes
+ g$fill_plotlyDomain <- scales::rescale(g$fill_plotlyDomain)
+ # create the colorscale
+ colScale <- unique(g[, c("fill_plotlyDomain", "fill")])
+ # colorscale goes crazy if there are NAs
+ colScale <- colScale[complete.cases(colScale), ]
+ colScale <- colScale[order(colScale$fill_plotlyDomain), ]
list(
x = x,
y = y,
- z = matrix(fill, nrow = length(y), ncol = length(x)),
- text = matrix(txt, nrow = length(y), ncol = length(x)),
- colorscale = colorscale,
+ z = matrix(g$fill_plotlyDomain, nrow = length(y), ncol = length(x), byrow = TRUE),
+ text = matrix(g$hovertext, nrow = length(y), ncol = length(x), byrow = TRUE),
+ colorscale = setNames(colScale, NULL),
type = "heatmap",
showscale = FALSE,
autocolorscale = FALSE
@@ -552,17 +553,17 @@ geom2trace.GeomTile <- function(data, params) {
}
#' @export
-geom2trace.GeomErrorbar <- function(data, params) {
+geom2trace.GeomErrorbar <- function(data, params, p) {
make_error(data, params, "y")
}
#' @export
-geom2trace.GeomErrorbarh <- function(data, params) {
+geom2trace.GeomErrorbarh <- function(data, params, p) {
make_error(data, params, "x")
}
#' @export
-geom2trace.default <- function(data, params) {
+geom2trace.default <- function(data, params, p) {
warning(
"geom_", class(data)[1], "() has yet to be implemented in plotly.\n",
" If you'd like to see this geom implemented,\n",
diff --git a/man/geom2trace.Rd b/man/geom2trace.Rd
index aa67d9f22e..9788792cc1 100644
--- a/man/geom2trace.Rd
+++ b/man/geom2trace.Rd
@@ -4,12 +4,14 @@
\alias{geom2trace}
\title{Convert a "basic" geoms to a plotly.js trace.}
\usage{
-geom2trace(data, params)
+geom2trace(data, params, p)
}
\arguments{
\item{data}{the data returned by \code{plotly::to_basic}.}
\item{params}{parameters for the geom, statistic, and 'constant' aesthetics}
+
+\item{p}{a ggplot2 object (the conversion may depend on scales, for instance).}
}
\description{
This function makes it possible to convert ggplot2 geoms that
diff --git a/man/to_basic.Rd b/man/to_basic.Rd
index 7442826d84..494b83ac83 100644
--- a/man/to_basic.Rd
+++ b/man/to_basic.Rd
@@ -4,7 +4,7 @@
\alias{to_basic}
\title{Convert a geom to a "basic" geom.}
\usage{
-to_basic(data, prestats_data, layout, params, ...)
+to_basic(data, prestats_data, layout, params, p, ...)
}
\arguments{
\item{data}{the data returned by \code{ggplot2::ggplot_build()}.}
@@ -15,6 +15,8 @@ to_basic(data, prestats_data, layout, params, ...)
\item{params}{parameters for the geom, statistic, and 'constant' aesthetics}
+\item{p}{a ggplot2 object (the conversion may depend on scales, for instance).}
+
\item{...}{currently ignored}
}
\description{
diff --git a/tests/testthat/test-ggplot-heatmap.R b/tests/testthat/test-ggplot-heatmap.R
index 34434632a8..8833f32ff4 100644
--- a/tests/testthat/test-ggplot-heatmap.R
+++ b/tests/testthat/test-ggplot-heatmap.R
@@ -25,7 +25,7 @@ test_that("geom_tile is translated to type=heatmap", {
L$data[[1]]$hoverinfo == "text"
)
expect_true(
- all(grepl("^value: [-]?[0-9]+$", c(L$data[[1]]$text)))
+ all(grepl("value: [-]?[0-9]+$", c(L$data[[1]]$text)))
)
})
@@ -42,4 +42,21 @@ test_that("geom_tile() scale_fill_gradient2()", {
L <- save_outputs(p, "heatmap-midpoint")
# one trace is for the colorbar
expect_equal(length(L$data), 2)
+ expect_equal(L$data[[1]]$type, "heatmap")
})
+
+tidy_cor <- function(x) {
+ co <- as.data.frame(cor(x))
+ co$var1 <- row.names(co)
+ tidyr::gather(co, var2, cor, -var1)
+}
+d <- tidy_cor(mtcars)
+p <- ggplot(d, aes(var1, var2, fill = cor)) + geom_tile()
+
+test_that("geom_tile() with discrete x/y", {
+ L <- save_outputs(p, "heatmap-discrete")
+ # one trace is for the colorbar
+ expect_equal(length(L$data), 2)
+ expect_equal(L$data[[1]]$type, "heatmap")
+})
+
diff --git a/tests/testthat/test-ggplot-tooltip.R b/tests/testthat/test-ggplot-tooltip.R
index 7312ead818..13ef4ddbd2 100644
--- a/tests/testthat/test-ggplot-tooltip.R
+++ b/tests/testthat/test-ggplot-tooltip.R
@@ -30,7 +30,7 @@ test_that("dates are displayed in tooltip properly", {
test_that("tooltip argument respects ordering", {
p <- qplot(mpg, fill = factor(cyl), data = mtcars, geom = "density")
- p <- ggplotly(p, tooltip = c("y", "x"))
+ p <- ggplotly(p, tooltip = c("density", "x"))
info <- plotly_build(p)
txt <- strsplit(info$data[[1]]$text, "
")
expect_true(all(grepl("^density", sapply(txt, "[[", 1))))
@@ -43,3 +43,17 @@ test_that("can hide x values in tooltip", {
l <- plotly_build(p)
expect_equal(sum(grepl("cyl", l$data[[1]]$text)), 0)
})
+
+cars <- ggplot(mtcars, aes(mpg, factor(cyl)))
+p <- cars + stat_bin2d(aes(fill = ..density..), binwidth = c(3,1))
+
+test_that("geom_tile() displays correct info in tooltip with discrete y", {
+ L <- save_outputs(p, "heatmap-discrete-tooltip")
+ expect_equal(length(L$data), 2)
+ expect_equal(L$data[[1]]$type, "heatmap")
+ txt <- c(L$data[[1]]$text)
+ txt <- txt[!is.na(txt)]
+ # tooltip should show y-values on the _data_ scale
+ expect_true(all(grepl("factor\\(cyl\\): [4,6,8]", txt)))
+})
+