How to rotate legend symbols in ggplot2? - r

Consider for example this plot using the data mtcars and the function coord_flip
library(ggplot2)
library(Hmisc)
ggplot(mtcars,aes(x=gear,y=cyl)) + stat_summary(aes(color=as.factor(rep(1:2,16))),
fun.data=mean_cl_boot, position=position_dodge(0.4)) + coord_flip()
The fact that error bars are horizontal on the graph but vertical in the legend bothers me :) How can I rotate these symbols?

Tweak the legend key
GeomPointrange$draw_key <- function (data, params, size) {
draw_key_vpath <- function (data, params, size) {
# only need to change the x&y coords so that the line is horizontal
# originally, the vertical line was `0.5, 0.1, 0.5, 0.9`
segmentsGrob(0.1, 0.5, 0.9, 0.5,
gp = gpar(col = alpha(data$colour, data$alpha),
lwd = data$size * .pt, lty = data$linetype,
lineend = "butt"), arrow = params$arrow)
}
grobTree(draw_key_vpath(data, params, size),
draw_key_point(transform(data, size = data$size * 4), params))
}
Then plot
ggplot(mtcars,aes(x=gear,y=cyl)) +
stat_summary(aes(color=as.factor(rep(1:2,16))),
fun.data=mean_cl_boot, position=position_dodge(0.4)) +
coord_flip()

I'm not coming up with an answer that works within the normal ggplot2 workflow, so for now, here's a hacky answer. Turn off the stat_summary legend. Then, add point and line geoms with data that is outside the range of the actual data you want to plot. This will create the point and horizontal line legend that you want. Then set the plot axis limits to include only the range of your real data, so that the fake data points are not visible.
ggplot(mtcars, aes(x=gear, y=cyl, color=as.factor(rep(1:2,16)))) +
stat_summary(fun.data=mean_cl_boot, position=position_dodge(0.4), show.legend=FALSE) +
geom_line(aes(y=cyl-100)) +
geom_point(aes(y=cyl-100), size=2.5) +
coord_flip(ylim=range(mtcars$cyl))
Another option would be to rotate the legend-key grobs by 90 degrees using grid functions, but I'll leave that for someone who's more skilled with grid than I am.

The ggstance package provides an easy to implement solution here:
library(ggplot2)
library(ggstance)
ggplot(mtcars,aes(x=cyl,y=gear)) + stat_summaryh(aes(color=as.factor(rep(1:2,16))),
fun.data=mean_cl_boot_h, position = position_dodgev(height = 0.4))
or as a geom:
df <- data.frame(x = 1:3, y = 1:3)
ggplot(df, aes(x, y, colour = factor(x))) +
geom_pointrangeh(aes(xmin = x - 1, xmax = x + 1))

Following up #eipi10's suggestion to use grid functions to edit the grobs - the relevant grobs are segments. There are two possibilities: 1) rotate the segment grobs; or 2) edit the x and y coordinates of the endpoints of the segment grobs.
library(ggplot2)
library(Hmisc)
library(grid)
p = ggplot(mtcars,aes(x=gear,y=cyl)) +
stat_summary(aes(color=as.factor(rep(1:2,16))),
fun.data=mean_cl_boot, position=position_dodge(0.4)) +
coord_flip()
g = ggplotGrob(p)
# Get names of segment grobs
grid.ls(grid.force(g))$name # "GRID.segments"
# Check the structure of the segment grobs
str(getGrob(grid.force(g), gPath("GRID.segments"), grep = TRUE, global = TRUE))
# Edit the segment grobs using the editGrob() function
# 1) Rotate the segments
g <- editGrob(grid.force(g), gPath("GRID.segments"), grep = TRUE, global = TRUE,
vp = viewport(angle = 90))
# 2) set end points of segments
# g <- editGrob(grid.force(g), gPath("GRID.segments"), grep = TRUE, global = TRUE,
# x0 = unit(0.1, "npc"), y0 = unit(0.5, "npc"), x1 = unit(0.9, "npc"), y1 = unit(0.5, "npc"))
# Draw it
grid.newpage()
grid.draw(g)

Edited from: https://gist.github.com/grantmcdermott/d86af2b8f21f4082595c0e717eea5a90
The main point is to use geom_pointrangeh from ggstance and remember to specify aes w.r.t. x-axis.
library(tidyverse)
library(broom)
library(hrbrthemes)
library('ggstance')
library('jtools')
df =
mtcars %>%
mutate(vs = factor(vs), am = factor(am))
fit1 = lm(mpg ~ vs * am * wt, data = df)
fit1_coefs = tidy(fit1, conf.int = T)
fit2 = lm(mpg ~ vs / am / wt, data = df)
fit2_coefs = tidy(fit2, conf.int = T)
bind_rows(
fit1_coefs %>% mutate(model = "Model 1"),
fit2_coefs %>% mutate(model = "Model 2")
) %>%
filter(grepl("wt", term)) %>%
## Optional regexp work to make plot look nicier
mutate(
am = ifelse(grepl("am1", term), "Automatic", "Manual"),
vs = ifelse(grepl("vs1", term), "V-shaped", "Straight"),
x_lab = paste(am, vs, sep="\n")
) %>%
ggplot(aes(col = model,y=x_lab, x=estimate, xmin=conf.low, xmax=conf.high)) +
geom_pointrangeh(position = position_dodge(width = 0.5)) +
guides(color = guide_legend(reverse = TRUE)) +
geom_vline(xintercept = 0, col = "black",lty=4) +
labs(x = NULL, y = NULL,title = "Title") +
theme_nice() +
theme(plot.title = element_text(hjust = 0.5))

Related

How do I make the lines in a ggplot legend horizontal? [duplicate]

Consider for example this plot using the data mtcars and the function coord_flip
library(ggplot2)
library(Hmisc)
ggplot(mtcars,aes(x=gear,y=cyl)) + stat_summary(aes(color=as.factor(rep(1:2,16))),
fun.data=mean_cl_boot, position=position_dodge(0.4)) + coord_flip()
The fact that error bars are horizontal on the graph but vertical in the legend bothers me :) How can I rotate these symbols?
Tweak the legend key
GeomPointrange$draw_key <- function (data, params, size) {
draw_key_vpath <- function (data, params, size) {
# only need to change the x&y coords so that the line is horizontal
# originally, the vertical line was `0.5, 0.1, 0.5, 0.9`
segmentsGrob(0.1, 0.5, 0.9, 0.5,
gp = gpar(col = alpha(data$colour, data$alpha),
lwd = data$size * .pt, lty = data$linetype,
lineend = "butt"), arrow = params$arrow)
}
grobTree(draw_key_vpath(data, params, size),
draw_key_point(transform(data, size = data$size * 4), params))
}
Then plot
ggplot(mtcars,aes(x=gear,y=cyl)) +
stat_summary(aes(color=as.factor(rep(1:2,16))),
fun.data=mean_cl_boot, position=position_dodge(0.4)) +
coord_flip()
I'm not coming up with an answer that works within the normal ggplot2 workflow, so for now, here's a hacky answer. Turn off the stat_summary legend. Then, add point and line geoms with data that is outside the range of the actual data you want to plot. This will create the point and horizontal line legend that you want. Then set the plot axis limits to include only the range of your real data, so that the fake data points are not visible.
ggplot(mtcars, aes(x=gear, y=cyl, color=as.factor(rep(1:2,16)))) +
stat_summary(fun.data=mean_cl_boot, position=position_dodge(0.4), show.legend=FALSE) +
geom_line(aes(y=cyl-100)) +
geom_point(aes(y=cyl-100), size=2.5) +
coord_flip(ylim=range(mtcars$cyl))
Another option would be to rotate the legend-key grobs by 90 degrees using grid functions, but I'll leave that for someone who's more skilled with grid than I am.
The ggstance package provides an easy to implement solution here:
library(ggplot2)
library(ggstance)
ggplot(mtcars,aes(x=cyl,y=gear)) + stat_summaryh(aes(color=as.factor(rep(1:2,16))),
fun.data=mean_cl_boot_h, position = position_dodgev(height = 0.4))
or as a geom:
df <- data.frame(x = 1:3, y = 1:3)
ggplot(df, aes(x, y, colour = factor(x))) +
geom_pointrangeh(aes(xmin = x - 1, xmax = x + 1))
Following up #eipi10's suggestion to use grid functions to edit the grobs - the relevant grobs are segments. There are two possibilities: 1) rotate the segment grobs; or 2) edit the x and y coordinates of the endpoints of the segment grobs.
library(ggplot2)
library(Hmisc)
library(grid)
p = ggplot(mtcars,aes(x=gear,y=cyl)) +
stat_summary(aes(color=as.factor(rep(1:2,16))),
fun.data=mean_cl_boot, position=position_dodge(0.4)) +
coord_flip()
g = ggplotGrob(p)
# Get names of segment grobs
grid.ls(grid.force(g))$name # "GRID.segments"
# Check the structure of the segment grobs
str(getGrob(grid.force(g), gPath("GRID.segments"), grep = TRUE, global = TRUE))
# Edit the segment grobs using the editGrob() function
# 1) Rotate the segments
g <- editGrob(grid.force(g), gPath("GRID.segments"), grep = TRUE, global = TRUE,
vp = viewport(angle = 90))
# 2) set end points of segments
# g <- editGrob(grid.force(g), gPath("GRID.segments"), grep = TRUE, global = TRUE,
# x0 = unit(0.1, "npc"), y0 = unit(0.5, "npc"), x1 = unit(0.9, "npc"), y1 = unit(0.5, "npc"))
# Draw it
grid.newpage()
grid.draw(g)
Edited from: https://gist.github.com/grantmcdermott/d86af2b8f21f4082595c0e717eea5a90
The main point is to use geom_pointrangeh from ggstance and remember to specify aes w.r.t. x-axis.
library(tidyverse)
library(broom)
library(hrbrthemes)
library('ggstance')
library('jtools')
df =
mtcars %>%
mutate(vs = factor(vs), am = factor(am))
fit1 = lm(mpg ~ vs * am * wt, data = df)
fit1_coefs = tidy(fit1, conf.int = T)
fit2 = lm(mpg ~ vs / am / wt, data = df)
fit2_coefs = tidy(fit2, conf.int = T)
bind_rows(
fit1_coefs %>% mutate(model = "Model 1"),
fit2_coefs %>% mutate(model = "Model 2")
) %>%
filter(grepl("wt", term)) %>%
## Optional regexp work to make plot look nicier
mutate(
am = ifelse(grepl("am1", term), "Automatic", "Manual"),
vs = ifelse(grepl("vs1", term), "V-shaped", "Straight"),
x_lab = paste(am, vs, sep="\n")
) %>%
ggplot(aes(col = model,y=x_lab, x=estimate, xmin=conf.low, xmax=conf.high)) +
geom_pointrangeh(position = position_dodge(width = 0.5)) +
guides(color = guide_legend(reverse = TRUE)) +
geom_vline(xintercept = 0, col = "black",lty=4) +
labs(x = NULL, y = NULL,title = "Title") +
theme_nice() +
theme(plot.title = element_text(hjust = 0.5))

How to add an histogram or density plot on the right hand side of this example plot to describe the distribution of y-values?

To make it clear, I am looking for a simple way of adding a 90-degree-rotated histogram or density plot whose x-axis aligns with the y-axis of the example plot given below.
library(ggplot2)
library(tibble)
x <- seq(100)
y <- rnorm(100)
my_data <- tibble(x = x, y = y)
ggplot(data = my_data, mapping = aes(x = x, y = y)) +
geom_line()
Created on 2019-01-28 by the reprex package (v0.2.1)
I'd try it with either geom_histogram or geom_density, the patchwork library, and dynamically setting limits to match the plots.
Rather than manually setting limits, get the range of y-values, set that as the limits in scale_y_continuous or scale_x_continuous as appropriate, and add some padding with expand_scale. The first plot is the line plot, and the second and third are distribution plots, with the axes flipped. All have the scales set to match.
library(ggplot2)
library(tibble)
library(patchwork)
y_range <- range(my_data$y)
p1 <- ggplot(data = my_data, mapping = aes(x = x, y = y)) +
geom_line() +
scale_y_continuous(limits = y_range, expand = expand_scale(mult = 0.1))
p2_hist <- ggplot(my_data, aes(x = y)) +
geom_histogram(binwidth = 0.2) +
coord_flip() +
scale_x_continuous(limits = y_range, expand = expand_scale(mult = 0.1))
p2_dens <- ggplot(my_data, aes(x = y)) +
geom_density() +
coord_flip() +
scale_x_continuous(limits = y_range, expand = expand_scale(mult = 0.1))
patchwork allows you to simply add plots to each other, then add the plot_layout function where you can customize the layout.
p1 + p2_hist + plot_layout(nrow = 1)
p1 + p2_dens + plot_layout(nrow = 1)
I've generally seen these types of plots where the distribution is shown in a "marginal" plot—that is, setup to be secondary to the main (in this case, line) plot. The ggExtra package has a marginal plot, but it only seems to work where the main plot is a scatterplot.
To do this styling manually, I'm setting theme arguments on each plot inline as I pass them to plot_layout. I took off the axis markings from the histogram so its left side is clean, and shrunk the margins on the sides of the two plots that meet. In plot_layout, I'm scaling the widths so the histogram appears more in the margins of the line chart. The same could be done with the density plot.
(p1 +
theme(plot.margin = margin(r = 0, unit = "pt"))
) +
(p2_hist +
theme(axis.text.y = element_blank(),
axis.ticks.y = element_blank(),
axis.title.y = element_blank(),
plot.margin = margin(l = 0, unit = "pt"))
) +
plot_layout(nrow = 1, widths = c(1, 0.2))
Created on 2019-01-28 by the reprex package (v0.2.1)
You can try using geom_histogram or geom_density, however it's a little bit complicated as you have to rotate axis for them (while keeping original orientation for geom_line). I would use geom_violin (which is a density plot, but mirrored). If you want to get only one sided violin plot you can use custom geom_flat_violin geom. It was first posted by #David Robinson on his gists.
I used this geom in different answer, however I don't think that it's a duplicate as you need to put it at the end of the plot and combine with different geom.
Final code is:
library(ggplot2)
ggplot(data.frame(x = seq(100), y = rnorm(100))) +
geom_flat_violin(aes(100, y), color = "red", fill = "red", alpha = 0.5, width = 10) +
geom_line(aes(x, y))
geom_flat_violin code:
library(dplyr)
"%||%" <- function(a, b) {
if (!is.null(a)) a else b
}
geom_flat_violin <- function(mapping = NULL, data = NULL, stat = "ydensity",
position = "dodge", trim = TRUE, scale = "area",
show.legend = NA, inherit.aes = TRUE, ...) {
layer(
data = data,
mapping = mapping,
stat = stat,
geom = GeomFlatViolin,
position = position,
show.legend = show.legend,
inherit.aes = inherit.aes,
params = list(
trim = trim,
scale = scale,
...
)
)
}
GeomFlatViolin <-
ggproto(
"GeomFlatViolin",
Geom,
setup_data = function(data, params) {
data$width <- data$width %||%
params$width %||% (resolution(data$x, FALSE) * 0.9)
# ymin, ymax, xmin, and xmax define the bounding rectangle for each group
data %>%
dplyr::group_by(.data = ., group) %>%
dplyr::mutate(
.data = .,
ymin = min(y),
ymax = max(y),
xmin = x,
xmax = x + width / 2
)
},
draw_group = function(data, panel_scales, coord)
{
# Find the points for the line to go all the way around
data <- base::transform(data,
xminv = x,
xmaxv = x + violinwidth * (xmax - x))
# Make sure it's sorted properly to draw the outline
newdata <-
base::rbind(
dplyr::arrange(.data = base::transform(data, x = xminv), y),
dplyr::arrange(.data = base::transform(data, x = xmaxv), -y)
)
# Close the polygon: set first and last point the same
# Needed for coord_polar and such
newdata <- rbind(newdata, newdata[1,])
ggplot2:::ggname("geom_flat_violin",
GeomPolygon$draw_panel(newdata, panel_scales, coord))
},
draw_key = draw_key_polygon,
default_aes = ggplot2::aes(
weight = 1,
colour = "grey20",
fill = "white",
size = 0.5,
alpha = NA,
linetype = "solid"
),
required_aes = c("x", "y")
)
You could use egg::ggarrange(). So basically what you want is this:
p <- ggplot(data=my_data, mapping=aes(x=x, y=y)) +
geom_line() + ylim(c(-2, 2))
q <- ggplot(data=my_data, mapping=aes(x=y)) +
geom_histogram(binwidth=.05) + coord_flip() + xlim(c(-2, 2))
egg::ggarrange(p, q, nrow=1)
Result
Data
set.seed(42)
my_data <- data.frame(x=seq(100), rnorm(100))
my_data1 <- count(my_data, vars=c("y"))
p1 <- ggplot(data = my_data, mapping = aes(x = x, y = y)) + geom_line()
p2 <- ggplot(my_data1,aes(x=freq,y=y))+geom_line()+theme(axis.title.y = element_blank(),axis.text.y = element_blank())
grid.draw(cbind(ggplotGrob(p1), ggplotGrob(p2), size = "last"))

using y-axis values to create secondary x-axis in ggplot2

I would like to create a dot plot with percentiles, which looks something like this-
Here is the ggplot2 code I used to create the dot plot. There are two things I'd like to change:
I can plot the percentile values on the y-axis but I want these
values on the x-axis (as shown in the graph above). Note that
the coordinates are flipped.
The axes don't display label for the
minimum value (for example the percentile axis labels start at 25
when they should start at 0 instead.)
# loading needed libraries
library(tidyverse)
library(ggstatsplot)
# creating dataframe with mean mileage per manufacturer
cty_mpg <- ggplot2::mpg %>%
dplyr::group_by(.data = ., manufacturer) %>%
dplyr::summarise(.data = ., mileage = mean(cty, na.rm = TRUE)) %>%
dplyr::rename(.data = ., make = manufacturer) %>%
dplyr::arrange(.data = ., mileage) %>%
dplyr::mutate(.data = ., make = factor(x = make, levels = .$make)) %>%
dplyr::mutate(
.data = .,
percent_rank = (trunc(rank(mileage)) / length(mileage)) * 100
) %>%
tibble::as_data_frame(x = .)
# plot
ggplot2::ggplot(data = cty_mpg, mapping = ggplot2::aes(x = make, y = mileage)) +
ggplot2::geom_point(col = "tomato2", size = 3) + # Draw points
ggplot2::geom_segment(
mapping = ggplot2::aes(
x = make,
xend = make,
y = min(mileage),
yend = max(mileage)
),
linetype = "dashed",
size = 0.1
) + # Draw dashed lines
ggplot2::scale_y_continuous(sec.axis = ggplot2::sec_axis(trans = ~(trunc(rank(.)) / length(.)) * 100, name = "percentile")) +
ggplot2::coord_flip() +
ggplot2::labs(
title = "City mileage by car manufacturer",
subtitle = "Dot plot",
caption = "source: mpg dataset in ggplot2"
) +
ggstatsplot::theme_ggstatsplot()
Created on 2018-08-17 by the reprex package (v0.2.0.9000).
I am not 100% sure to have understood what you really want, but below is my attempt to reproduce the first picture with mpg data:
require(ggplot2)
data <- aggregate(cty~manufacturer, mpg, FUN = mean)
data <- data.frame(data[order(data$cty), ], rank=1:nrow(data))
g <- ggplot(data, aes(y = rank, x = cty))
g <- g + geom_point(size = 2)
g <- g + scale_y_continuous(name = "Manufacturer", labels = data$manufacturer, breaks = data$rank,
sec.axis = dup_axis(name = element_blank(),
breaks = seq(1, nrow(data), (nrow(data)-1)/4),
labels = 25 * 0:4))
g <- g + scale_x_continuous(name = "Mileage", limits = c(10, 25),
sec.axis = dup_axis(name = element_blank()))
g <- g + theme_classic()
g <- g + theme(panel.grid.major.y = element_line(color = "black", linetype = "dotted"))
print(g)
That produces:
data <- aggregate(cty~manufacturer, mpg, FUN = mean)
data <- data.frame(data[order(data$cty), ], rank=1:nrow(data))
These two lines generate the data for the graph. Basically we need the manufacturers, the mileage (average of cty by manufacturer) and the rank.
g <- g + scale_y_continuous(name = "Manufacturer", labels = data$manufacturer, breaks = data$rank,
sec.axis = dup_axis(name = element_blank(),
breaks = seq(1, nrow(data), (nrow(data)-1)/4),
labels = 25 * 0:4))
Note that here the scale is using rank and not the column manufacturer. To display the name of the manufacturers, you must use the labels property and you must force the breaks to be for every values (see property breaks).
The second y-axis is generated using the sec.axis property. This is very straight-forward using dup_axis that easily duplicate the axis. By replacing the labels and the breaks, you can display the %-value.
g <- g + theme(panel.grid.major.y = element_line(color = "black", linetype = "dotted"))
The horizontal lines are just the major grid. This is much easier to manipulate than geom_segments in my opinion.
Regarding your question 1, you can flip the coordinates easily using coord_flip, with minor adjustments. Replace the following line:
g <- g + theme(panel.grid.major.y = element_line(color = "black", linetype = "dotted")
By the following two lines:
g <- g + coord_flip()
g <- g + theme(panel.grid.major.x = element_line(color = "black", linetype = "dotted"),
axis.text.x = element_text(angle = 90, hjust = 1))
Which produces:
Regarding your question 2, the problem is that the value 0% is outside the limits. You can solve this issue by changing the way you calculate the percentage (starting from zero and not from one), or you can extend the limit of your plot to include the value zero, but then no point will be associated to 0%.

How to prevent ggplot from clipping points that are out of bounds

I am using the following code to try to preserve geom elements that are out of bounds of the plotting area but it still seems to be clipping them beyond a certain distance above the plotting area.
g <- ggplot(iris, aes(x = Species, y = Petal.Length)) +
stat_summary(geom = 'bar', fun.y = mean) +
geom_point() +
scale_y_continuous(limits = c(0,8), expand = c(0,0), oob = function(x, ...) x) +
geom_text(label = 'obText', aes(x = 2, y = 9)) #+
# theme(plot.margin = unit(c(60,5.5,5.5,5.5), "points"),
# aspect.ratio = 1)
gb <- suppressWarnings(ggplot_build(g))
gt <- ggplot_gtable(gb)
gt$layout$clip[gt$layout$name=="panel"] <- "off"
grid::grid.newpage()
grid::grid.draw(gt)
Any ideas on why this is and how to correct it? If I uncomment the theme argument, I can get close to what I want but this changes the aspect ratio of the plotting area.
Not sure if this is what you're looking for but you can use clip = 'off' option in ggplot 3.0.0 to get the text show up
See also this answer for more information
# install.packages("devtools")
# devtools::install_github("tidyverse/ggplot2")
library(ggplot2)
g <- ggplot(iris, aes(x = Species, y = Petal.Length)) +
stat_summary(geom = 'bar', fun.y = mean) +
geom_point() +
scale_y_continuous(limits = c(0,8), expand = c(0,0), oob = function(x, ...) x) +
geom_text(label = 'obText', aes(x = 2, y = 9), check_overlap = TRUE) +
# this will allow the text outside of the plot panel
coord_cartesian(clip = 'off') +
theme(plot.margin = margin(4, 2, 2, 2, "cm"))
g
Created on 2018-06-28 by the reprex package (v0.2.0.9000).
If you want to see the points, you can change the value of oob =...
oob = function(x, ...) x
oob = squish
oob = censor
squish and censor are part of the scales package.
Note that the mean changes in both cases; squish lowers the value of the points above 6, and censor removes the points above 6.

It is possible to create inset graphs?

I know that when you use par( fig=c( ... ), new=T ), you can create inset graphs. However, I was wondering if it is possible to use ggplot2 library to create 'inset' graphs.
UPDATE 1: I tried using the par() with ggplot2, but it does not work.
UPDATE 2: I found a working solution at ggplot2 GoogleGroups using grid::viewport().
Section 8.4 of the book explains how to do this. The trick is to use the grid package's viewports.
#Any old plot
a_plot <- ggplot(cars, aes(speed, dist)) + geom_line()
#A viewport taking up a fraction of the plot area
vp <- viewport(width = 0.4, height = 0.4, x = 0.8, y = 0.2)
#Just draw the plot twice
png("test.png")
print(a_plot)
print(a_plot, vp = vp)
dev.off()
Much simpler solution utilizing ggplot2 and egg. Most importantly this solution works with ggsave.
library(ggplot2)
library(egg)
plotx <- ggplot(mpg, aes(displ, hwy)) + geom_point()
plotx +
annotation_custom(
ggplotGrob(plotx),
xmin = 5, xmax = 7, ymin = 30, ymax = 44
)
ggsave(filename = "inset-plot.png")
Alternatively, can use the cowplot R package by Claus O. Wilke (cowplot is a powerful extension of ggplot2). The author has an example about plotting an inset inside a larger graph in this intro vignette. Here is some adapted code:
library(cowplot)
main.plot <-
ggplot(data = mpg, aes(x = cty, y = hwy, colour = factor(cyl))) +
geom_point(size = 2.5)
inset.plot <- main.plot + theme(legend.position = "none")
plot.with.inset <-
ggdraw() +
draw_plot(main.plot) +
draw_plot(inset.plot, x = 0.07, y = .7, width = .3, height = .3)
# Can save the plot with ggsave()
ggsave(filename = "plot.with.inset.png",
plot = plot.with.inset,
width = 17,
height = 12,
units = "cm",
dpi = 300)
I prefer solutions that work with ggsave. After a lot of googling around I ended up with this (which is a general formula for positioning and sizing the plot that you insert.
library(tidyverse)
plot1 = qplot(1.00*mpg, 1.00*wt, data=mtcars) # Make sure x and y values are floating values in plot 1
plot2 = qplot(hp, cyl, data=mtcars)
plot(plot1)
# Specify position of plot2 (in percentages of plot1)
# This is in the top left and 25% width and 25% height
xleft = 0.05
xright = 0.30
ybottom = 0.70
ytop = 0.95
# Calculate position in plot1 coordinates
# Extract x and y values from plot1
l1 = ggplot_build(plot1)
x1 = l1$layout$panel_ranges[[1]]$x.range[1]
x2 = l1$layout$panel_ranges[[1]]$x.range[2]
y1 = l1$layout$panel_ranges[[1]]$y.range[1]
y2 = l1$layout$panel_ranges[[1]]$y.range[2]
xdif = x2-x1
ydif = y2-y1
xmin = x1 + (xleft*xdif)
xmax = x1 + (xright*xdif)
ymin = y1 + (ybottom*ydif)
ymax = y1 + (ytop*ydif)
# Get plot2 and make grob
g2 = ggplotGrob(plot2)
plot3 = plot1 + annotation_custom(grob = g2, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax)
plot(plot3)
ggsave(filename = "test.png", plot = plot3)
# Try and make a weird combination of plots
g1 <- ggplotGrob(plot1)
g2 <- ggplotGrob(plot2)
g3 <- ggplotGrob(plot3)
library(gridExtra)
library(grid)
t1 = arrangeGrob(g1,ncol=1, left = textGrob("A", y = 1, vjust=1, gp=gpar(fontsize=20)))
t2 = arrangeGrob(g2,ncol=1, left = textGrob("B", y = 1, vjust=1, gp=gpar(fontsize=20)))
t3 = arrangeGrob(g3,ncol=1, left = textGrob("C", y = 1, vjust=1, gp=gpar(fontsize=20)))
final = arrangeGrob(t1,t2,t3, layout_matrix = cbind(c(1,2), c(3,3)))
grid.arrange(final)
ggsave(filename = "test2.png", plot = final)
'ggplot2' >= 3.0.0 makes possible new approaches for adding insets, as now tibble objects containing lists as member columns can be passed as data. The objects in the list column can be even whole ggplots... The latest version of my package 'ggpmisc' provides geom_plot(), geom_table() and geom_grob(), and also versions that use npc units instead of native data units for locating the insets. These geoms can add multiple insets per call and obey faceting, which annotation_custom() does not. I copy the example from the help page, which adds an inset with a zoom-in detail of the main plot as an inset.
library(tibble)
library(ggpmisc)
p <-
ggplot(data = mtcars, mapping = aes(wt, mpg)) +
geom_point()
df <- tibble(x = 0.01, y = 0.01,
plot = list(p +
coord_cartesian(xlim = c(3, 4),
ylim = c(13, 16)) +
labs(x = NULL, y = NULL) +
theme_bw(10)))
p +
expand_limits(x = 0, y = 0) +
geom_plot_npc(data = df, aes(npcx = x, npcy = y, label = plot))
Or a barplot as inset, taken from the package vignette.
library(tibble)
library(ggpmisc)
p <- ggplot(mpg, aes(factor(cyl), hwy, fill = factor(cyl))) +
stat_summary(geom = "col", fun.y = mean, width = 2/3) +
labs(x = "Number of cylinders", y = NULL, title = "Means") +
scale_fill_discrete(guide = FALSE)
data.tb <- tibble(x = 7, y = 44,
plot = list(p +
theme_bw(8)))
ggplot(mpg, aes(displ, hwy, colour = factor(cyl))) +
geom_plot(data = data.tb, aes(x, y, label = plot)) +
geom_point() +
labs(x = "Engine displacement (l)", y = "Fuel use efficiency (MPG)",
colour = "Engine cylinders\n(number)") +
theme_bw()
The next example shows how to add different inset plots to different panels in a faceted plot. The next example uses the same example data after splitting it according to the century. This particular data set once split adds the problem of one missing level in one of the inset plots. As these plots are built on their own we need to use manual scales to make sure the colors and fill are consistent across the plots. With other data sets this may not be needed.
library(tibble)
library(ggpmisc)
my.mpg <- mpg
my.mpg$century <- factor(ifelse(my.mpg$year < 2000, "XX", "XXI"))
my.mpg$cyl.f <- factor(my.mpg$cyl)
my_scale_fill <- scale_fill_manual(guide = FALSE,
values = c("red", "orange", "darkgreen", "blue"),
breaks = levels(my.mpg$cyl.f))
p1 <- ggplot(subset(my.mpg, century == "XX"),
aes(factor(cyl), hwy, fill = cyl.f)) +
stat_summary(geom = "col", fun = mean, width = 2/3) +
labs(x = "Number of cylinders", y = NULL, title = "Means") +
my_scale_fill
p2 <- ggplot(subset(my.mpg, century == "XXI"),
aes(factor(cyl), hwy, fill = cyl.f)) +
stat_summary(geom = "col", fun = mean, width = 2/3) +
labs(x = "Number of cylinders", y = NULL, title = "Means") +
my_scale_fill
data.tb <- tibble(x = c(7, 7),
y = c(44, 44),
century = factor(c("XX", "XXI")),
plot = list(p1, p2))
ggplot() +
geom_plot(data = data.tb, aes(x, y, label = plot)) +
geom_point(data = my.mpg, aes(displ, hwy, colour = cyl.f)) +
labs(x = "Engine displacement (l)", y = "Fuel use efficiency (MPG)",
colour = "Engine cylinders\n(number)") +
scale_colour_manual(guide = FALSE,
values = c("red", "orange", "darkgreen", "blue"),
breaks = levels(my.mpg$cyl.f)) +
facet_wrap(~century, ncol = 1)
In 2019, the patchwork package entered the stage, with which you can create
insets
easily by using the inset_element() function:
require(ggplot2)
require(patchwork)
gg1 = ggplot(iris, aes(Sepal.Length, Sepal.Width)) +
geom_point()
gg2 = ggplot(iris, aes(Sepal.Length)) +
geom_density()
gg1 +
inset_element(gg2, left = 0.65, bottom = 0.75, right = 1, top = 1)

Resources