Pass arbitrary number of variables into facet_grid() - r

I'm trying to develop a function that will create a plot from just a few arguments. I need it to be flexible, so that I can input an unknown number of variables to facet the graph by. I've used all the quoting/unquoting methods I can find: {{ }}, .data[], !!!, inject, and more. I think part of the problem is that facet_grid calls it's variables within vars(), which is not cooperating with these operators. I have also tried passing the variables that I want to facet by into the function using dots, and by a named argument that takes a vector.
It seems to me that the best option here, based on this article (https://adv-r.hadley.nz/quasiquotation.html) is the unquote-splice operator. It seems to me like one of these should work, but neither does:
make_graph <- function (factor.by){
ggplot(mpg, aes(cyl, cty)) +
geom_col() +
facet_grid(cols = vars(!!!factor.by))
}
make_graph(factor.by = c("manufacturer", "model"))
make_graph_2 <- function (...){
ggplot(mpg, aes(cyl, cty)) +
geom_col() +
facet_grid(cols = vars(...))
}
make_graph_2("manufactuer", "model")
I would really appreciate any help figuring out how to make this work! I'm pretty new to programming with R, and I've been stuck on this issue for a long time. Thank you so much!

Some options:
Ellipses:
make_graph <- function(...) {
ggplot(mpg, aes(cyl, cty)) +
geom_col() +
facet_grid(cols = vars(...))
}
make_graph(manufacturer, model)
Programmatic formula:
make_graph <- function(factor.by) {
ggplot(mpg, aes(cyl, cty)) +
geom_col() +
facet_grid(rows = as.formula(paste0(". ~ ", paste(factor.by, collapse = "+"))))
}
make_graph(factor.by = c("manufacturer", "model"))
Using ensyms (thank you #stefan!), for a variadic function:
make_graph <- function(...) {
ggplot(mpg, aes(cyl, cty)) +
geom_col() +
facet_grid(cols = vars(!!!rlang::ensyms(...)))
}
make_graph("manufacturer", "model")
Using syms for a single-argument function:
make_graph <- function(factor.by) {
ggplot(mpg, aes(cyl, cty)) +
geom_col() +
facet_grid(cols = vars(!!!rlang::syms(factor.by)))
}
make_graph(c("manufacturer", "model"))

Related

R ggplot - box plot in generalzied function

With ggplot I'm trying to make a custom function to plot boxplot for a single column in a dataframe such that it can be used with any dataframe
Specific Example
male = data.frame(male = c(127,44,28,83,0,6,78,6,5,213,73,20,214,28,11)) # data from
ggplot(data = male, aes(x = "", y = male)) + geom_boxplot() +
stat_summary(fun=mean, geom="point", shape=20, size=2, color="red", fill="red")
This gives the expected boxplot with the mean shown as a point.
Generalized function - here the operation done in the specific example is wrapped into a generalized function
boxPlotFn = function (df, colName) {
ggplot(data = df, aes_string(x = "", y = colName)) + geom_boxplot() +
stat_summary(fun=mean, geom="point", shape=20, size=2, color="red", fill="red")
}
And I call the function like below
boxPlotFn(male, "male")
However, this gives the error Error: No expression to parse - rlang::last_error() indicates that the error is happening at the call to ggplot. What am I not doing right here?
That's a bit tricky but easily solved. To make your function work with aes_string you have to quote the "double quotes" mapped on x using e.g. single quotes. Additionally it should probably be data = df inside your function:
library(ggplot2)
male = data.frame(male = c(127,44,28,83,0,6,78,6,5,213,73,20,214,28,11)) # data from
boxPlotFn = function (df, colName) {
ggplot(data = df, aes_string(x = '""', y = colName)) +
geom_boxplot() +
stat_summary(fun=mean, geom="point", shape=20, size=2, color="red", fill="red")
}
boxPlotFn(male, "male")

Using `facet_wrap` in a function with non-standard evaluation with vector valued arguments

I want to make a function that uses ggplot and facet_wrap and passes the function variables to facet by.
I can do the following using quoted arguments.
library(tidyverse)
my_grid_plot <- function(facet_by){
ggplot(mtcars, aes(wt, disp)) +
geom_point() +
facet_wrap(facet_by)
}
my_grid_plot(facet_by = 'am')
my_grid_plot(facet_by = 'vs')
my_grid_plot(facet_by = c('am', 'vs'))
However, I would like to do this using unquoted arguments, e.g.
# does not work yet; this is what I want
my_grid_plot(facet_by = am)
my_grid_plot(facet_by = vs)
my_grid_plot(facet_by = c(am, vs))
I know I can use vars with facet_wrap, e.g.
ggplot(mtcars, aes(wt, disp)) +
geom_point() +
facet_wrap(vars(am, vs))
And so I could do something like this
my_grid_plot2 <- function(facet_by){
ggplot(mtcars, aes(wt, disp)) +
geom_point() +
facet_wrap(vars({{facet_by}}))
}
# works fine
my_grid_plot2(am)
my_grid_plot2(vs)
But that won't work if facet_by is a vector.
# does not work; obscure error
my_grid_plot2(c(am, vs))
So is there a way to get what I want above, i.e. where facet_by takes a single variable name of vector of names?
I don't know if this is the right or best way, and it seems a bit hacky, but I think it does the job. Inspired by https://stackoverflow.com/a/57709169/1009979
my_grid_plot3 <- function(facet_by){
by_expr <- enexpr(facet_by)
if(length(by_expr) == 1) {
args_vars <- as.list(by_expr)
} else {
args_vars <- as.list(by_expr)[-1]
}
quoted_args_vars <- sapply(args_vars, quo_name)
ggplot(mtcars, aes(wt, disp)) +
geom_point() +
facet_wrap(quoted_args_vars)
}
# this works
my_grid_plot3(am)
my_grid_plot3(vs)
my_grid_plot3(c(am, vs))
One option is to let tidyselect do the heavy lifting:
my_grid_plot2 <- function(facet_by){
by <- rlang::enexpr(facet_by) %>%
tidyselect::eval_select(mtcars)
ggplot(mtcars, aes(wt, disp)) +
geom_point() +
facet_wrap(names(by))
}
## All of the following now work as expected
my_grid_plot2(am)
my_grid_plot2(vs)
my_grid_plot2(c(am, vs))
my_grid_plot2('am')
my_grid_plot2('vs')
my_grid_plot2(c('am', 'vs'))

Only show one variable label in facet_wrap strip text?

I am plotting multiple graphs using facet_wrap() from the ggplot2 package in R. When facetting by multiple variables, the result includes both labels in the strip text. How can I remove one?
In this toy example from the mpg dataset, how to keep the cyl labels only? Thanks
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_wrap(c("cyl", "drv"))
The biggest concern with this is as #aelwan mentioned several plots will have the same strip labels but are not the same. Ignoring this issue, I believe the best way to proceed is by creating a new cross variable between cyl and drv.
So if you just want one row for the strip labels you can for example:
ggplot(mpg %>% mutate(cyl_drv = paste0(cyl, '-', drv)), aes(displ, hwy)) +
geom_point() +
facet_wrap(~ cyl_drv)
You can then change the labels if you need as follows:
ggplot(mpg %>% mutate(cyl_drv = paste0(cyl, '-', drv)), aes(displ, hwy)) +
geom_point() +
facet_wrap(~ cyl_drv, labeller = as_labeller(c(`4-4`="4", `4-f`="4", `5-f`=5, `6-4`=6, `6-f`=6, `6-r`=6, `8-4`=8, `8-f`=8, `8-r`=8)))
Another (admittedly not great) way to change it is as follows (which I suspect there is a better way to do):
library(ggplot2)
library(ggExtra)
library(grid)
library(gtable)
gg <- ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_wrap(~ cyl * drv)
g1 <- ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_wrap(~ cyl)
gtab <- ggplotGrob(gg)
gtab$grobs[[47]] <- ggplotGrob(g1)$grobs[[23]]
gtab$grobs[[48]] <- ggplotGrob(g1)$grobs[[23]]
gtab$grobs[[49]] <- ggplotGrob(g1)$grobs[[23]]
gtab$grobs[[50]] <- ggplotGrob(g1)$grobs[[22]]
gtab$grobs[[51]] <- ggplotGrob(g1)$grobs[[22]]
gtab$grobs[[52]] <- ggplotGrob(g1)$grobs[[22]]
gtab$grobs[[53]] <- ggplotGrob(g1)$grobs[[24]]
gtab$grobs[[54]] <- ggplotGrob(g1)$grobs[[24]]
gtab$grobs[[55]] <- ggplotGrob(g1)$grobs[[25]]
grid.draw(gtab)

How do I change the number of decimal places on axis labels in ggplot2?

Specifically, this is in a facet_grid. Have googled extensively for similar questions but not clear on the syntax or where it goes. What I want is for every number on the y-axes to have two digits after the decimal, even if the trailing one is 0. Is this a parameter in scale_y_continuous or element_text or...?
row1 <- ggplot(sector_data[sector_data$sector %in% pages[[x]],], aes(date,price)) + geom_line() +
geom_hline(yintercept=0,size=0.3,color="gray50") +
facet_grid( ~ sector) +
scale_x_date( breaks='1 year', minor_breaks = '1 month') +
scale_y_continuous( labels = ???) +
theme(panel.grid.major.x = element_line(size=1.5),
axis.title.x=element_blank(),
axis.text.x=element_blank(),
axis.title.y=element_blank(),
axis.text.y=element_text(size=8),
axis.ticks=element_blank()
)
From the help for ?scale_y_continuous, the argument 'labels' can be a function:
labels One of:
NULL for no labels
waiver() for the default labels computed by the transformation object
A character vector giving labels (must be same length as breaks)
A function that takes the breaks as input and returns labels as output
We will use the last option, a function that takes breaks as an argument and returns a number with 2 decimal places.
#Our transformation function
scaleFUN <- function(x) sprintf("%.2f", x)
#Plot
library(ggplot2)
p <- ggplot(mpg, aes(displ, cty)) + geom_point()
p <- p + facet_grid(. ~ cyl)
p + scale_y_continuous(labels=scaleFUN)
The "scales" package has some nice functions for formatting the axes. One of these functions is number_format(). So you don't have to define your function first.
library(ggplot2)
# building on Pierre's answer
p <- ggplot(mpg, aes(displ, cty)) + geom_point()
p <- p + facet_grid(. ~ cyl)
# here comes the difference
p + scale_y_continuous(
labels = scales::number_format(accuracy = 0.01))
# the function offers some other nice possibilities, such as controlling your decimal
# mark, here ',' instead of '.'
p + scale_y_continuous(
labels = scales::number_format(accuracy = 0.01,
decimal.mark = ','))
The scales package has been updated, and number_format() has been retired. Use label_number(). This can also be applied to percentages and other continuous scales (ex: label_percent(); https://scales.r-lib.org/reference/label_percent.html).
#updating Rtists answer with latest syntax from scales
library(ggplot2); library(scales)
p <- ggplot(mpg, aes(displ, cty)) + geom_point()
p <- p + facet_grid(. ~ cyl)
# number_format() is retired; use label_number() instead
p + scale_y_continuous(
labels = label_number(accuracy = 0.01)
)
# for whole numbers use accuracy = 1
p + scale_y_continuous(
labels = label_number(accuracy = 1)
)
Several people have suggested the scales package, but you could just do pretty much the same with base R as well, here by using the format() function.
require(ggplot2)
ggplot(iris, aes(y = Sepal.Length, x = Sepal.Width)) +
geom_point() +
scale_y_continuous(labels = function(x) format(x, nsmall = 2)) +
facet_wrap(~Species)

Apply two transformations on one axis

I have found coord_trans, but I'd like to apply log10 and
reverse to my x-axis. I tried applying two transformation
ggplot(table) + aes(color=Vowel, x=F1, y=F2) + geom_point() + coord_trans(x="log10", y="log10") + coord_trans(x="reverse", y="reverse")
but only the first one was applied. So I tried linking them
ggplot(table) + aes(color=Vowel, x=F2, y=F1) + geom_point() + coord_trans(x=c("log10", "reverse"), y=c("log10", "reverse"))
Which gives me a plain error.
'c("log10_trans", "reverse_trans")' is not a function, character or symbol
How do I chain them?
You can define new transformations using trans_new.
library(scales)
log10_rev_trans <- trans_new(
"log10_rev",
function(x) log10(rev(x)),
function(x) rev(10 ^ (x)),
log_breaks(10),
domain = c(1e-100, Inf)
)
p <- ggplot(mtcars, aes(wt, mpg)) +
geom_point()
p + coord_trans(y = log10_rev_trans)
A quick and easy way is to apply one of the transformations directly to the data and use the other with the plot function.
e.g.
ggplot(iris, aes(log10(Sepal.Length), log10(Sepal.Width), colour = Species)) +
geom_point() + coord_trans(x="reverse", y="reverse")
Note: the reverse transformation does not work with the iris data but you get the idea.
I wandered in here looking for a 'composition of scales' function. I think one might be able to write such a thing as follows:
# compose transforms a and b, applying b first, then a:
`%::%` <- function(atrans,btrans) {
mytran <- scales::trans_new(name = paste(btrans$name,'then',atrans$name),
transform = function(x) { atrans$transform(btrans$transform(x)) },
inverse = function(y) { btrans$inverse(atrans$inverse(y)) },
domain = btrans$domain, # this could use improvement...
breaks = btrans$breaks, # not clear how this should work, tbh
format = btrans$format)
}
ph <- ggplot(mtcars, aes(wt, mpg)) +
geom_point() +
scale_y_continuous(trans=scales::reverse_trans() %::% scales::log10_trans())
print(ph)

Resources