问题
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.
回答1:
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.
回答2:
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?
回答3:
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
.
回答4:
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")
来源:https://stackoverflow.com/questions/43974780/how-to-detect-if-bare-variable-or-string