rggplot2ggproto

How to align scale transformation across geoms?


I have a geom_foo() which will do some transformation of the input data and I have a scale transformation. My problem is that these work not as I would expect together with other geom_*s in terms of scaling.

To illustrate the behavior, consider foo() which will be used in the setup_data method of GeomFoo, defined at the end of the question.

foo <- function(x, y) {
  data.frame(
    x = x + 2,
    y = y + 2
  )
}
foo(1, 1)

The transformer is:

foo_trans <- scales::trans_new(
  name = "foo",
  transform = function(x) x / 5,
  inverse = function(x) x * 5
  )

Given this input data:

df1 <- data.frame(x = c(1, 2), y = c(1, 2))

Here is a basic plot:

library(ggplot2)
ggplot(df1, aes(x = x, y = y)) +
  geom_foo()

enter image description here

When I apply the transformation to the vertical scale, I get this

ggplot(df1, aes(x = x, y = y)) +
  geom_foo() +
  scale_y_continuous(trans = foo_trans)

enter image description here

What I can say is that the y-axis limits are calculate as 11 = 1 + (2*5) and 12 = 2 + (2*5), where 1 and 2 are df1$y, and (2 * 5) are taken from the setup_data method and from trans_foo.

My real problem is, that I would like add a text layer with labels. These labels and their coordinates come from another dataframe, as below.

df_label <- foo(df1$x, df1$y)
df_label$label <- c("A", "B")

Label and point layers are on same x-y positions without the scale transformation

p <- ggplot(df1, aes(x = x, y = y)) +
  geom_foo(color = "red", size = 6) +
  geom_text(data = df_label, aes(x, y, label = label)) 
p

enter image description here

But when I apply the transformation, the coordinates do not match anymore

p +
  scale_y_continuous(trans = foo_trans)

enter image description here

How do I get the to layer to match in x-y coordinates after the transformation? Thanks


ggproto object:

GeomFoo <- ggproto("GeomFoo", GeomPoint,
  setup_data = function(data, params) {
    cols_to_keep <- setdiff(names(data), c("x", "y"))
    cbind(
      foo(data$x, data$y), 
      data[, cols_to_keep]
    )
  }
)

geom constructor:

geom_foo <- function(mapping = NULL, data = NULL, ...,
                     na.rm = FALSE, show.legend = NA,
                     inherit.aes = TRUE) {
  layer(
    data = data,
    mapping = mapping,
    stat = "identity",
    geom = GeomFoo,
    position = "identity",
    show.legend = show.legend,
    inherit.aes = inherit.aes,
    params = list(
      na.rm = na.rm,
      ...
    )
  )
}

Solution

  • Doing data transformations isn't really the task of a geom, but a task of a stat instead. That said, the larger issue is that scale transformations are applied before the GeomFoo$setup_data() method is called. There are two ways one could accomplish this task that I could see.

    1. Apply foo() before scale transformation. I don't think geoms or stats ever have access to the data before scale transformation. A possible place for this is in the ggplot2:::Layer$setup_layer() method. However, this isn't exported, which probably means the devs would like to discourage this even before we make an attempt.

    2. Inverse the scale transformation, apply foo(), and transform again. For this, you need a method with access to the scales. AFAIK, no geom method has this access. However Stat$compute_panel() does have access, so we can use this.

    To give an example of (2), I think you could get away with the following:

    StatFoo <- ggproto(
      "StatFoo", Stat,
      compute_panel = function(self, data, scales) {
        cols_to_keep <- setdiff(names(data), c("x", "y"))
        food <- foo(scales$x$trans$inverse(data$x), 
                    scales$y$trans$inverse(data$y))
        cbind(
          data.frame(x = scales$x$trans$transform(food$x),
                     y = scales$y$trans$transform(food$y)),
          data[, cols_to_keep]
        )
      }
    )
    
    geom_foo <- function(mapping = NULL, data = NULL, ...,
                         na.rm = FALSE, show.legend = NA,
                         inherit.aes = TRUE) {
      layer(
        data = data,
        mapping = mapping,
        stat = StatFoo,
        geom = GeomPoint,
        position = "identity",
        show.legend = show.legend,
        inherit.aes = inherit.aes,
        params = list(
          na.rm = na.rm,
          ...
        )
      )
    }
    

    If someone else has brighter ideas to do this, I'd also like to know!