It is possible to create inset graphs?
Asked Answered
D

6

39

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

Dissatisfy answered 7/3, 2011 at 12:21 Comment(1)
This post on the Learning R blog goes over how to plot inside a plot. The blog has a lot of other great posts on ggplot2.Farlay
N
28

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()
Nammu answered 7/3, 2011 at 14:6 Comment(1)
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. +1Pricket
D
25

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")
Defendant answered 15/6, 2018 at 13:30 Comment(2)
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)Runesmith
Absolutely right, @valentin. Changed the tidyverse dependency to ggplot2.Hypoxia
H
23

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

Herod answered 16/10, 2018 at 18:21 Comment(3)
I think you can also pass main.plot to ggdraw() to save one line of code.Docker
@Skiea, yes, of course, just add a new draw_plot() every time you need a new inset graphRunesmith
@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 :/Runesmith
J
17

'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

Johnathan answered 11/4, 2019 at 14:45 Comment(6)
+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 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 package.Noetic
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 :)Hanseatic
@Hanseatic 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.Johnathan
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.Hanseatic
#62580233Hanseatic
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.Johnathan
C
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

Charentemaritime answered 16/6, 2016 at 11:15 Comment(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....Prolamine
You are right. I have updated the answer accordingly.Charentemaritime
H
10

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

Hypoxia answered 13/8, 2021 at 8:40 Comment(1)
Adding a comment here to say that this approach worked best for me when I had a plot with several facets, but only a few facets had an inset plot. I had to specify the location individually for each facet, so it was a bit time consuming, but it was the only option that I could get to work.Evangelical

© 2022 - 2024 — McMap. All rights reserved.