37

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().

zx8754
  • 52,746
  • 12
  • 114
  • 209
Sam
  • 7,922
  • 16
  • 47
  • 62
  • 1
    This [post](http://learnr.wordpress.com/2009/05/08/ggplot2-plot-inside-a-plot/) on the Learning R blog goes over how to plot inside a plot. The blog has a lot of other great posts on ggplot2. – Erik Shilts Mar 07 '11 at 15:03

6 Answers6

27

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()
Jessica Burnett
  • 395
  • 1
  • 13
Richie Cotton
  • 118,240
  • 47
  • 247
  • 360
  • 3
    Worth pointing out that, if one wants another plot as the inset, the crucial line is to be `print(another_plot, vp = vp)`. It took me a while to figure out. +1 – mts Apr 09 '17 at 20:44
23

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")
andschar
  • 3,504
  • 2
  • 27
  • 35
stackinator
  • 5,429
  • 8
  • 43
  • 84
  • 10
    Hey @stackinator. +1 for concise example and mentioning the `egg` package. Note that just using `library(ggplot2)` is more than enough (one does not need the extra `tidyverse` nor `egg`) – Valentin_Ștefan Aug 14 '18 at 19:26
  • Absolutely right, @valentin. Changed the tidyverse dependency to ggplot2. – andschar Aug 13 '21 at 08:43
22

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)

enter image description here

Valentin_Ștefan
  • 6,130
  • 2
  • 45
  • 68
  • I think you can also pass `main.plot` to `ggdraw()` to save one line of code. – crsh May 03 '19 at 10:27
  • 1
    @Skiea, yes, of course, just add a new `draw_plot()` every time you need a new inset graph – Valentin_Ștefan Feb 20 '20 at 14:06
  • @crsh, not sure what you mean, but I prefer to have the `main.plot` created separately as also `inset.plot`, fallowed "naturally" by `plot.with.inset` in the above example (naming is not great though). Here are my thoughts about this: I think the point here (and in general) is to have code as familiar as possible for the general reader (impacts readability, understanding and debugging). So, I believe that less syntax can actually hider understanding sometimes. Yet, familiarity is highly subjective and I often become confusing for others and my future self :/ – Valentin_Ștefan Feb 21 '20 at 10:14
16

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)

Image showing inset and relatively complex layout

pallevillesen
  • 735
  • 6
  • 14
  • 2
    I think they've updated the ggplot package and now, in order to extract the position from the plot1 coordinates the format should be like this: `l1$layout$panel_ranges[[1]]$x.range[1]`. Note the `l1$layout$panel_ranges....` – s_scolary Dec 04 '17 at 21:39
  • 1
    You are right. I have updated the answer accordingly. – pallevillesen Dec 14 '17 at 09:42
16

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

ggplot with ggplot as inset

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

enter image description here

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)

enter image description here

Pedro J. Aphalo
  • 5,796
  • 1
  • 22
  • 23
  • +1 It's the only solution that worked for me when 1) using `hms` / `difftime` objects on one dimension and 2) wanted a solution to combine the plot with inset with another plot (using the [patchwork](https://cran.r-project.org/web/packages/patchwork) package in my case). I had errors with the `rescale` function being unable to handle `difftime` objects with the other solutions. Also, the inset dimension can be adjusted using arguments `vp.width` and `vp.height`. Thanks for the [ggpmisc](https://cran.r-project.org/web/packages/ggpmisc) package. – Pascal Martin May 13 '20 at 22:20
  • Your answer helped me a lot for a similar problem, so thak you for that! But unfortunately I don't understand what you mean by "can add multiple insets per call and obey faceting" - I wish it did but I can't get it to work. Right now I have a plot that I split in four via faceting. But when I try to add inlays, I get either an inlay with all four facets in every facet or, if I build the inlays before facetin, the same inlay in all four facets showing all data. Any idea what I could change? Thank you :) – Apatura Jun 23 '20 at 14:37
  • @Apatura The easiest way is to imagine that the inset plots are like say text strings mapped to the label aesthetics, and this is the reason why I reused "label". If you want to have four inset plots then you need to have each one in a different row of the tibble. If you want to have them in specific panels, then this tibble needs to include a column with a factor with the same name and levels as used for the faceting. The level of the factor in the corresponding row determines to which panel each inset plot is added. You are not limited to one inset plot per panel, it simply driven by data. – Pedro J. Aphalo Jun 24 '20 at 18:24
  • Thank you for your fast reply! Do you mean something like fourinsets <- tibble ( x=0, y= 10, plot = list (plot_fourfacets$1 plot_fourfacets$2))) ? Or do I do four lines with plot = etc? Also, what do you mean by including a column? And is it correct that I can do the faceting first, then extract inlays from the faceted plot into a tibble and than add that tibble to the facets? -- maybe I should do another question. – Apatura Jun 25 '20 at 15:05
  • https://stackoverflow.com/questions/62579733/produce-an-inset-in-each-facet-of-a-plot – Apatura Jun 25 '20 at 16:14
  • Please,see above. I have added an example with two panels, each with a different inset. This example is for only two panels to keep is short but should give an idea of what I meant in my comment above. – Pedro J. Aphalo Jun 26 '20 at 20:32
7

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)

enter image description here

andschar
  • 3,504
  • 2
  • 27
  • 35