Set a document-persistent ggplot2 color theme - r

I would like to define a color palette for every plot built in a markdown document. In essence this would overwrite the default choices.
There are several very old answers -- thanks for the links here and here suggested by #dww -- that solve for old versions (specifically calling out a solution on 0.8.2 when the modern release several major releases ahead, currently at 3.2.x).
I'll illustrate the closest use case, setting themes. In the case of general purpose themes, this is trivial: rather than appending + theme_minimal() on every plot, I can instead set the theme which persists across all plots.
library(ggplot2)
d <- diamonds[sample(1:nrow(diamonds), 1000), ]
## without theming
ggplot(d, aes(x=carat, y=price, color=clarity)) +
geom_point() +
theme_minimal() # must set this theme call for every plot
## setting theme
theme_set(theme_minimal())
ggplot(d, aes(x=carat, y=price, color=clarity)) +
geom_point() # plot in theme, for all subsequent plots
Is there a similar, modification that exists to set the color palette throughout? For example, a theme-based replacement for calling,
ggplot(d, aes(x=carat, y=price, color=clarity)) +
geom_point() +
scale_color_brewer(palette='Set2') # requesting a global option to set this for all plots
The linked solution that does not depend on old versions instead overloads the entire ggplot function. That seems risky.
ggplot <- function(...) ggplot2::ggplot(...) + scale_color_brewer(palette = 'Set1')

There is a ggplot_global environment which is used internally within ggplot2 but isn't exported. You can see its structure by temporarily unlocking the bindings of a ggplot function and modifying it to return the contents of the environment as a list. You can do this non-destructively like this:
library(ggplot2)
get_ggplot_global <- function()
{
unlockBinding("theme_set", as.environment("package:ggplot2"))
backup <- body(theme_set)[[5]]
body(theme_set)[[5]] <- substitute(return(as.list(ggplot_global)))
global_list <- theme_set(theme_bw())
body(theme_set)[[5]] <- backup
lockBinding("theme_set", as.environment("package:ggplot2"))
return(global_list)
}
global_list <- get_ggplot_global()
names(global_list)
#> [1] "date_origin" "element_tree" "base_to_ggplot" "all_aesthetics"
#> [5] "theme_current" "time_origin"
By examining this you can see that ggplot global environment has an object called theme_current, which is why you can set the various line, text and axis elements globally including their colours.
When you are talking about a colour scheme in your question, you are referring to the colours defined in a scale object. This is not part of the ggplot_global environment, and you can't change the default scale object because there isn't one. When you create a new ggplot(), it has an empty slot for "scales".
You therefore have a few options:
Wrap ggplot with my_ggplot <- function(...) ggplot2::ggplot(...) + scale_color_brewer()
Overwrite ggplot with the above function (as suggested by #Phil)
Create your own theme object that you add on with standard ggplot syntax
The best thing might be to just write a wrapper around ggplot. However, the third option is also quite clean and idiomatic. You could achieve it like this:
set_scale <- function(...)
{
if(!exists("doc_env", where = globalenv()))
assign("doc_env", new.env(parent = globalenv()), envir = globalenv())
doc_env$scale <- (ggplot() + eval(substitute(...)))$scales$scales[[1]]
}
my_scale <- function() if(exists("doc_env", where = globalenv())) return(doc_env$scale)
You would use this by doing (for example)
set_scale(scale_color_brewer(palette = "Set2"))
At the start of your document.
So now you can just do + my_scale() for each plot:
d <- diamonds[sample(1:nrow(diamonds), 1000), ]
ggplot(d, aes(x=carat, y=price, color=clarity)) +
geom_point() +
my_scale()

Related

Simultaneously applying same modification to all ggarrange subplots

I am in the process of finalising some ggplot figures for a journal with strict graphics requirements.
For example, I have to make sure that the text size is smaller than 7pt, and I defined the following variable, which I can add to any ggplot:
reqs <- theme(axis.text = element_text(size=5, family='sans'), text=element_text(size=7,family='sans'))
I was thinking of generalising this one step further, by writing a ggsave_reqs function which applies the required changes to simple ggplots or ggarrange objects.
According to ?ggarrange, elements from this class are either ggplots or lists of ggplots, and so I tried automatising "suplot changes' with:
lapply(ggarr, function(gg) gg + reqs )
However, this does not work, and I get the bug:
" Cannot add ggproto objects together. Did you forget to add this object to a ggplot object?"
Does anyone have any ideas on whether automatising these ggarrange subplot modifications is possible?
A minimal working example is as follows:
library(ggplot2)
library(ggpubr)
df <- data.frame(X=runif(1000, 0,1),
Y=runif(1000, 2,3),
CLASS=as.factor(rbinom(1000, size=1, p=.5)))
.g <- function(DF)
{
ggplot(DF, aes(x=X, y=Y,color=CLASS)) +
geom_point()
}
ggarr <- ggarrange( .g(df), .g(df[1:10,]))
reqs <- theme(legend.position='bottom')
# none of these works
ggarr + reqs
lapply(ggarr, function(p){ p + reqs } )
# however this works
ggarr <- ggarrange( .g(df) + reqs, .g(df[1:10,]) + reqs)

ggplot - global scale_color_manual [duplicate]

I would like to define a color palette for every plot built in a markdown document. In essence this would overwrite the default choices.
There are several very old answers -- thanks for the links here and here suggested by #dww -- that solve for old versions (specifically calling out a solution on 0.8.2 when the modern release several major releases ahead, currently at 3.2.x).
I'll illustrate the closest use case, setting themes. In the case of general purpose themes, this is trivial: rather than appending + theme_minimal() on every plot, I can instead set the theme which persists across all plots.
library(ggplot2)
d <- diamonds[sample(1:nrow(diamonds), 1000), ]
## without theming
ggplot(d, aes(x=carat, y=price, color=clarity)) +
geom_point() +
theme_minimal() # must set this theme call for every plot
## setting theme
theme_set(theme_minimal())
ggplot(d, aes(x=carat, y=price, color=clarity)) +
geom_point() # plot in theme, for all subsequent plots
Is there a similar, modification that exists to set the color palette throughout? For example, a theme-based replacement for calling,
ggplot(d, aes(x=carat, y=price, color=clarity)) +
geom_point() +
scale_color_brewer(palette='Set2') # requesting a global option to set this for all plots
The linked solution that does not depend on old versions instead overloads the entire ggplot function. That seems risky.
ggplot <- function(...) ggplot2::ggplot(...) + scale_color_brewer(palette = 'Set1')
There is a ggplot_global environment which is used internally within ggplot2 but isn't exported. You can see its structure by temporarily unlocking the bindings of a ggplot function and modifying it to return the contents of the environment as a list. You can do this non-destructively like this:
library(ggplot2)
get_ggplot_global <- function()
{
unlockBinding("theme_set", as.environment("package:ggplot2"))
backup <- body(theme_set)[[5]]
body(theme_set)[[5]] <- substitute(return(as.list(ggplot_global)))
global_list <- theme_set(theme_bw())
body(theme_set)[[5]] <- backup
lockBinding("theme_set", as.environment("package:ggplot2"))
return(global_list)
}
global_list <- get_ggplot_global()
names(global_list)
#> [1] "date_origin" "element_tree" "base_to_ggplot" "all_aesthetics"
#> [5] "theme_current" "time_origin"
By examining this you can see that ggplot global environment has an object called theme_current, which is why you can set the various line, text and axis elements globally including their colours.
When you are talking about a colour scheme in your question, you are referring to the colours defined in a scale object. This is not part of the ggplot_global environment, and you can't change the default scale object because there isn't one. When you create a new ggplot(), it has an empty slot for "scales".
You therefore have a few options:
Wrap ggplot with my_ggplot <- function(...) ggplot2::ggplot(...) + scale_color_brewer()
Overwrite ggplot with the above function (as suggested by #Phil)
Create your own theme object that you add on with standard ggplot syntax
The best thing might be to just write a wrapper around ggplot. However, the third option is also quite clean and idiomatic. You could achieve it like this:
set_scale <- function(...)
{
if(!exists("doc_env", where = globalenv()))
assign("doc_env", new.env(parent = globalenv()), envir = globalenv())
doc_env$scale <- (ggplot() + eval(substitute(...)))$scales$scales[[1]]
}
my_scale <- function() if(exists("doc_env", where = globalenv())) return(doc_env$scale)
You would use this by doing (for example)
set_scale(scale_color_brewer(palette = "Set2"))
At the start of your document.
So now you can just do + my_scale() for each plot:
d <- diamonds[sample(1:nrow(diamonds), 1000), ]
ggplot(d, aes(x=carat, y=price, color=clarity)) +
geom_point() +
my_scale()

Conditionally modify ggplot theme based on presence of facets?

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.
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()
ggplot(mtcars, aes(mpg, wt)) +
geom_point() +
theme_custom()
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.
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.
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}.

User defined colour palette in R and ggpairs

I am trying to use another color palette for the scatter plots from ggpairs from the GGally library in R. See similar question here.
library(ggplot2)
library(GGally)
Works
ggplot(iris, aes(x=Sepal.Width, colour=Species)) + stat_ecdf() + scale_color_brewer(palette="Spectral")
Also works
ggplot <- function(...) ggplot2::ggplot(...) + scale_color_brewer(palette="Spectral")
ggplot(iris, aes(x=Sepal.Width, colour=Species)) + stat_ecdf()
Does not work
ggplot <- function(...) ggplot2::ggplot(...) + scale_color_brewer(palette="Spectral")
ggpairs(iris,
columns=, c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width"),
colour='Species',
lower=list(continuous='points'),
axisLabels='none',
upper=list(continuous='blank')
)
but adding
putPlot(p, ggplot(iris, aes(x=Sepal.Length, colour=Species)) + stat_ecdf(), 1,1)
adds a plot in the right colors.
Workaround
I can change the plots afterwards with getPlot, but that's not pretty..
subplot <- getPlot(a, 2, 1) # retrieve the top left chart
subplotNew <- subplot + scale_color_brewer(palette="Spectral")
a <- putPlot(a, subplotNew, 2, 1)
How can I change the color scheme for the scatter plots in ggpairs? More specifically, I'd like to manually define the colors like so
scale_colour_manual(values=c("#FF0000","#000000", "#0000FF","#00FF00"))
Thanks!
Here is a hack that works:
ggplot <- function(...) ggplot2::ggplot(...) + scale_color_brewer(palette="Spectral")
unlockBinding("ggplot",parent.env(asNamespace("GGally")))
assign("ggplot",ggplot,parent.env(asNamespace("GGally")))
When you assign a new value to the ggplot function, it is in the global environment. Now, GGally imports everything including ggplot when it loads (it didn't have to be that way). At that point, changing the ggplot function in your global environment has no effect, because imports from GGally have precedence. Instead, you need to update the ggplot function on the GGally:imports. There is only one problem: once a package is loaded, its bindings are locked. But we can unlock them (I am guessing this is frowned upon, hence labeling the solution a hack).
See Josh O'Brien's answer under Replace definition of built-in function in R? for more info.
as stacksia said (but also adding scale_fill_brewer)
ggplot <- function(...) ggplot2::ggplot(...) + scale_color_brewer(palette="Spectral") + scale_fill_brewer(palette="Spectral")
unlockBinding("ggplot",parent.env(asNamespace("GGally")))
assign("ggplot",ggplot,parent.env(asNamespace("GGally")))
See Josh O'Brien's answer under Replace definition of built-in function in R? for more info.

Get current ggplot object

Suppose I plot something like this:
ggplot(iris, aes(x=Sepal.Length, y=Petal.Length)) + geom_point()
Then I realise that I forgot to store the result (i.e. the ggplot object).
How can I retrieve the ggplot object corresponding to the current device?
Is there some ggplot function I can feed cur.dev() into to retrieve the associated plot object, or is it gone forever?
(Note - in this case I could do p <- .Last.value, but let's assume I've typed a few commands since then so that this is not available.
Motivation - adding a hook to knitr to automagically set fig.cap to the title of the plot (if any)).
You are after last_plot
It retrieves the last plot to be modified or created and is used by ggsave
Note that it is the last plot modified or created
set_last_plot is the relevant code (see the source)
It is important note that creating modifying or rendering a ggplot object will set the last plot.
ggplot(iris, aes(x=Sepal.Length, y=Petal.Length)) + geom_point()
f <- last_plot()
# will return the iris plot
p <- ggplot(mtcars, aes(x = wt, y = mpg)) + geom_point()
last_plot()
# will return p
f
last_plot()
# is now f
It will also not count any modifications / manipulation using grid or gridExtra (such as grid.arrange / grid.text
The last object assigned (and it does not need to be a plot object) can be recovered with .Last.value
>require(ggplot2)
#Loading required package: ggplot2
ggplot(iris, aes(x=Sepal.Length, y=Petal.Length)) + geom_point()
gp <- .Last.value
gp
This should return plot objects that have been modified by grid functions as long as there was an assignment. I'm not sure it this is true for actions that were mediated through print calls.

Resources