7

I am trying to write a plotting function where you can pass bare column names to select which columns are plotted. I would like also to be able to specify a string as the color.

I have found that I need to use shQuote if I want to pass a string to aes_string. Now my problem is to figure out if a bare name or a string was passed. How would I do this?

dat <- data.frame(
    time = factor(c("Lunch","Dinner"), levels=c("Lunch","Dinner")),
    total_bill = c(14.89, 17.23)
)



plot_it <- function(dat, x,y, fill){
    require(rlang)
    require(ggplot2)

    x <- enquo(x)
    y <- enquo(y)
    fill <- enquo(fill)

    xN <- quo_name(x)
    yN <- quo_name(y)
    fillN <- quo_name(fill)

ggplot(data=dat, aes_string(x=xN, y=yN, fill=fillN)) +
    geom_bar(stat="identity")

}

This works:

plot_it(dat, time, total_bill, time)

This does not:

plot_it(dat, time, total_bill, "grey")

Note that this requires the newest versions of rlang and ggplot2.

Jan Stanstrup
  • 1,152
  • 11
  • 28
  • The second one doesn't because you are quoting it and then using `enquo` on it. You can do that by removing the `enquo` and `quo_name` for the `fill` – akrun May 15 '17 at 08:36
  • Yeah I get why it is not working. What is eluding me is how to make it possible to pass a string like that while also being able to pass bare names. So I was thinking it should be possible to detect if what is passed is bare or not. – Jan Stanstrup May 15 '17 at 08:39
  • it seems to be a problem in the `aes_string` as you are passing a color and column name – akrun May 15 '17 at 09:34

4 Answers4

3

Based on @akrun's suggestion of how to detect which case we had (was removed) I found something that does what I asked for:

plot_it <- function(dat, x, y, fill) {

    lst <- as.list(match.call())

    if(is.character(lst$fill)){
        fillN <- shQuote(fill)
    } else{
        fillN <- quo_name(enquo(fill))
    }

    x <- enquo(x)
    y <- enquo(y)


    xN <- quo_name(x)
    yN <- quo_name(y)


    p <- ggplot(data=dat, aes_string(x=xN, y=yN, fill=fillN)) +
        geom_bar(stat="identity")

    return(p)
}

Turns out this doesn't actually do what I had in mind since it assigns the quoted value as a factor to assign colors by. Not the actual color.

I came up with this that seems to work but is not really elegant:

plot_it <- function(dat, x, y, fill) {

    lst <- as.list(match.call())

    if(!(type_of(lst$fill)=="symbol" | (type_of(lst$fill)=="string" & length(lst$fill)==1))) stop("Fill must either be a bare name or a vector of length 1.")

    x <- enquo(x)
    y <- enquo(y)

    xN <- quo_name(x)
    yN <- quo_name(y)


    if(is.character(lst$fill)){
        dat[,"fillN"] <- fill
        fillN <- fill

        p <- ggplot(data=dat, aes_string(x=xN, y=yN, fill = shQuote(fillN))) +
             scale_fill_manual(name="fill", values=setNames(fillN,fillN))
    } else{
        fillN <- quo_name(enquo(fill))

        p <- ggplot(data=dat, aes_string(x=xN, y=yN, fill = fillN))
    }



       p <- p + geom_bar(stat="identity")

    return(p)
}

Any idea to make this a bit more elegant?

Jan Stanstrup
  • 1,152
  • 11
  • 28
  • I was working on a similar kind of thing and dropped it in the middle as it was not elegant. – akrun May 15 '17 at 12:38
2

So the way I did it was to use do.call and a list of parameters to optionally pass a parameter to the geom_bar function.

plot_it <- function(dat, x, y, fill) {

  lst <- as.list(match.call())

  xN <- quo_name(enquo(x))
  yN <- quo_name(enquo(y))
  fillN <- quo_name(enquo(fill))

  # Build the geom_bar call using do.call and a list of parameters
  # If the fill parameter is a character then the parameter list contains
  # both stat = "identity" and colour = ...; this colour will override the 
  # colour aesthetic
  p <- ggplot(data=dat, aes_string(x=xN, y=yN, fill=fillN)) +
    do.call(geom_bar, c(list(stat = "identity"), list(fill = lst$fill)[is.character(lst$fill)]))

  return(p)
}

plot_it(dat, time, total_bill, time)
plot_it(dat, time, total_bill, "blue")
plot_it(dat, time, total_bill, 5)

I used "blue" for clarity as ggplot will default to grey anyway but it does work with any colour literal as far as I can tell. I think this is more elegant than using conditionals.

As a matter of interest, geom_col might be more appropriate in this context than geom_bar.

Eumenedies
  • 1,618
  • 9
  • 13
2

Working from your answer and Eumenedies' answer/test cases, here is a version that:

  • Solves your original problem
  • Prevents an unnecessary legend, as Eumenedies did
  • Provides ggplot object that can be added to without error
  • Uses the more appropriate geom_col, per Eumenedies' suggestion

The main trick is that, if fill isn't a factor in the plot, you want it outside of the aes / aes_string block.

plot_it <- function(dat, x, y, fill) {

  lst <- as.list(match.call())

  xN <- quo_name(enquo(x))
  yN <- quo_name(enquo(y))

  if(is.character(lst$fill)) {
    p <- ggplot(data=dat, aes_string(x=xN, y=yN)) +
      geom_col(fill = fill)
  } else {
    p <- ggplot(data=dat, aes_string(x=xN, y=yN, fill = quo_name(enquo(fill)))) +
      geom_col()
  }

  return(p)
}

plot_it(dat, time, total_bill, time)
plot_it(dat, time, total_bill, "blue")
plot_it(dat, time, total_bill, "blue") + geom_point()

You could make the if block shorter by moving the fill aesthetic in the second case to the geom_col call, but that will extend in a different way if you add more geoms.

Also, once ggplot is updated to support rlang, it would be cleaner to avoid aes_string and quo_name combination and just use !!fill.

Note that, assuming that the fill factor exists, if it's always going to be the same as the x factor, it would probably make more sense to have a version where fill is an optional argument. You would only overwrite the default per-factor color if the argument is included.

Nick Nimchuk
  • 386
  • 2
  • 11
  • I gave the bounty to Eumenedies as I think it was the most elegant as asked for. But I marked this answer as best for the reasons stated. Thanks to both of you! – Jan Stanstrup Aug 14 '17 at 08:52
  • I am surprised that moving the `fill` to the `ggplot` (but outside `aes_string`) is not equivalent. Could you explain this? If it was the `geom_col` could go outside the `if` statement. – Jan Stanstrup Aug 14 '17 at 08:53
  • @Jan, you mean like `ggplot(data = dat, aes_string(x=Xn, y=Yn), fill = fill) + geom_col()`? While ggplot takes a dots (`...`) argument, the help says it's not currently used, which means that it wouldn't be passed to subsequent `geom_*`s. I think the idea is that the base `ggplot` function only sets up (at most) the data source and how it is associated with the plot; specific visuals (such as setting specific colors) are left to other functions. – Nick Nimchuk Aug 14 '17 at 17:41
  • Ah OK. I understand. Thanks. – Jan Stanstrup Aug 15 '17 at 07:24
0

You're asking for one parameter to represent two types of arguments: one naming a column, the other naming a color. The simplest solution is to just split it into two parameters and add some checks to make sure only one is supplied.

plot_it <- function(dat, x, y, fill_column, fill_color = NULL){
  require(rlang)
  require(ggplot2)

  x <- enquo(x)
  y <- enquo(y)

  xN <- quo_name(x)
  yN <- quo_name(y)

  if (!missing(fill_column) && !is.null(fill_color)) {
    stop("Specify either fill_column or fill_color, not both")
  }
  if (missing(fill_column) && is.null(fill_color)) {
    stop("Specify one of fill_column or fill_color")
  }
  plot_geom <- if (!is.null(fill_color)) {
    geom_bar(stat = "identity", fill = fill_color)
  } else {
    fill <- enquo(fill_column)
    fillN <- quo_name(fill)
    geom_bar(stat = "identity", aes_string(fill = fillN))
  }

  ggplot(data = dat, aes_string(x = xN, y = yN)) +
    plot_geom
}


plot_it(dat, time, total_bill, fill_column = time)
plot_it(dat, time, total_bill, fill_color  = "grey")
Nathan Werth
  • 5,093
  • 18
  • 25