How to align grob with ggplot using ggplotGrob and annotation_custom? - r

I am creating a grob from a ggplot using ggplotGrob and then adding it as a background layer in a complex ggplot construction using annotation_custom since the performance is much improved in a facet_wrap plot with a large dataset. However I am unable to align the underlying grob correctly with the ggplot.
This simple example shows the issue I am trying to solve.
library(ggplot2)
p <- ggplot(mtcars, aes(wt, mpg)) +
geom_point(color = "red")
ggplot(mtcars, aes(wt, mpg)) +
annotation_custom(grob = ggplotGrob(p)) +
geom_point()
I want the plot with red points to be perfectly underneath the plot with black points.

You'd probably want to grab just the panel without axis, margins etc. before adding it as a custom annotation then. In example below, I made the red points larger so you can see that they overlap.
library(ggplot2)
p <- ggplot(mtcars, aes(wt, mpg)) +
geom_point(color = "red", size = 3)
grab_panel <- function(p) {
gt <- ggplotGrob(p)
layout <- gt$layout
is_panel <- which(layout$name == "panel")[[1]]
i <- layout$t[is_panel]
j <- layout$l[is_panel]
gt[i,j]
}
ggplot(mtcars, aes(wt, mpg)) +
annotation_custom(grob = grab_panel(p)) +
geom_point()
Created on 2021-03-20 by the reprex package (v1.0.0)

Related

geom_point plot with only number without circles

In ggplot in R, is it possible to plot each point with a unique number but without circles surrounded? I tried to use color "white" but it doesn't work.
I would recommend geom_text.
set.seed(101)
dd <- data.frame(x=rnorm(50),y=rnorm(50),id=1:50)
library(ggplot2)
ggplot(dd,aes(x,y))+geom_text(aes(label=id))
I'll show how to do it with geom_text and/or geom_point.
Using geom_text (recommended)
For this example I'll use the built-in dataset mtcars and let's pretend the numbers you want to display are the weights (wt) variable:
data(mtcars)
p <- ggplot(mtcars, aes(wt, mpg, label = rownames(mtcars)))
p + geom_text(aes(label = wt),
parse = TRUE)
or if you want an example with truly unique numbers, we can just make up an index using seq:
data(mtcars)
p <- ggplot(mtcars, aes(wt, mpg, label = rownames(mtcars)))
p + geom_text(aes(label = seq(1:32)),
parse = TRUE)
Using geom_point
While it would require more work, it actually is possible to do this with geom_point.
This is a reference image of some of the shapes you can use with geom_point:
As you can see, shapes 48 to 57 are 0 to 9. You can leverage these shapes (and combinations of them to form an infinite amount of numbers) via geom_point like this:
d=data.frame(p=c(48:57))
ggplot() +
scale_y_continuous(name="") +
scale_x_continuous(name="") +
scale_shape_identity() +
geom_point(data=d, mapping=aes(x=p%%16, y=p%/%16, shape=p), size=5, fill="red")
Finally, a trivial example using mtcars + geom_point with arbitrary numbers:
d=data.frame(p=c(48:57,48:57,48:57,48,49))
attach(mtcars)
ggplot(mtcars) +
scale_y_continuous(name="") +
scale_x_continuous(name="") +
scale_shape_identity() +
geom_point(data=d, mapping=aes(x=wt, y=mpg, shape=p), size=5, fill="red")

show ggplot2 title without reserving space for it

I am trying to set titles to some ggplot2 charts, while leaving some without titles. Unfortunately, when title is set, the y axis and the plot shrink (see the plot on the right). I need to plot the title without changing the Y axis size, so that titled charts are on the same scale with the others (as in the middle plot).
grid.arrange(
(ggplot(mtcars, aes(mpg, hp)) + geom_point()),
(ggplot(mtcars, aes(mpg, hp)) + geom_point() +
geom_text(aes(22.5, 340, label="fake title", vjust = 1, hjust = .5, show_guide = FALSE))),
(ggplot(mtcars, aes(mpg, hp)) + geom_point() +
labs(title="real title")),
ncol=3)
I cannot use fake empty-string titles on the other plots, because I am short on space.
I could use the geom_text() method, if anyone can tell me how to make it look less garbled.
So, how do I removing any reserved space for the title above the plot, while still showing the plot title on and at the top of the plot area? The latter is done with theme(plot.title = element_text(vjust=-1)).)
Edit
Thanks to #baptiste for pointing out a more concise way to accomplish this. Given p1, p2, and p3 from below:
pl = lapply(list(p1,p2,p3), ggplotGrob)
grid.newpage()
grid.draw(do.call(cbind, c(pl, size="first")))
Original answer
You can build the ggplot grobs and standardize the heights parameter across plots:
p1 <- ggplot(mtcars, aes(mpg, hp)) + geom_point()
p2 <- ggplot(mtcars, aes(mpg, hp)) + geom_point() + labs(title="real title")
p3 <- ggplot(mtcars, aes(mpg, hp)) + geom_point() +
geom_text(aes(22.5, 340, label="fake title", vjust = 1, hjust = .5, show_guide = FALSE))
p1 <- ggplot_gtable(ggplot_build(p1))
p2 <- ggplot_gtable(ggplot_build(p2))
p3 <- ggplot_gtable(ggplot_build(p3))
p2$heights <- p1$heights
p3$heights <- p1$heights
grid.arrange(p1, p2, p3, ncol=3)
You can then use the title's vjust setting to move it off of the plot or further onto the plot, if you want:
p2 <- ggplot(mtcars, aes(mpg, hp)) + geom_point() +
labs(title="real title") +
theme(plot.title=element_text(vjust=-.3))
You should use annotate for this. If you use geom_text, it will print as many labels as there are rows in your data, hence the poor-looking overplotted label in your question. A painful work-around is to create a 1-row data frame to use as the data for the geom_text layer. However, annotate is intended for this sort of thing so you don't need a work-around. Something like:
annotate(geom = "text", x = 22.5, y = 340, label="fake title")
is good practice. Annotate is also useful for adding single horizontal or vertical lines to a plot, or to highlight a region by drawing a rectangle around it.

incorrect linetype in geom_vline with legend in r ggplot2

I am trying to display the xvar median as a dotted line & show it in the legend. Here's my code:
require(ggplot2)
require(scales)
medians_mtcars <- data.frame("wt.median"=median(mtcars$wt))
# legend shows but linetype is wrong (solid)
p <- ggplot(mtcars, aes(wt, mpg))
p <- p + geom_point()
p <- p + geom_vline(aes(xintercept=wt.median, linetype="dotted"),
data=medians_mtcars, show_guide=TRUE)
p
I also tried:
# linetype is correct but legend does not show
p <- ggplot(mtcars, aes(wt, mpg))
p <- p + geom_point()
p <- p + geom_vline(aes(xintercept=wt.median),
data=medians_mtcars, show_guide=TRUE, linetype="dotted")
p
Would have liked to post the plot images, but haven't crossed the reputation threshold yet.
There were 2 other posts on this forum that comes close to this topic but does not offer a solution to this problem:
Add vline to existing plot and have it appear in ggplot2 legend?
;
Incorrect linetype in legend, ggplot2 in R
I am using ggplot2 version 1.0.0
What am I doing wrong ?
Thanks in advance
If you need to show linetype in legend and also change it then inside aes() you can just write name for that linetype (as you have only one line) and then change linetype with scale_linetype_manual().
ggplot(mtcars, aes(wt, mpg)) +
geom_point() +
geom_vline(aes(xintercept=wt.median, linetype="media"),
data=medians_mtcars, show_guide=TRUE)+
scale_linetype_manual(values="dotted")
If you really want to type linetype in aes() and also get correct legend then you should use scale_linetype_identity() with argument guide="legend".
ggplot(mtcars, aes(wt, mpg)) +
geom_point() +
geom_vline(aes(xintercept=wt.median, linetype="dotted"),
data=medians_mtcars,show_guide=TRUE)+
scale_linetype_identity(guide="legend")

Separate y-axis labels by facet OR remove legend but keep the space

Ok, I'm stumped on a home-brew ggplot.
What I would like to do is have a three row, one column faceted plot with a different y-axis label for each facet. The units of the y-axis are all the same. This would be the most convenient, but googling tells me it may not be possible.
Alternatively, I found this solution using grid.arrange, which seems like it will work. However, I want to keep a legend only for one plot and remove it from the other two, but maintain the spacing as if it were still there so that everything lines up nice. Someone had the same problem a few years ago, but the suggested solution is depreciated and I can't sort out how to make it work in modern ggplot.
Any help is appreciated! Using facets would be easiest!
Edited to add copy of plot after using user20560's gridArrange solution below. Very nearly there, just would like to get back the box around the top and bottom facet panels!
I have assumed (possibly wrongly) that you are wanting to add separate y-axis titles rather than axis labels. [If it is the labels you want different you can use the scales argument in facet_grid]
There will be a ggplot way to do this but here are a couple of ways you could tweak the grobs yourself.
So using mtcars dataset as example
library(ggplot2)
library(grid)
library(gridExtra)
One way
p <- ggplot(mtcars, aes(mpg, wt, col=factor(vs))) + geom_point() +
facet_grid(gear ~ .)
# change the y axis labels manually
g <- ggplotGrob(p)
yax <- which(g$layout$name=="ylab")
# define y-axis labels
g[["grobs"]][[yax]]$label <- c("aa","bb", "cc")
# position of labels (ive just manually specified)
g[["grobs"]][[yax]]$y <- grid::unit(seq(0.15, 0.85, length=3),"npc")
grid::grid.draw(g)
Or using grid.arrange
# Create a plot for each level of grouping variable and y-axis label
p1 <- ggplot(mtcars[mtcars$gear==3, ], aes(mpg, wt, col=factor(vs))) +
geom_point() + labs(y="aa") + theme_bw()
p2 <- ggplot(mtcars[mtcars$gear==4, ], aes(mpg, wt, col=factor(vs))) +
geom_point() + labs(y="bb") + theme_bw()
p3 <- ggplot(mtcars[mtcars$gear==5, ], aes(mpg, wt, col=factor(vs))) +
geom_point() + labs(y="cc") + theme_bw()
# remove legends from two of the plots
g1 <- ggplotGrob(p1)
g1[["grobs"]][[which(g1$layout$name=="guide-box")]][["grobs"]] <- NULL
g3 <- ggplotGrob(p3)
g3[["grobs"]][[which(g3$layout$name=="guide-box")]][["grobs"]] <- NULL
gridExtra::grid.arrange(g1,p2,g3)
If it is the axis titles you want to add I should ask why you want a different titles - can the facet strip text not do?
Following the comments by Axeman and aosmith (thank you), here's a way to do this using the facet labels using ggplot2 version 2.2.0
library(ggplot2) # From sessionInfo(): ggplot2_2.2.0
ggplot(mtcars, aes(mpg, wt, col=factor(vs))) + geom_point() +
facet_grid(gear ~ ., switch = 'y') +
theme( axis.title.y = element_blank(), # remove the default y-axis title, "wt"
strip.background = element_rect(fill = 'transparent'), # replace the strip backgrounds with transparent
strip.placement = 'outside', # put the facet strips on the outside
strip.text.y = element_text(angle=180)) # rotate the y-axis text (optional)
# (see ?ggplot2::theme for a list of theme elements (args to theme()))
I know this is an old post, but after finding it, I could not get #user20560's response to work.
I've edited #user20560's grid.extra approach as follows:
library(ggplot2)
library(gridExtra)
library(grid)
# Create a plot for each level of grouping variable and y-axis label
p1 <- ggplot(mtcars[mtcars$gear==3, ], aes(mpg, wt, col=factor(vs))) +
geom_point() + labs(y="aa") + theme_bw()
p2 <- ggplot(mtcars[mtcars$gear==4, ], aes(mpg, wt, col=factor(vs))) +
geom_point() + labs(y="bb") + theme_bw()
p3 <- ggplot(mtcars[mtcars$gear==5, ], aes(mpg, wt, col=factor(vs))) +
geom_point() + labs(y="cc") + theme_bw()
# get the legend as a grob
legend <- ggplotGrob(p1)
legend <- legend$grobs[[which(legend$layout$name=="guide-box")]]
lheight <- sum(legend$height)
lwidth <- sum(legend$width)
# remove the legend from all the plots
p1 <- p1 + theme(legend.position = 'none')
p2 <- p2 + theme(legend.position = 'none')
p3 <- p3 + theme(legend.position = 'none')
# force the layout to the right side
layoutMat <- matrix(c(1,2,3,4,4,4),ncol = 2)
grid.arrange(p1,p2,p3,legend, layout_matrix = layoutMat, ncol = 2,
widths = grid::unit.c(unit(1,'npc') - lwidth, lwidth))
This example is somewhat specific to this particular layout. There is a more general approach on the ggplot2 wiki.
I too had trouble getting the first approach in the answer of user20560 (above) to work. This is probably because the internals of ggplot2 have evolved, and there is no guarantee that these internals should stay the same. In any case, here is a version that currently works:
library(ggplot2) # From sessionInfo(): ggplot2_2.1.0
library(grid)
p <- ggplot(mtcars, aes(mpg, wt, col=factor(vs))) + geom_point() + facet_grid(gear ~ .)
g <- ggplotGrob(p)
yax <- which(g$layout$name == "ylab")
g[["grobs"]][[yax]]$children[[1]]$label <- c('fo','bar','foobar')
g[["grobs"]][[yax]]$children[[1]]$y <- grid::unit(seq(0.15, 0.85, length=3), "npc")
grid.draw(g)
Note that this is the approach that keeps the facets and does not repeat the x-axes.

Inserting an image as a facet in ggplot2

I'm using ggplot2 to produce faceted plots in a function (i.e., the number of facets varies). For each plot that I've produced, I'd like to insert one image (e.g. imported jpg) in a free facet at the end (i.e., ggplot2 has to calculate the facet_wrap design in a way that foresees the existence of an additional facet, but this facet would be filled with a non-ggplot2 image afterwards). Here is an MWE of a faceted plot, in this case I'd like to insert an image as the 4th facet:
p <- ggplot(mtcars, aes(wt, mpg))
p + geom_point() + facet_wrap(~cyl, ncol = 2)
library(ggplot2)
library(png)
img <- readPNG(system.file("img", "Rlogo.png", package="png"))
p <- ggplot(mtcars, aes(wt, mpg)) + geom_point() +
facet_wrap(~cyl, ncol = 2)
g <- ggplotGrob(p)
library(gtable)
g <- gtable_add_grob(g, rasterGrob(img), nrow(g)-4, ncol(g)-2)
grid.newpage()
grid.draw(g)
baptiste's solution was throwing an error for me; I had to direct some of the grid functions to the {grid} package, the code below should overcome this. Though I am not seeing the R logo appear.
library(ggplot2)
library(png)
img <- readPNG(system.file("img", "Rlogo.png", package="png"))
p <- ggplot(mtcars, aes(wt, mpg)) + geom_point() +
facet_wrap(~cyl, ncol = 2)
g <- ggplotGrob(p)
library(gtable)
g <- gtable_add_grob(g, grid::rasterGrob(img), nrow(g)-4, ncol(g)-2)
grid::grid.newpage()
grid::grid.draw(g)

Resources