mspices
mspices

Reputation: 459

One function per facet

I can plot a function in ggplot2, like so:

library(ggplot2)
ggplot(data.frame(x=0), aes(x)) + geom_function(fun = sin) + xlim(c(-5,5))

Can I use ggplot2's facetting to make the plot for multiple functions, one in each facet? (for example sin and cos)

Upvotes: 3

Views: 569

Answers (3)

tjebo
tjebo

Reputation: 23727

... fun is not an aesthetic ... you can make it one :)

It's a bit of an overkill, but just a quick demonstration what is possible by modifying the Geoms and Stats. The below is a quick hack and I've referenced with quite a lot of ugly :::, which you wouldn't do if you would properly package this. Also this is clearly not properly tested on many use cases. Few more comments in the code.

This was quite ... fun :)

df <- data.frame(x = 0, fun = c("sin", "cos", "tan", "mean"))

ggplot(df, aes(x)) +
  stat_function2(aes(fun = fun)) +
  xlim(c(-5,5)) +
  facet_wrap(~fun, scales = "free_y")

Modifying Geom and Stat - StatFunction2

StatFunction2 <- ggproto(NULL, StatFunction)
## removing fun from the arguments
StatFunction2$compute_group <- function (data, scales, xlim = NULL, n = 101, args = list()) 
{
  if (is.null(scales$x)) {
    ## need to change that here a bit
    range <- rlang::`%||%`(xlim, c(0, 1))
    xseq <- seq(range[1], range[2], length.out = n)
    x_trans <- xseq
  }
  else {
    ## same same
    range <- rlang::`%||%`(xlim, scales$x$dimension())
    xseq <- seq(range[1], range[2], length.out = n)
    if (scales$x$is_discrete()) {
      x_trans <- xseq
    }
    else {
      x_trans <- scales$x$trans$inverse(xseq)
    }
  }
  ## get the function, this is the trick :)
  fun <- unique(data$fun)
  if (plyr::is.formula(fun)) 
    fun <- as_function(fun)
  y_out <- do.call(fun, c(list(quote(x_trans)), args))
  if (!is.null(scales$y) && !scales$y$is_discrete()) {
    y_out <- scales$y$trans$transform(y_out)
  }
  ggplot2:::new_data_frame(list(x = xseq, y = y_out))
}
## update stat_function - remove fun argument and reference new geom_function2
stat_function2 <- function (mapping = NULL, data = NULL, geom = "function2", position = "identity", 
                            ..., fun, xlim = NULL, n = 101, args = list(), na.rm = FALSE, 
                            show.legend = NA, inherit.aes = TRUE) 
{
  if (is.null(data)) {
    ### those ::: are just for to make it work here
    data <- ggplot2:::ensure_nonempty_data
  }
  layer(data = data, mapping = mapping, stat = StatFunction2, 
        geom = geom, position = position, show.legend = show.legend, 
        ## fun needs to be removed here too.
        inherit.aes = inherit.aes, params = list(n = n, 
                                                 args = args, na.rm = na.rm, xlim = xlim, ...))
}
## This is the correct way to create copies (children) of ggproto objects
## see https://stackoverflow.com/a/70637511/7941188
GeomFunction2 <- ggproto(NULL, GeomFunction)
## change the required aesthetics - this removes the warning that aesthetics are not known
GeomFunction2$required_aes <- c("x", "y", "fun")
## update the corresponding geom (two locations in this function definition)
geom_function2 <- function (mapping = NULL, data = NULL, stat = "function2", position = "identity", 
                            ..., na.rm = FALSE, show.legend = NA, inherit.aes = TRUE) 
{
  if (is.null(data)) {
    data <- ensure_nonempty_data
  }
  layer(data = data, mapping = mapping, stat = stat, geom = GeomFunction2, 
        position = position, show.legend = show.legend, inherit.aes = inherit.aes, 
        params = list(na.rm = na.rm, ...))
}

Upvotes: 3

Jon Spring
Jon Spring

Reputation: 66415

Looks like you in fact can facet by the function if you feed each layer its own data with the faceting variable specified:

library(ggplot2) # using ggplot2 3.3.5
ggplot(data.frame(x=0), aes(x)) + 
  geom_function(fun = sin, data = data.frame(x = -5:5, fun_name = "sin")) +
  geom_function(fun = cos, data = data.frame(x = -5:5, fun_name = "cos")) +
  facet_wrap(~fun_name)
  

enter image description here

Upvotes: 7

r2evans
r2evans

Reputation: 160407

Because fun= is not an aesthetic, I think you cannot facet on it. However, you can faux-facet using the patchwork package.

library(ggplot2)
gsin <- ggplot(data.frame(x=0), aes(x)) +
  geom_function(fun = sin) +
  xlim(c(-5,5)) +
  labs(title = "sin()")
gtan <- ggplot(data.frame(x=0), aes(x)) +
  geom_function(fun = tan) +
  xlim(c(-5,5)) +
  labs(title = "tan()")
gsin + gtan

ggplot2 with faux-facets using patchwork

If you prefer the "look" of ggplot2's facets, you can choose this method instead:

gsin <- ggplot(data.frame(x=0, fun="sin"), aes(x)) +
  facet_wrap(~fun) +
  geom_function(fun = sin) +
  xlim(c(-5,5))
gtan <- ggplot(data.frame(x=0, fun="tan"), aes(x)) +
  facet_wrap(~fun) +
  geom_function(fun = tan) +
  xlim(c(-5,5))
gsin + gtan

ggplot2 faux-facets using real facet labels

All of this so far has the effect of facet_*(scales="free_y") (because we fixed xlim(.)). If you want to mimic faceting more closely, you need to control the limits of all facets:

ylims <- c(-1, 1)
gsin <- ggplot(data.frame(x=0, fun="sin"), aes(x)) +
  facet_wrap(~fun) +
  geom_function(fun = sin) +
  xlim(c(-5,5)) +
  scale_y_continuous(limits = ylims)
gtan <- ggplot(data.frame(x=0, fun="tan"), aes(x)) +
  facet_wrap(~fun) +
  geom_function(fun = tan) +
  xlim(c(-5,5)) +
  scale_y_continuous(name = NULL, guide = NULL, limits = ylims)
gsin + gtan
# Warning: Removed 22 row(s) containing missing values (geom_path).

ggplot2 faux-facet with y-limits under control

Technically you are not required here to set the y-limits on all, but ... unless you know with certainty that the limits on unconstrained y-axes will be what you need, it is possible that they could be slightly off. For instance, if you change the initial function (in a quick hack) to be 2*sin(x) but forget to update the remaining facets' y-limits, then your plots will be lying. It's best to set the limits in a single place (ylims <- ...) and reference in all plots.

Upvotes: 2

Related Questions