Ggmagnify Save

Create a magnified inset of part of a ggplot object

Project README

ggmagnify

r-universe R-CMD-check Lifecycle:
experimental

ggmagnify creates a magnified inset of part of a ggplot object. The magnified area can be a (rounded) rectangle, an ellipse, a convex hull of points, or an arbitrary shape. Borders can be drawn around the target area and the inset, along with projection lines and/or shading between the two. The inset can have a drop shadow.

You can install ggmagnify from r-universe:

install.packages("ggmagnify", repos = c("https://hughjonesd.r-universe.dev", 
                 "https://cloud.r-project.org"))

This will install the latest github release (currently ggmagnify 0.4.0).

Or install the development version from GitHub with:

# install.packages("remotes")
remotes::install_github("hughjonesd/ggmagnify")

Basic inset

To create an inset, use geom_magnify(from, to). from can be a vector giving the four corners of the area to magnify: from = c(xmin, xmax, ymin, ymax).

Similarly, to specifies where the magnified inset should go: to = c(xmin, xmax, ymin, ymax).

library(ggplot2)
library(ggmagnify)

ggp <- ggplot(dv, aes(Position, NegLogP)) + 
  geom_point(color = "darkblue", alpha = 0.8, size = 0.8) +
  labs(title = "GWAS p-values for cognitive function",
       subtitle = "Davies et al. (2018).", y = "-log(p)")

from <- c(xmin = 9.75e7, xmax = 9.95e7, ymin = 16, ymax = 28)
# Names xmin, xmax, ymin, ymax are optional:
to <- c(2e8 - 2e7, 2e8 + 2e7,10, 26)

ggp + geom_magnify(from = from, to = to)

Inset with shadow


loadNamespace("ggfx")
#> <environment: namespace:ggfx>

ggp + geom_magnify(from = from, to = to, 
                   shadow = TRUE)

Rounded corners

New in version 0.3.0, use corners to give a proportional radius for rounded corners.

ggp + geom_magnify(from = from, to = to, 
                   corners = 0.1, shadow = TRUE)

Ellipse

This requires R 4.1 or higher, and an appropriate graphics device.


ggp + geom_magnify(from = from, to = to, 
                   shape = "ellipse", shadow = TRUE)

Pick points to magnify

To choose points to magnify, map from in an aes():

ggpi <- ggplot(iris, aes(Sepal.Width, Sepal.Length, colour = Species)) +
              geom_point() + xlim(c(1.5, 6))

ggpi + geom_magnify(aes(from = Species == "setosa" & Sepal.Length < 5), 
                    to = c(4, 6, 6, 7.5))

Faceting


ggpi +
  facet_wrap(vars(Species)) +
  geom_magnify(aes(from = Sepal.Length > 5 & Sepal.Length < 6.5), 
                    to = c(4.5, 6, 6, 7.5),
                    shadow = TRUE)

Magnify an arbitrary region (experimental)

Use shape = "outline" to magnify the convex hull of a set of points:


library(dplyr)

starwars_plot <- starwars |> 
  mutate(Human = species == "Human") |> 
  select(mass, height, Human) |> 
  na.omit() |> 
  ggplot(aes(mass, height, color = Human)) + 
    geom_point() + xlim(0, 220) + ylim(0, 250) + 
    theme_dark() +
    theme(panel.grid = element_line(linetype = 2, colour = "yellow"), 
          axis.line = element_blank(), 
          panel.background = element_rect(fill = "black"),
          legend.key = element_rect(fill= "black"),
          rect = element_rect(fill = "black"),
          text = element_text(colour = "white")) +
    scale_colour_manual(values = c("TRUE" = "red", "FALSE" = "lightblue")) +
    ggtitle("Mass and height of Star Wars characters",
            subtitle = "Humans magnified")

starwars_plot +
  geom_magnify(aes(from = Human), to = c(30, 200, 0, 120), shadow = TRUE,
               shadow.args = list(colour = "yellow", sigma = 10,
                                  x_offset = 2, y_offset = 5),
               alpha = 0.8, colour = "yellow", linewidth = 0.6, 
               shape = "outline", expand = 0.2)
#> Warning: Removed 1 rows containing missing values (`geom_point()`).

Use a grob or data frame to magnify any shape:


s <- seq(0, 2*pi, length = 7)
hex <- data.frame(x = 3 + sin(s)/2, y = 6 + cos(s)/2) 

ggpi + geom_magnify(from = hex, 
                    to = c(4, 6, 5, 7), shadow = TRUE, aspect = "fixed")

Maps (experimental)

With maps, shape = "outline" magnifies just the selected map polygons:


usa <- sf::st_as_sf(maps::map("state", fill=TRUE, plot =FALSE))

ggpm <- ggplot(usa) +
          geom_sf(aes(fill = ID == "texas"), colour = "grey20") +
          coord_sf(default_crs = sf::st_crs(4326), ylim = c(10, 50)) + 
          theme(legend.position = "none") +
          scale_fill_manual(values = c("TRUE" = "red", "FALSE" = "steelblue4"))


ggpm + geom_magnify(aes(from = ID == "texas"),
                    to = c(-125, -98, 10, 30), 
                    shadow = TRUE, linewidth = 1, colour = "orange2",
                    shape = "outline", 
                    aspect = "fixed", 
                    expand = 0) 

Axes


ggp + 
  scale_x_continuous(labels = NULL) + 
  geom_magnify(from = from, to = to, 
               axes = "xy")

Projection lines and borders

Colour and linetype


ggp + 
  geom_magnify(from = from, to = to,
               colour = "darkgreen", linewidth = 0.5, proj.linetype = 3)

Projection line styles


ggpi <- ggplot(iris, aes(Sepal.Width, Sepal.Length, colour = Species)) +
              geom_point()
from2 <- c(3, 3.5, 6.5, 7)
to2 <- c(2.75, 3.75, 5, 6)

ggpi + 
  geom_magnify(from = from2, to = to2,
               proj = "facing") # the default

ggpi + 
  geom_magnify(from = from2, to = to2,
               proj = "corresponding") # always project corner to corner

ggpi + 
  geom_magnify(from = from2, to = to2,
               proj = "single") # just one line

Projection fill


ggpi + 
  geom_magnify(from = from2, to = to2,
               proj.fill = alpha("yellow", 0.2)) # fill between the lines

ggpi + 
  geom_magnify(from = from2, to = to2, shape = "ellipse",
               proj.fill = alpha("orange", 0.2)) # works with any shape

Tips and tricks

Graphics devices

geom_magnify() uses masks. This requires R version 4.1.0 or higher, and a graphics device that supports masking. If you are using knitr, you may have luck with the ragg_png device (which was used to create this README). If your device doesn’t support masks, only shape = "rect" will work, and the plot inset will not be clipped to the panel area.

Adding layers to the inset

geom_magnify() stores the plot when it is added to it. So, order matters:


ggpi <- ggplot(iris, aes(Sepal.Width, Sepal.Length, colour = Species)) +
              geom_point() + xlim(2, 6)

from3 <-  c(2.5, 3.5, 6, 7)
to3 <- c(4.7, 6.1, 4.3, 5.7)
ggpi + 
  geom_smooth() + 
  geom_magnify(from = from3, to = to3)
#> `geom_smooth()` using method = 'loess' and formula = 'y ~ x'
#> `geom_smooth()` using method = 'loess' and formula = 'y ~ x'
# Print the inset without the smooth:
ggpi +
  geom_magnify(from = from3, to = to3) +
  geom_smooth()
#> `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

For complex modifications to the inset, set plot explicitly:


booms <- ggplot(faithfuld, aes(waiting, eruptions)) +
         geom_contour_filled(aes(z = density), bins = 50) +
         scale_fill_viridis_d(option = "B") + 
         theme(legend.position = "none")

booms_inset <- booms + 
  geom_point(data = faithful, color = "red", fill = "white", alpha = 0.7, 
             size = 2, shape = "circle filled") + 
  coord_cartesian(expand = FALSE)

shadow.args <- list(
  colour = alpha("grey80", 0.8),
  x_offset = 0,
  y_offset = 0,
  sigma = 10
)

booms + geom_magnify(from = c(78, 90, 4.0, 4.8), to = c(70, 90, 1.7, 3.3),
                     colour = "white", shape = "ellipse",
                     shadow = TRUE, shadow.args = shadow.args,
                     plot = booms_inset)

Draw an inset outside the plot region


ggp + 
  coord_cartesian(clip = "off") + 
  theme(plot.margin = ggplot2::margin(10, 60, 10, 10)) +
  geom_magnify(from = from, to = to + c(0.5e8, 0.5e8, 0, 0), 
               shadow = TRUE)

Keep grid lines the same

To make sure the inset uses the same grid lines as the main graph, set breaks in scale_x and scale_y:


ggp2 <- ggplot(iris, aes(Sepal.Width, Sepal.Length, color = Species)) + 
        geom_point() +
        theme_classic() + 
        theme(panel.grid.major = element_line("grey80"),
              panel.grid.minor = element_line("grey90"))

# different grid lines:
ggp2 + 
  geom_magnify(from = c(2.45, 3.05, 5.9, 6.6), to = c(3.4, 4.4, 5.5, 6.6),
               shadow = TRUE) 

# fix the grid lines:
ggp2 +
  scale_x_continuous(breaks = seq(2, 5, 0.5)) + 
  scale_y_continuous(breaks = seq(5, 8, 0.5)) + 
  geom_magnify(from = c(2.45, 3.05, 5.9, 6.6), to = c(3.4, 4.4, 5.5, 6.6),
               shadow = TRUE) 

Recomputing data

Use recompute if you want to recompute smoothers, densities, etc. in the inset.

 
df <- data.frame(x = seq(-5, 5, length = 500), y = 0)
df$y[abs(df$x) < 1] <- sin(df$x[abs(df$x) < 1])
df$y <- df$y + rnorm(500, mean = 0, sd = 0.25)

ggp2 <- ggplot(df, aes(x, y)) + 
  geom_point() + 
  geom_smooth(method = "loess", formula = y ~ x) + 
  ylim(-5, 5)

# The default:
ggp2 + geom_magnify(from = c(-1.25, 1.25, -1, 1),
                    to = c(2, 5, 1, 5))

# Recomputing recalculates the smooth for the inset:
ggp2 + geom_magnify(from = c(-1.25, 1.25, -1, 1),
                    to = c(2, 5, 1, 5),
                    recompute = TRUE)

Magnify twice


data <- data.frame(
  x = runif(4000), 
  y = runif(4000)
)
ggm_unif <- ggplot(data, aes(x, y)) +
            coord_cartesian(expand = FALSE) +
            geom_density2d_filled(bins = 50, linewidth = 0, n = 200) +
            geom_point(color='white', alpha = .5, size = .5) + 
            theme(legend.position = "none")


ggm_unif + 
  geom_magnify(from = c(0.05, 0.15, 0.05, 0.15), to = c(0.2, 0.4, 0.2, 0.4), 
               colour = "white", proj.linetype = 1, linewidth = 0.6) +
  geom_magnify(from = c(0.25, 0.35, 0.25, 0.35), to = c(0.45, 0.85, 0.45, 0.85), 
               expand = 0, colour ="white", proj.linetype = 1)

An inset within an inset is a bit more complex, but also doable:


ggp <- data.frame(x = rnorm(1e5), y = rnorm(1e5), 
                  colour = sample(8L, 1e5, replace = TRUE)) |> 
  ggplot(aes(x = x, y = y, colour = factor(colour))) + 
  scale_color_brewer(type = "qual", palette = 2) +
  geom_point(alpha = 0.12, size = 0.7) + 
  lims(x = c(-3,3), y = c(-3,3)) +
  theme_classic() + theme(panel.grid = element_blank(), 
                          axis.line = element_blank(), 
                          plot.background = element_rect(fill = "black"),
                          panel.background = element_rect(fill = "black"),
                          title = element_text(colour = "white"),
                          legend.position = "none")

ggpm <- ggp + 
  lims(x = c(-0.3, 0.3), y = c(-0.3, 0.3)) + 
  geom_magnify(from = c(-0.03, 0.03, -0.03, 0.03),
               to = c(-0.3, -0.1, -0.3, -0.1),
               expand = FALSE, colour = "white")
#> Scale for x is already present.
#> Adding another scale for x, which will replace the existing scale.
#> Scale for y is already present.
#> Adding another scale for y, which will replace the existing scale.

ggp + 
  geom_magnify(plot = ggpm, 
               from = c(-0.3, 0.3, -0.3, 0.3),
               to = c(-3, -1, -3, -1),
               expand = FALSE, colour = "white") +
  labs(title = "Normal data", 
       subtitle = "The distribution gets more uniform as you zoom in")
#> Warning: Removed 570 rows containing missing values (`geom_point()`).

Acknowledgements

ggmagnify was inspired by this post and motivated by making these plots.

Data for the GWAS plots comes from:

Davies et al. (2018) ‘Study of 300,486 individuals identifies 148 independent genetic loci influencing general cognitive function.’ Nature Communications.

Data was trimmed to remove overlapping points.

Open Source Agenda is not affiliated with "Ggmagnify" Project. README Source: hughjonesd/ggmagnify
Stars
264
Open Issues
4
Last Commit
1 month ago

Open Source Agenda Badge

Open Source Agenda Rating