Insert Layer underneath existing layers in ggplot2 object - r

Given an Existing plot object is it possible to add a layer UNDERNEATH an existing layer?
Example, in the graph below, is it possible to add geom_boxplot() to P such that the boxplot appears underneath geom_point()?
## Starting from:
library(ggplot2)
P <- ggplot(data=dat, aes(x=id, y=val)) + geom_point()
## This adds boxplot, but obscures some of the points
P + geom_boxplot()
Expected Output:
# Which is essentially
ggplot(data=dat, aes(x=id, y=val)) + geom_boxplot() + geom_point()
## However, this involves re-coding all of P (after the point insertion of the new layer).
## which is what I am hoping to avoid.
Bonus question: If there are multiple layers in the existing plot, is it possible to indicate where specifically to insert the new layer (with respect to the existing layers)?
SAMPLE DATA
set.seed(1)
N <- 100
id <- c("A", "B")
dat <- data.frame(id=sample(id, N, TRUE), val=rnorm(N))

Thanks #baptiste for pointing me in the right direction. To insert a layer underneath all other layers, just modify the layers element of the plot object.
## For example:
P$layers <- c(geom_boxplot(), P$layers)
Answer to the Bonus Question:
This handy little function inserts a layer at a designated z-level:
insertLayer <- function(P, after=0, ...) {
# P : Plot object
# after : Position where to insert new layers, relative to existing layers
# ... : additional layers, separated by commas (,) instead of plus sign (+)
if (after < 0)
after <- after + length(P$layers)
if (!length(P$layers))
P$layers <- list(...)
else
P$layers <- append(P$layers, list(...), after)
return(P)
}

Expanding on Ricardo' answer, I use the following snippet:
library(ggplot2)
`-.gg` <- function(plot, layer) {
if (missing(layer)) {
stop("Cannot use `-.gg()` with a single argument. Did you accidentally put - on a new line?")
}
if (!is.ggplot(plot)) {
stop('Need a plot on the left side')
}
plot$layers = c(layer, plot$layers)
plot
}
As it allows you to:
P <- ggplot(data=dat, aes(x=id, y=val)) + geom_point()
P - geom_boxplot()
And the boxplot will be placed below the points.

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)

Duplicate Same Legend Twice in Ggplot2

I am preparing a chart where I have client's requirement to put same legend on top and bottom. Using ggplot I can put it either at top or at bottom. But I am not aware about option of duplicating at both the places.
I have tried putting legend.position as c('top','bottom') but that is giving me error and I know if should give error.
Can it be done with other libraries? I want to same legend twice at top and at bottom?
Take this code for an instance
library(ggplot2)
bp <- ggplot(data=PlantGrowth, aes(x=group, y=weight, fill=group)) + geom_boxplot()
bp <- bp + theme(legend.position="bottom")
bp
You have to work with the intermediate graphic objects (grobs) that ggplot2 uses when being plotted.
I grabbed a function that was flowing around here on StackOverflow to extract the legend, and put it into a package that is now on CRAN.
Here's a solution:
library(lemon)
bp <- bp + theme(legend.position='bottom')
g <- ggplotGrob(bp)
l <- g_legend(g)
grid.arrange(g, top=l)
g_legend accepts both the grob-version (that cannot be manipulated with ggplot2 objects) and the ordinary ggplot2 objects. Using ggplotGrob is a one-way street; once converted you cannot convert it back to ggplot2. But, as in the example, we keep the original ggplot2 object. ;)
Depending on the use case, a center-aligned top legend may not be appropriate as in the contributed answer by #MrGrumble here: https://stackoverflow.com/a/46725487/5982900
Alternatively, you can copy the "guide-box" element of the ggplotGrob, append it to your grob object, and reset the coordinates to the top of the ggplot.
createTopLegend <- function(ggplot, heightFromTop = 1) {
g <- ggplotGrob(ggplot)
nGrobs <- (length(g$grobs))
legendGrob <- which(g$layout$name == "guide-box")
g$grobs[[nGrobs+ 1]] <- g$grobs[[legendGrob]]
g$layout[nGrobs+ 1,] <- g$layout[legendGrob,]
rightLeft <- unname(unlist(g$layout[legendGrob, c(2,4)]))
g$layout[nGrobs+ 1, 1:4] <- c(heightFromTop, rightLeft[1], heightFromTop, rightLeft[2])
g
}
Load the gridExtra package. From your ggplot object bp, use createTopLegend to duplicate another legend, then use grid.draw to produce your final figure. Note you may need to alter your plot margins depending on your figure.
library(ggplot2)
library(grid)
library(gridExtra)
bp <- ggplot(data=PlantGrowth, aes(x=group, y=weight, fill=group)) + geom_boxplot()
bp <- bp + theme(legend.position="bottom", plot.margin = unit(c(2,0,0,0), "lines"))
g <- createTopLegend(bp)
grid.draw(g)
# dev.off()
This will ensure the legend is aligned in the same way horizontally as it appears in your original ggplot.

Colouring ggplot x axis in facets

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"))

R - chart Y,X over categories Z, some categories as points others as lines

In a plot of Y and X over categories Z, I would like for categories to be represented by points of different collor, except for one category, which I would like to be displayed as a line connecting the points.
Here is the data and what I have so far:
library(ggplot2);library(reshape);library(scales);library(directlabels)
dat <- read.csv("https://dl.dropboxusercontent.com/u/4329509/Fdat_graf.csv")
dat_long <- melt(dat, id="ano")
p <- qplot(ano,value, data=dat_graf_long, colour=variable)+
scale_y_log10(breaks=c(.1,1,10,100,500,1000),labels = comma) +
scale_x_continuous(breaks=seq(from=1960, to=2010, by=10)) +
theme_bw()
direct.label(p)
I would like for the "Lei_de_Moore" category to be represented by a line, as in this example (done in Stata):
Also, I would like to change a few things (maybe I should ask tem in different topic?):
Change the style of the graph colors more "vivid", as in the Stata
example
Change the Y aixis. I just want plain Numbers in non-scientific
notation form. I used the labels="comma", but I don't want the coma
itself. Ideally I would like the comma to be the decimal place
separator.
EDIT: I had asked another question on how to embed the legend for this graph (this post: Legend as text alongside points for each category and with same collor)
You can mix geoms if you use ggplot and pass only a subset of the data to different geoms. Here you can pass everything in dat_long to geom_point except rows where variable is Lei_de_Moore, and then pass only those dat_long rows to geom_line in a different call.
p <- ggplot(dat_long, aes(ano, value, color=variable)) +
geom_point(data=dat_long[dat_long$variable != 'Lei_de_Moore',]) +
geom_line(data=dat_long[dat_long$variable == 'Lei_de_Moore',]) +
scale_y_log10(breaks=c(.1,1,10,100,500,1000),labels = comma) +
scale_x_continuous(breaks=seq(from=1960, to=2010, by=10)) +
theme_bw()
For colors, have a look at RColorBrewer package palettes. Install the package and use ?brewer.pal to see some more options. For example, this one might work:
p <- p + scale_color_brewer(palette="Set1")
For the y-axis labels, you'll probably have to hack something together. Have a look at this question. So you could do something like this:
fmt <- function(){
f <- function(x) sub(".", ",", as.character(round(x,1)), fixed=T)
f
}
p <- ggplot(dat_long, aes(ano, value, color=variable)) +
geom_point(data=dat_long[dat_long$variable != 'Lei_de_Moore',]) +
geom_line(data=dat_long[dat_long$variable == 'Lei_de_Moore',]) +
scale_y_log10(breaks=c(.1,1,10,100,500,1000), labels=fmt()) +
scale_x_continuous(breaks=seq(from=1960, to=2010, by=10)) +
theme_bw() +
scale_color_brewer(palette="Set1")

Remove a layer from a ggplot2 chart

I'd like to remove a layer (in this case the results of geom_ribbon) from a ggplot2 created grid object. Is there a way I can remove it once it's already part of the object?
library(ggplot2)
dat <- data.frame(x=1:3, y=1:3, ymin=0:2, ymax=2:4)
p <- ggplot(dat, aes(x=x, y=y)) + geom_ribbon(aes(ymin=ymin, ymax=ymax), alpha=0.3)
+ geom_line()
# This has the geom_ribbon
p
# This overlays another ribbon on top
p + geom_ribbon(aes(ymin=ymin, ymax=ymax, fill=NA))
I'd like this functionality to allow me to build more complicated plots on top of less complicated ones. I am using functions that return a grid object and then printing out the final plot once it is fully assembled. The base plot has a single line with a corresponding error bar (geom_ribbon) surrounding it. The more complicated plot will have several lines and the multiple overlapping geom_ribbon objects are distracting. I'd like to remove them from the plots with multiple lines. Additionally, I'll be able to quickly create alternative versions using facets or other ggplot2 functionality.
Edit: Accepting #mnel's answer as it works. Now I need to determine how to dynamically access the geom_ribbon layer, which is captured in the SO question here.
Edit 2: For completeness, this is the function I created to solve this problem:
remove_geom <- function(ggplot2_object, geom_type) {
layers <- lapply(ggplot2_object$layers, function(x) if(x$geom$objname == geom_type) NULL else x)
layers <- layers[!sapply(layers, is.null)]
ggplot2_object$layers <- layers
ggplot2_object
}
Edit 3: See the accepted answer below for the latest versions of ggplot (>=2.x.y)
For ggplot2 version 2.2.1, I had to modify the proposed remove_geom function like this:
remove_geom <- function(ggplot2_object, geom_type) {
# Delete layers that match the requested type.
layers <- lapply(ggplot2_object$layers, function(x) {
if (class(x$geom)[1] == geom_type) {
NULL
} else {
x
}
})
# Delete the unwanted layers.
layers <- layers[!sapply(layers, is.null)]
ggplot2_object$layers <- layers
ggplot2_object
}
Here's an example of how to use it:
library(ggplot2)
set.seed(3000)
d <- data.frame(
x = runif(10),
y = runif(10),
label = sprintf("label%s", 1:10)
)
p <- ggplot(d, aes(x, y, label = label)) + geom_point() + geom_text()
Let's show the original plot:
p
Now let's remove the labels and show the plot again:
p <- remove_geom(p, "GeomText")
p
If you look at
p$layers
[[1]]
mapping: ymin = ymin, ymax = ymax
geom_ribbon: na.rm = FALSE, alpha = 0.3
stat_identity:
position_identity: (width = NULL, height = NULL)
[[2]]
geom_line:
stat_identity:
position_identity: (width = NULL, height = NULL)
You will see that you want to remove the first layer
You can do this by redefining the layers as just the second component in the list.
p$layer <- p$layer[2]
Now build and plot p
p
Note that p$layer[[1]] <- NULL would work as well. I agree with #Andrie and #Joran's comments regarding in wehat cases this might be useful, and would not expect this to be necessarily reliable.
As this problem looked interesting, I have expanded my 'ggpmisc' package with functions to manipulate the layers in a ggplot object (currently in package 'gginnards'). The functions are more polished versions of the example in my earlier answer to this same question. However, be aware that in most cases this is not the best way of working as it violates the Grammar of Graphics. In most cases one can assemble different variations of the same figure in the normal way with operator +, possibly "packaging" groups of layers into lists to have combined building blocks that can simplify the assembly of complex figures. Exceptionally we may want to edit an existing plot or a plot output by a higher level function that whose definition we cannot modify. In such cases these layer manipulation functions can be useful. The example above becomes.
library(gginnards)
p1 <- delete_layers(p, match_type = "GeomText")
See the documentation of the package for other examples, and for information on the companion functions useful for modifying the ordering of layers, and for inserting new layers at arbitrary positions.
#Kamil Slowikowski Thanks! Very useful. However I could not stop myself from creating a new variation on the same theme... hopefully easier to understand than that in the original post or the updated version by Kamil, also avoiding some assignments.
remove_geoms <- function(x, geom_type) {
# Find layers that match the requested type.
selector <- sapply(x$layers,
function(y) {
class(y$geom)[1] == geom_type
})
# Delete the layers.
x$layers[selector] <- NULL
x
}
This version is functionally identical to Kamil's function, so the usage example above does not need to be repeated here.
As an aside, this function can be easily adapted to select the layers based on the class of the stat instead of the class of the geom.
remove_stats <- function(x, stat_type) {
# Find layers that match the requested type.
selector <- sapply(x$layers,
function(y) {
class(y$stat)[1] == stat_type
})
# Delete the layers.
x$layers[selector] <- NULL
x
}
#Kamil and #Pedro Thanks a lot! For those interested, one can also augment Pedro's function to select only specific layers, as shown here with a last_only argument:
remove_geoms <- function(x, geom_type, last_only = T) {
# Find layers that match the requested type.
selector <- sapply(x$layers,
function(y) {
class(y$geom)[1] == geom_type
})
if(last_only)
selector <- max(which(selector))
# Delete the layers.
x$layers[selector] <- NULL
x
}
Coming back to #Kamil's example plot:
set.seed(3000)
d <- data.frame(
x = runif(10),
y = runif(10),
label = sprintf("label%s", 1:10)
)
p <- ggplot(d, aes(x, y, label = label)) + geom_point() + geom_point(color = "green") + geom_point(size = 5, color = "red")
p
p %>% remove_geoms("GeomPoint")
p %>% remove_geoms("GeomPoint") %>% remove_geoms("GeomPoint")

Resources