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}.
Related
I am creating an R package that will produce graphs that naturally go into all four quadrants - positive and negative values on the x and y axis.
I would like to create a ggplot2 theme that will center the axes at (0,0) to help make this look nice!
Obviously I could do this on a graph-by-graph basis by adding geom_vline and geom_hline. Effectively, this is what I want the result to look like:
library(ggplot2)
ggplot(mtcars, aes(x = mpg-20, y = hp-150)) +
geom_point() +
theme_void() +
geom_vline(aes(xintercept = 0)) +
geom_hline(aes(yintercept = 0))
This is perfect - add some decoration and I'm done.
However, I would like to do this in the theme so that users of the package can just do +theme_fourquadrant() and have it done for them.
When I try to define a theme like this...
theme_fourquadrant <- function() {
theme_void() %+replace%
geom_vline(aes(xintercept = 0)) +
geom_hline(aes(yintercept = 0))
}
I get the error that %+replace% requires a theme object on either side. If I replace that with + I get that you can't add geoms to a theme object, naturally.
I also haven't had luck trying to only get a major grid axis to show up for 0.
Is this possible to do? I'm a little worried that "0" counts as data and so the theme will refuse to have anything to do with it. I'm sure there are some ways I can do this by not making it happen in the theme, but having it done in the theme would be ideal.
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()
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()
I want the user to choose whether they want to see a geom_line or a geom_bar on the graph.
What should be the best way to do it, using a checkboxGroupInput?
Let's say I have this plot with geom_line:
ggplot(regiao, aes(x=Ano,fill=Regiao, color=Regiao))+
geom_line(stat='count')
and this one with geom_bar:
ggplot(regiao, aes(x=Ano,fill=Regiao, color=Regiao))+
geom_bar()+
stat_count()
I'd like to know how to set reactively the arguments for the ggplot.
I've searched everywhere but could only find reactive functions for values, not arguments, like this:
ggplot2 with reactive Geom lines
I guess it should look like this:
ggplot(regiao, aes(x=Ano,fill=Regiao,color=Regiao))+
if (input$checkbox == "bars") {
+ geom_bar()+
stat_count()
}
if (input$checkbox=="lines") {
+geom_line(stat='count')
}
You can save ggplot objects to a variable and build onto it however you need. Outside of a Shiny context, you could do this by referring to a variable, or encapsulating the task of adding onto a ggplot in a function. Inside a Shiny context, you can decide what to add to the object based on a user input.
For example, in a plain R context:
library(ggplot2)
set.seed(123)
df <- data.frame(
month = 1:10,
value = runif(10)
)
Create the base ggplot object. As an example, I also specified a y scale, because the ggplot elements like geoms and scales don't have to be added in any kind of order. You might have some specification like a scale that holds regardless of the geom type.
plot_base <- ggplot(df, aes(x = month, y = value)) +
scale_y_continuous(labels = scales::percent)
Then add a geom based on what type of plot. In a Shiny app, this would instead be input$plot_type, and take the value of a pair of radio buttons or some other input. That decision-making would go inside your render function, such as renderPlot.
plot_type <- "Line"
if(plot_type == "Line") {
p <- plot_base +
geom_line()
} else {
p <- plot_base +
geom_col()
}
p
Same logic applies with a bar/column. You just wouldn't have to run through the if/else again—it would rerun reactively when the user changes the input.
plot_type <- "Bar"
if(plot_type == "Line") {
p <- plot_base +
geom_line()
} else {
p <- plot_base +
geom_col()
}
p
Created on 2018-10-17 by the reprex package (v0.2.1)
Could you make both plots and then use the checkbox as a condition to determine what gets output?
output$myPlot <- renderPlot(ifelse(input$checkbox == "bars", myBarPlot, myLinePlot))
I am trying to highlight an x-axis value on my chart which I can do based on this example, however I run into issues when I try to facet things. The facets have varying sizes and orders along the x-axis. This is ultimately what complicates things. I also suspect that the x-axis for each of the facets has to be the same, however I am hoping someone can prove me different.
My example is pure sample data, and the size of my sets is a bit larger, so I'll apologise now if when I test it on the real data set it leads to more questions.
Data
library(data.table)
dt1 <- data.table(name=as.factor(c("steve","john","mary","sophie","steve","sophie")),
activity=c("a","a","a","a","b","b"),
value=c(22,32,12,11,25,32),
colour=c("black","black","black","red","black","red"))
dt1[,myx := paste(activity, name,sep=".")]
dt1$myx <- reorder(dt1$myx, dt1$value,sum)
Function to help with the sorting of the items in the x axis based on this SO question.
roles <- function(x) sub("[^_]*\\.","",x )
Chart
ggplot() +
geom_bar(data=dt1,aes(x=myx, y=value), stat="identity") +
facet_grid( ~ activity, scales = "free_x",space = "free_x") +
theme(axis.text.x = element_text(colour=dt1[,colour[1],by=myx][,V1])) +
scale_x_discrete(labels=roles)
You can see that even though the "red" is assigned to sophie the formatting is applied to john. Some of this has to do with the ordering of the dataset.
Chart2
If I add in the setkey i get close to the right outcome
setkey(dt1,myx)
ggplot() +
geom_bar(data=dt1,aes(x=myx, y=value), stat="identity") +
facet_grid( ~ activity, scales = "free_x",space = "free_x") +
theme(axis.text.x = element_text(colour=dt1[,colour[1],by=myx][,V1])) +
scale_x_discrete(labels=roles)
Unfortunately we see that the 2nd facet has the x-axis item highlighted red. I think this is because it takes the formatting from the first chart and applies it in the same order in the 2nd chart.
Any ideas on how to apply the formatting to work where the same person exists across activities or where a person exists in only one activity would be greatly appreciated.
If you can live with a rather dirty hack, I can share what I do in these cases. Basically I mess around with the underlying grid structure, which is basically a lot of browser and str calls in the beginning :)
ggplot
p <- ggplot() +
geom_bar(data=dt1,aes(x=myx, y=value), stat="identity") +
facet_grid( ~ activity, scales = "free_x",space = "free_x") +
scale_x_discrete(labels=roles)
grid
Now you have to extract the underlying grob object representing the x-axis to be able to change the color.
library(grid)
bp <- ggplotGrob(p)
wh <- which(grepl("axis-b", bp$layout$name)) # get the x-axis grob
bp$grobs[wh] contains now the two x-axis. Now you have to dive even further into the object to change the color.
bp$grobs[wh] <- lapply(bp$grobs[wh], function(gg) {
## we need to extract the right element
## this is not that straight forward, but in principle I used 'str' to scan through
## the objects to find out which element I would need
kids <- gg$children
wh <- which(sapply(kids$axis$grobs, function(.) grepl("axis\\.text", .$name)))
axis.text <- kids$axis$grobs[[wh]]
## Now that we found the right element, we have to replicate the colour and change
## the element corresponding to 'sophie'
axis.text$gp$col <- rep(axis.text$gp$col, length(axis.text$label))
axis.text$gp$col[grepl("sophie", axis.text$label)] <- "red"
## write the changed object back to the respective slot
kids$axis$grobs[[wh]] <- axis.text
gg$children <- kids
gg
})
So, now 'all' we have to do is to plot the grid object:
grid.draw(bp)
Admittedly, that's rather a rough hack, but it delivers what is needed:
Update
This does not work for more recent versions of ggplot2 as the internal structure of the grob changed. Thus, you need a little adaptation to make it work again. In principle the relevant grob slot moved one slot further down and can be now found in .$children[[1]]
bp$grobs[wh] <- lapply(bp$grobs[wh], function(gg) {
## we need to extract the right element
## this is not that straight forward, but in principle I used 'str' to scan through
## the objects to find out which element I would need
kids <- gg$children
wh <- which(sapply(kids$axis$grobs, function(.) grepl("axis\\.text", .$name)))
axis.text <- kids$axis$grobs[[wh]]$children[[1]]
## Now that we found the right element, we have to replicate the colour and change
## the element corresponding to 'sophie'
axis.text$gp$col <- rep(axis.text$gp$col, length(axis.text$label))
axis.text$gp$col[grepl("sophie", axis.text$label)] <- "red"
## write the changed object back to the respective slot
kids$axis$grobs[[wh]]$children[[1]] <- axis.text
gg$children <- kids
gg
})
grid.draw(bp)
Try:
ggplot() +
geom_bar(data=dt1,aes(x=name, y=value, fill = name), stat="identity") +
facet_grid( ~ activity) + scale_fill_manual(values = c("black","black","red", "black"))