Skip to content

geom_tile() behaves differently from geom_rect with transformed x / y scales #5257

@linzi-sg

Description

@linzi-sg

I noticed this while answering a question on SO recently.

geom_rect() uses the locations of the four corners (xmin, xmax, ymin and ymax), while geom_tile() uses the center of the tile and its size (x, y, width, height). With appropriate calculations in aesthetic mappings, the two geoms can be used to produce identical results when x-axis & y-axis are both linear.

When non-linear transformations are applied to x or y scales, on the other hand, the two behave differently. Based on what I see from debugging in ggplot_build, scale transformations are performed on the inputted dataset much earlier than the geom's setup_data, so GeomTile$setup_data, which aims to convert (x, y, width, height) to (xmin, xmax, ymin and ymax), performs its transformation on a dataset that has already gone through scale transformation, with results differing from those of GeomRect$setup_data.

Here is the code to reproduce the bug for geom_tile:

library(ggplot2)

df <- data.frame(
  x = rep(c(3, 6, 8, 10, 13), 2),
  y = rep(c(1, 2), each = 5),
  z = factor(rep(1:5, each = 2)),
  w = rep(diff(c(0, 4, 6, 8, 10, 14)), 2)
)

p1 <- ggplot(df, aes(fill = z)) +
  geom_tile(aes(x = x, y = y, width = w, height = 1),
            colour = "grey50", alpha = 0.5, linewidth = 1) +
  geom_rect(aes(xmin = x - w/2, xmax = x + w/2,
                ymin = y - 0.5, ymax = y + 0.5), 
            colour = "white", alpha = 0.5, linewidth = 1) +            # white lines overlap grey when scales are linear
  ggtitle("linear scales") +
  theme_void() +
  theme(legend.position = "none")

p2 <- p1 + scale_x_log10() + ggtitle("transformed x scale")
p3 <- p1 + scale_y_log10() + ggtitle("transformed y scale")
p4 <- p1 + scale_x_log10() + scale_y_log10() + ggtitle("transformed both scales")

library(patchwork)
(p1 | p2) / (p3 | p4)

geom_tile plots patched together for comparison

The same behaviour can be observed with geom_raster(), though in that case we'd at least get a warning that raster pixels are placed at uneven intervals and will be shifted.

set.seed(1)
df1 <- expand.grid(x = 1:5, y = 1:5)
df1$z <- runif(nrow(df1))
ggplot(df1, aes(x, y, fill = z)) +
  geom_raster(alpha = 0.5) +
  geom_rect(aes(xmin = x - 0.5, xmax = x + 0.5,
                ymin = y - 0.5, ymax = y + 0.5),
            colour = "white", alpha = 0.5, linewidth = 1) +
  theme_void() +
  scale_x_log10() +
  scale_y_log10()

geom_raster version

It may not be worth changing the underlying code (I for one can't imagine many scenarios where one would want to perform scale transformations while plotting tiles), but having a note in the help page cautioning people about possibly unintended consequences from scale transformations could be helpful.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions