rggplot2aestheticsnon-standard-evaluation

combining predefined aesthetics (ggplot2)


I have a function that optionally accepts predefined aesthetics (for ggplot2). This can be in one of several forms, including

mapping <- aes(color = c(cyl, gear))
mapping
# Aesthetic mapping: 
# * `colour` -> `c(cyl, gear)`

or programmatically as

mapping <- aes(color = c(.data[["cyl"]], .data[["gear"]]))
mapping
# Aesthetic mapping: 
# * `colour` -> `c(.data[["cyl"]], .data[["gear"]])`

(I'm using color= here, but really I'm using group= and subgroup= in various ways. I'm simplifying to color= here for simplicity.)

My function cannot control if or how the caller defines mapping (it defaults to NULL), it can either be using symbols (first example) or one of the programmatic options like .data (second example), but internally after I adjust/augment the data, I need to add a variable to that aesthetic.

I can combine different aesthetics using c, as in

mapping <- aes(color = factor(cyl))
othermap1 <- aes(x = mpg, y = disp)
ggplot(mtcars, mapping = aes(!!!c(mapping, othermap1))) +
  geom_path()

(The plot itself doesn't matter much, just that it works.)

simple grouped ggplot lines

If I want to add a variable to the color= aesthetic, I don't know how to best do it.

othermap2 <- aes(color = gear)

I would like to be able to get the effect of color= combined, with an result similar to the manual:

ggplot(mtcars, mapping = aes(mpg, disp, color = interaction(cyl, gear))) +
  geom_path()

This is all programmatic, and I would prefer to allow the user to provide a "real" mapping= argument (e.g., mapping=aes(...)) in the call to my function.

I think one possible way is to require the user to provide a named-list of variables if they intend to assign color=, such as mapping=list(group="cyl") and internally I'll augment/merge and then create the .data[[x]] aesthetics internally ... but I really would prefer to continue to allow the user to use mapping=aes(..).


Solution

  • You can use some rlang magic to manipulate the mapping. Briefly, we just assign the new aesthetic if it was abent, otherwise we create a new call combining the two expressions.

    library(ggplot2)
    library(rlang)
    
    combine_mapping <- function(user, fixed, fun = interaction) {
      names(user)  <- standardise_aes_names(names(user))
      names(fixed) <- standardise_aes_names(names(fixed))
      fun <- enexpr(fun)
      for (var in names(fixed)) {
        inject <- quo_get_expr(fixed[[var]])
        if (var %in% names(user)) {
          expr <- quo_get_expr(user[[var]])
          user[[var]] <- call2(fun, !!!list(inject, expr))
        } else {
          user[[var]] <- fixed[[var]]
        }
      }
      user
    }
    

    Some demonstrations

    user_mapping <- aes(mpg, disp, colour = cyl)
    
    # Case of missing aesthetic
    combine_mapping(aes(mpg, disp), aes(colour = cyl))
    #> Aesthetic mapping: 
    #> * `x`      -> `mpg`
    #> * `y`      -> `disp`
    #> * `colour` -> `cyl`
    
    # Combining aesthetic
    combine_mapping(user_mapping, aes(colour = gear))
    #> Aesthetic mapping: 
    #> * `x`      -> `mpg`
    #> * `y`      -> `disp`
    #> * `colour` -> `interaction(gear, cyl)`
    
    # Multiple aesthetics
    combine_mapping(user_mapping, aes(colour = gear, x = cty))
    #> Aesthetic mapping: 
    #> * `x`      -> `interaction(cty, mpg)`
    #> * `y`      -> `disp`
    #> * `colour` -> `interaction(gear, cyl)`
    
    # Other function to combine
    combine_mapping(user_mapping, aes(colour = gear), fun = paste0)
    #> Aesthetic mapping: 
    #> * `x`      -> `mpg`
    #> * `y`      -> `disp`
    #> * `colour` -> `paste0(gear, cyl)`
    
    # Works in plot
    ggplot(mtcars, combine_mapping(aes(mpg, disp, colour = cyl), aes(colour = gear))) +
      geom_point()
    

    Created on 2023-12-22 with reprex v2.0.2