3

I am building a custom geom for ggplot2. See code below. However, I am trying to implement scale_y_reverse() by default. Where and how in the code should I add that?. I couldn't find any info about this.

geomName <- ggplot2::ggproto("geomName", ggplot2::Geom,
                            
              required_aes = c("x", "y"),
              default_aes = ggplot2::aes(colour = "black", fill = "orange", alpha = 1, linetype = 1),
              draw_key = ggplot2::draw_key_polygon,
              
              draw_group = function(data, panel_scales, coord) {
                coords <- coord$transform(data, panel_scales)
                grid::polygonGrob(
                coords$x, coords$y,
                gp = grid::gpar(col = coords$colour, group = coords$Id, fill = coords$fill, lty = coords$linetype)
                )
              }
)

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

Thank you for the help

Gregor Thomas
  • 136,190
  • 20
  • 167
  • 294
Jdv
  • 329
  • 1
  • 10
  • 2
    My guess is this is not possible within a geom - and also not quite what a geom is for... I guess you can maybe rather create a wrapper around the custom geom you are making and adding the scale_reverse function. – tjebo Jan 06 '21 at 14:44
  • 2
    I agreed with tjebo. I'm not very familiar with the `ggplot` internals, but I can't think of any geoms that change a scale by themselves. Sounds messy. – Gregor Thomas Jan 06 '21 at 14:46

2 Answers2

3

Well, it's in the name 'ggplot' that it is based on the grammar of graphics, which in turn theorizes that geoms, scales, facets, themes etc. should be fully separated. This makes it very difficult to set the scales from the context of the geom.

Normally, scales are chosen based on the scale_type.my_class() S3 method, but this happens before the geoms actually sees the data first in the ggproto's setup_data() method. This prevents the geom ggproto from quickly re-classing the y to a dummy class to provoke a scale_y_reverse(), which I tried out.

That said, we can just take note of how geom_sf() is handled, and automatically add a scale_y_reverse() whenever we use the geom (like how geom_sf() adds the coord_sf()). By wrapping both the geom-part and the scale-part in a list, these get added to the plot sequentially. The only downside I can think of, is that the user gets a warning whenever it overrides the scale.

library(ggplot2)

geomName <- ggplot2::ggproto(
  "geomName", ggplot2::Geom,
  
  required_aes = c("x", "y"),
  default_aes = ggplot2::aes(colour = "black", fill = "orange", alpha = 1, linetype = 1),
  draw_key = ggplot2::draw_key_polygon,
  
  draw_group = function(data, panel_scales, coord) {
    coords <- coord$transform(data, panel_scales)
    grid::polygonGrob(
      coords$x, coords$y,
      gp = grid::gpar(col = coords$colour, group = coords$Id, fill = coords$fill, lty = coords$linetype)
    )
  }
)

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

df <- data.frame(x = rnorm(10), y = rnorm(10))

ggplot(df, aes(x, y)) +
  geom_Name()

Created on 2021-01-06 by the reprex package (v0.3.0)

EDIT for question in comments:

Yes you can kind of set a default group. The thing is, you can be sure that the default no-group interpretation will set group to -1, but you cannot be certain that the user didn't specify aes(..., group = -1). If you're willing to accept this, then you can add the following setup_data ggproto method to the geomName object:

geomName <- ggplot2::ggproto(
  "geomName", ggplot2::Geom,
  ...
  setup_data = function(data, params) {
    if (all(data$group == -1)) {
      data$group <- seq_len(nrow(data))
    }
    data
  },
  ...
)

And then instead of seq_len(nrow(data)) you put whatever you wish the default grouping to be.

teunbrand
  • 33,645
  • 4
  • 37
  • 63
  • that's cool. learned again. I am not sure if I shall delete my ignorant answer now :) – tjebo Jan 06 '21 at 15:45
  • 1
    I wouldn't delete it. There aren't too many ggplot internals / ggproto questions on SO as it is, so people looking to build their own geoms might learn something! – teunbrand Jan 06 '21 at 15:48
  • Thanks for all the help. Good for me to learn as I don't have a lot of experience in this area – Jdv Jan 06 '21 at 16:18
  • one more question - is there a way to set the group aesthetics by default? for example aes(x,y, group = Id) but the user only specifies aes(x,y) – Jdv Jan 06 '21 at 17:06
  • 1
    Yes but not perfectly. See the edit to my answer. – teunbrand Jan 06 '21 at 17:35
  • if I am grouping by a column called Id, what should I replace `seq_len(nrow(data))` with? I tried data$group <- data$Id but throws an error `Error in split.default(x = seq_len(nrow(x)), f = f, drop = drop, ...) : group length is 0 but data length > 0` – Jdv Jan 06 '21 at 22:01
  • I don't know if your `Id` column survived the initial data culling wherein aesthetics are evaluated. Though if you'd forgive my self-promotion, I've posted an answer [here](https://stackoverflow.com/a/62702645/11374827) that might help you debug the problem. – teunbrand Jan 06 '21 at 22:30
  • Thank you. I actually came across your answer a few times today! Unfortunately I can't seem to find the issue but will get there eventually. Thanks for your help – Jdv Jan 06 '21 at 22:49
2

Turning my comment to a tentative answer - I don't think this is possible within a geom, nor its intended purpose to change scales. After all, you are changing the way the values are represented on the axis. You can modify and "do stuff" with your data in order to create geometries that "interpret" your data, such as a polygon in your example. In order to reverse a scale with a geom, you would need to multiply it by -1, but this changes the underlying values.

I will compare the reverse the values option with the wrapper below.

Reverse values option (not good! changes values!) This is a very very stripped down version for a stat, please don't use it for building a new geom.

library(ggplot2)
StatName <- ggproto("StatName", Stat,
  compute_group = function(data, scales) {
    data$y <- -1 * data$y
    data
  }
)
stat_name <- function( geom = "polygon", position = "identity", ...) {
  layer(
    stat = StatName, geom = geom, position = position, params = list( ...)
  )
}

ggplot(mtcars, aes(mpg, disp)) +
  stat_name()

I guess you fare better with a simple wrapper:


revax_polygon <- function(data, x, y, ...) {
  ggplot(data, aes({{ x }}, {{ y }})) +
    geom_polygon(...) +
    scale_y_reverse()
}

revax_polygon(mtcars, mpg, disp)

Created on 2021-01-06 by the reprex package (v0.3.0)

tjebo
  • 21,977
  • 7
  • 58
  • 94