8

I'm working on a custom ggplot2 theme and was thinking it could be nifty to automatically modify elements of the theme depending on certain characteristics of the the plot object. For instance, is there a way to specify that if the plot contains facets, add a border to each panel?

I guess the question is really, can I access the current gg object from within a custom theme() call and then conditionally apply certain theme elements? In my head I would define my theme function to be something like this:

theme_custom <- function() {
  if (plot$facet$params > 0) {
  theme_minimal() +
    theme(panel.border = element_rect(color = "gray 50", fill = NA))
  }
  else {
    theme_minimal()
    }
}

If this is possible, it would look like this in use:

library(ggplot2)

# plot with facets automatically adds panel borders
ggplot(mtcars, aes(mpg, wt)) +
  geom_point() +
  facet_wrap(vars(cyl)) +
  theme_custom()

# plot without facets no panel border
ggplot(mtcars, aes(mpg, wt)) +
  geom_point() +
  theme_custom() 

NOTE: This was originally posted on RStudio Community and did not receive an answer.

duckmayr
  • 16,303
  • 3
  • 35
  • 53
mfherman
  • 392
  • 4
  • 16

3 Answers3

8

I think Oliver was thinking in the correct direction.

I don't think the theme_custom function is the correct place to check the plot for conditional theming, because theme functions are mostly agnostic about the precise plot that they are added to.

Instead, I think the appropriate place to check the plot is when the theme is added to the plot. You could write a theme function like the following, which sets a different class to the output.

theme_custom <- function() {
  out <- theme_minimal()
  class(out) <- c("conditional_theme", class(out))
  out
}

Now everytime a theme is added to a plot, this is done through the ggplot_add.theme function, which we can rewrite for the conditional_theme class. In my opinion, the correct way to check if a plot is facetted, is to check the class of the plot$facet slot, which can be FacetGrid, FacetWrap etc when a proper facet is added and defaults to FacetNull when no facet is set.

ggplot_add.conditional_theme <- function(object, plot, object_name) {
  if (!inherits(plot$facet, "FacetNull")) {
    object <- object + theme(panel.border = element_rect(colour = "grey50", fill = NA))
  }
  plot$theme <- ggplot2:::add_theme(plot$theme, object, object_name)
  plot
}

And now the use cases should work as intended:

ggplot(mtcars, aes(mpg, wt)) +
  geom_point() +
  facet_wrap(vars(cyl)) +
  theme_custom()

enter image description here

ggplot(mtcars, aes(mpg, wt)) +
  geom_point() +
  theme_custom() 

enter image description here

The only downside is that you would literally have to add the theme to the plot every time and you can't use the theme_set(theme_custom()) to have this apply to any plot in the session.

Martin Schmelzer
  • 23,283
  • 6
  • 73
  • 98
teunbrand
  • 33,645
  • 4
  • 37
  • 63
  • This is exactly what I was looking for, thank you! I thought it was going to be something like modifying `ggplot_add`, but couldn't put it all together like this. – mfherman Sep 30 '19 at 17:05
0

This requires a bit more knowledge than my current level of expertise in ggproto and ggproto_method objects. So this is not a complete answer, but a possible direction.

If you can gain access to the plot ggproto object, this object contains a ggproto_method in stored in the ggproto$facet$compute_layout. Depending on whether the plot contains a call to geom_facet, this will have a varying function length, as illustrated below

data(mtcars)
library(ggplot2)
p <- ggplot(mtcars, mapping = aes(x = hp, y = mpg)) + 
    geom_point()
pfacet <- p + facet_wrap(.~cyl)

nchar(format(p$facet$compute_layout))
[1] 139
nchar(format(pfacet$facet$compute_layout))
[1] 1107

(Note that 139 seems to be standard for any ggproto not containing a facet)

This assumes you can gain access to the proto object every time the plot is called or that you place your method as the a call after facet_wrap or similar methods are called, and is indeed just a hacky method due to my lack of knowledge of the intricates of gg, ggproto and ggproto_method objects.

Oliver
  • 8,169
  • 3
  • 15
  • 37
0

From a related post about conditionally adding ggplot elements it transpires one can add elements using {if(cond)expr}+ formatting, i.e. put the whole element in {} then follow with the +. One can combine this with theme element replacement formatting, e.g.

theme_minimal() %+replace% theme(axis.title.y.right = element_text(angle = 90)) +

To give:

{if(cond) theme() %+replace% theme(element = value)} +

So, shamelessly stealing from (/standing on the gigantic shoulders of) @teunbrand 's answer:

{if (!inherits(plot$facet, "FacetNull")) theme() %+replace% theme(panel.border = element_rect(colour = "grey50", fill = NA))} +

This works for my code but I'm not 100% sure about your example, apologies for not testing, in the middle of a huge function edit, but wanted to share this approach for its general applicability. One nice thing about this approach is that it's easy to chain element edits within the same condition, and have different conditions in their own {if}.

dez93_2000
  • 1,730
  • 2
  • 23
  • 34