简体   繁体   中英

Writing a custom function for ggplot: How to avoid overriding `theme()` settings when adding external themes

I want avoid repeating code for plotting, so I wrote a simple function for ggplot . I specify several preferences using theme() . However, if I want to apply an external theme (eg, theme_bw() ), it overrides my specific theme() preferences. If I had been writing code not in a user defined function, the solution would've been simple: rearrange the structure of ggplot 's code so that theme_bw() comes before theme() . But I don't know how to address this in the case of a custom function.

Data

df <- 
  data.frame(
    x_names = c("asia", "europe", "america", "africa", "australia"),
    y_values = 1:5
  )

##     x_names y_values
## 1      asia        1
## 2    europe        2
## 3   america        3
## 4    africa        4
## 5 australia        5

Function for plotting

library(ggplot2)

plot_from_data <- function(data_input, x_col, y_col) {
  
  p_barplot <- 
    ggplot(data = data_input, aes(x = {{ x_col }}, y = {{ y_col }}, fill = as_factor({{ x_col}} ))) +
    geom_bar(stat = "identity") +
    labs(caption = "caption blah") +
    theme(plot.title = element_text(hjust = 0.5, size = 14),    ## I have a bunch
          axis.text.x=element_text(angle = -60, hjust = 0),     ## of preferences
          axis.title.x = element_blank(),                       ## I set up using
          legend.title = element_blank(),                       ## `theme()`
          panel.grid  = element_blank(),
          panel.grid.major=element_blank(),
          plot.caption = element_text(hjust = 0, size = 8),
          legend.position = "none")
  
  return(p_barplot)
  
}

Plotting using the function

p <- plot_from_data(data_input = df,
               x_col = x_names,
               y_col = y_values)

p

只是_p

But let's say I want to add a theme

p + theme_bw()

p_w_theme_bw

Yikes. This is undesired. I didn't want a legend and didn't want x axis title, etc. So one solution is to go back to plot_from_data 's code and add theme_bw() before theme() . But this is not a good solution because I want to be able to flexibly apply different themes on top of plot_from_data 's plot, without having to re-edit the function's code for each plot.

Another solution would be to keep both bw_theme() and theme() outside of plot_from_data . Then, for each plot I generate I'd add p + theme_bw() + theme(...) . But this is undesired as well because theme() is full of preferences I don't want to repeat each time.

How could I solve this? One idea I have, but don't know how to apply, is having a "theme placeholder" within plot_from_data , before theme() . Something like:

plot_from_data <- function(data_input, x_col, y_col) {
  
  p_barplot <- 
    ggplot(data = data_input, aes(x = {{ x_col }}, y = {{ y_col }}, fill = as_factor({{ x_col}} ))) +
    geom_bar(stat = "identity") +
    labs(caption = "caption blah") +
    ***theme_placeholder*** + ##################################  <----------------------- here
    theme(plot.title = element_text(hjust = 0.5, size = 14),
          axis.text.x=element_text(angle = -60, hjust = 0),
          axis.title.x = element_blank(),
          legend.title = element_blank(),
          panel.grid  = element_blank(),
          panel.grid.major=element_blank(),
          plot.caption = element_text(hjust = 0, size = 8),
          legend.position = "none")
  
  return(p_barplot)
  
}

However, I don't know how to make this flexible enough. One problem I can immediately think of is with ggthemes library that sometimes requires two components for a theme (eg, theme_economist() and scale_color_economist() ).

Any ideas?

You could add an optional theme as an argument to the function. If you need to add a color scale, you can tack this on after the function runs, since the scale function won't override any theme elements.

library(tidyverse)
library(ggthemes)

plot_from_data <- function(data_input, x_col, y_col, my_theme=NULL) {
  
  my_theme = list(my_theme)
  
  p_barplot <- ggplot(data = data_input, aes(x = {{ x_col }}, y = {{ y_col }}, fill = as_factor({{ x_col}} ))) +
    geom_col() +
    labs(caption = "caption blah") +
    my_theme +
    theme(plot.title = element_text(hjust = 0.5, size = 14),
          axis.text.x=element_text(angle = -60, hjust = 0),
          axis.title.x = element_blank(),
          legend.title = element_blank(),
          panel.grid  = element_blank(),
          panel.grid.major=element_blank(),
          plot.caption = element_text(hjust = 0, size = 8),
          legend.position = "none")
  
  return(p_barplot)
  
}

plot_from_data(iris, Species, Petal.Width)
plot_from_data(iris, Species, Petal.Width, theme_bw())
plot_from_data(iris, Species, Petal.Width, theme_economist()) + 
  scale_fill_economist()

在此处输入图片说明

You could even add the scale function in the my_theme argument if you enter the argument as a list:

plot_from_data(iris, Species, Petal.Width, 
               list(theme_economist(), scale_fill_economist())

Another option (maybe overkill) would be to allow the user to optionally add the name of a fill scale as a string. For example:

  plot_from_data <- function(data_input, x_col, y_col, my_theme=NULL, my_fill_scale=NULL) {

  
  my_theme = list(my_theme)
  
  p_barplot <- ggplot(data = data_input, aes(x = {{ x_col }}, y = {{ y_col }}, fill = as_factor({{ x_col}} ))) +
    geom_col() +
    labs(caption = "caption blah") +
    my_theme +
    theme(plot.title = element_text(hjust = 0.5, size = 14),
          axis.text.x=element_text(angle = -60, hjust = 0),
          axis.title.x = element_blank(),
          legend.title = element_blank(),
          panel.grid  = element_blank(),
          panel.grid.major=element_blank(),
          plot.caption = element_text(hjust = 0, size = 8),
          legend.position = "none")
  
  if(!is.null(my_fill_scale)) {
    p_barplot = p_barplot + 
      match.fun(paste0("scale_fill_", my_fill_scale))()
  }
  
  return(p_barplot)
  
}

plot_from_data(iris, Species, Petal.Width, theme_economist())
plot_from_data(iris, Species, Petal.Width, theme_economist(), "economist")
plot_from_data(iris, Species, Petal.Width, theme_economist(), "fivethirtyeight") 

在此处输入图片说明

Going even further, you could use the same trick to enter the theme as a string and also set up the function to automatically pick the corresponding scale, where appropriate:

plot_from_data <- function(data_input, x_col, y_col, my_theme=NULL, my_fill_scale=NULL) {
  
  p <- ggplot(data = data_input, aes(x ={{ x_col }}, y={{ y_col }}, fill=as_factor({{ x_col}} ))) +
    geom_bar(stat = "identity") +
    labs(caption = "caption blah")
  
  if(!is.null(my_theme)) {
    p = p + match.fun(paste0("theme_", my_theme))()
  }
  
  p = p + theme(plot.title = element_text(hjust = 0.5, size = 14),
                axis.text.x=element_text(angle = -60, hjust = 0),
                axis.title.x = element_blank(),
                legend.title = element_blank(),
                panel.grid  = element_blank(),
                panel.grid.major=element_blank(),
                plot.caption = element_text(hjust = 0, size = 8),
                legend.position = "none")
  
  if(!is.null(my_fill_scale)) {
    p = p + 
      match.fun(paste0("scale_fill_", my_fill_scale))()
  }
  
  if(!is.null(my_theme)) {
    if(my_theme %in% c("economist","fivethirtyeight","few") & 
       is.null(my_fill_scale)) {
      p = p + match.fun(paste0("scale_fill_", my_theme))()
    }
  }
  
  return(p)
}

plot_from_data(iris, Species, Petal.Width, "economist")
plot_from_data(iris, Species, Petal.Width, "economist", "viridis_d") 
plot_from_data(iris, Species, Petal.Width, "economist", "few") 
plot_from_data(iris, Species, Petal.Width, "fivethirtyeight") 

在此处输入图片说明

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM