This blog has relocated to https://coolbutuseless.github.ioand associated packages are now hosted at https://github.com/coolbutuseless.

29 April 2018

mikefc

Problem

  1. I want to add pipes to ALL THE THINGS.
  2. ggplot2 is a thing
  3. ggplot2 doesn’t use pipes
  4. Add pipe support to ggplot2
  5. Write a blog post.
  6. ???
  7. Profit

Note: for keen followers, this is a rehash of my hack that was the pipify package.

ggplot without pipes. :(

Using the + character is soooo early 2010’s.

ggplot(mtcars) +
  geom_point(aes(mpg, cyl))

Create add_geom_point as a pipe-able geom_point :)

Let’s write a wrapper around geom_point so that I can use it in a pipe

#-----------------------------------------------------------------------------
#' A wrapper around 'geom_point' that is pipe-aware
#'
#' @param lhs the left-hand side of the addition e.g. "ggplot(mtcars)"
#' @param ... the arguments to be passed to the 'geom_point' function
#-----------------------------------------------------------------------------
add_geom_point <- function(lhs, ...) {
  `+`(lhs, geom_point(...))
}
ggplot(mtcars) %>%
  add_geom_point(aes(mpg, cyl))

Create pipe-able versions of any ggplot2 function

Instead of writing all the wrappers for all the gglot functions manually, I wrote a function to create a pipe-aware function out of a plus-aware function.

#-----------------------------------------------------------------------------
#' Create a pipe-aware func from any plus-aware function 
#'
#' Assigns the new function in the global environemnt
#'
#' @param plus_aware_func_name character. e.g. "geom_point"
#'
#-----------------------------------------------------------------------------
create_pipe_aware_func <- function(plus_aware_func_name) {
  pipe_aware_func_name <- paste0("add_", plus_aware_func_name)
    
  assign(
    pipe_aware_func_name,
    function(lhs, ...) {
      func <- get(plus_aware_func_name)
      `+`(lhs, func(...))
    },
    envir = globalenv()
  )
}

Use this function to create add_geom_line() and use it.

create_pipe_aware_func("geom_line")  # This will create 'add_geom_line()' in the global environment

ggplot(mtcars) %>%
  add_geom_point(aes(mpg, cyl)) %>%
  add_geom_line(aes(mpg, cyl))

Create add_* as pipe-able versions of all ggplot functions :) :) :)

Now that I can generate a pipe-aware function for a single ggplot function, I’ll do the whole of the ggplot package:

  1. Find all ggplot2 function names of interest
  2. Create pipe-aware wrapper function for each one
ls('package:ggplot2') %>%
  purrr::keep(~is.function(get(.x))) %>%
  purrr::keep(~grepl('^(geom_|scale_|stat_|coord_|annot|xlim|ylim|theme_|facet_)', .x)) %>%
  purrr::walk(create_pipe_aware_func)
ggplot(mtcars) %>%
  add_geom_point(aes(mpg, cyl, colour=as.factor(am))) %>%
  add_facet_grid(~vs) %>%
  add_theme_bw()

Evil version

And now instead of adding the add_ prefix in front of the function names, just mask the original ggplot2 functions in the global environment and use the original function names with pipes.

Note: This will break in all sorts of wonderful ways!

#-----------------------------------------------------------------------------
#' Create a pipe-aware func from any plus-aware function 
#'
#' Creates the pipe-aware function of the same name in the global environment
#'
#' @param plus_aware_func_name character. e.g. "geom_point"
#'
#-----------------------------------------------------------------------------
create_pipe_aware_func_evil <- function(plus_aware_func_name) {
  assign(
    plus_aware_func_name,
    function(lhs, ...) {
      func <- get(plus_aware_func_name, envir = as.environment('package:ggplot2'))
      `+`(lhs, func(...))
    },
    pos = globalenv()
  )
}


ls('package:ggplot2') %>%
  purrr::keep(~is.function(get(.x))) %>%
  purrr::keep(~grepl('^(geom_|stat_|coord_|annot|xlim|ylim|theme_|facet_)', .x)) %>%
  purrr::walk(create_pipe_aware_func_evil)


ggplot(mtcars) %>%
  geom_point(aes(mpg, cyl, colour=as.factor(am))) %>%
  facet_grid(~vs) %>%
  theme_bw()

Conclusion

A solution to a problem that nobody really has.