Plotception with {ggpattern}

dataviz
ggpattern
ggplot2
r
Author

Matt Dray

Published

April 5, 2020

tl;dr

The {ggpattern} package lets you add pattern fills to your {ggplot2} plot… so I did the inevitable.

Yo dawg

Followers of this blog might remember the unveiling of cloud_pie(), the greatest new visualisation technique of the 21st Century.

Luckily, R-package machine mikefc of @coolbutuseless has released {ggpattern}, which lets you image- or pattern-fill the bars of your {ggplot2} plot. Most usefully with pictures of kittens or Bill Murray.

This has opened the door to yet another ground-breaking viz. The secret yet obvious real purpose of {ggpattern} is to put plots inside your plots.1

Voilà.

A bar lot where each bar is also a plot.

Shield the eyes of any younglings.

It may be too much to take in; let me explain. The main plot is of car weight by the number of cylinders from the mtcars dataset.

But lo, each bar is itself a plot of weight by gears for the number of cylinders on the x-axis of the main plot. The most efficient plot of all time?

Please clean your blown mind from the ceilling before you leave, thank you.

The secret

Expand for the full, hacky, non-reproducible code used to create this masterpiece.
# Plot weight by cylinders, with weight by gear inside
# Using {ggpattern} by @coolbutuseless
# https://coolbutuseless.github.io/package/ggpattern/

# Load packages
library(dplyr)
library(purrr)
library(tidyr)
library(ggplot2)
library(ggpattern)

# Prepare mean wt per cyl with filenames for gear-wt plots
cyl_weights <- mtcars %>% group_by(cyl) %>% summarise(mean_wt = mean(wt))

# See the pixel dimensions of the bars
# This will depend on the size of your plot; I just used the default 7 x 7
dummy_plot <- ggplot(cyl_weights, aes(as.character(cyl), mean_wt)) +
  geom_col_pattern(pattern = "placeholder", pattern_type = "dummy") +
  labs(
    title = "Weight by gears by weight by cylinders",
    caption = "Thanks to {ggpattern} by @coolbutuseless",
    x = "Cylinders", y = "Weight"
  ) +
  theme_grey(base_size = 15)

# Save the dummy plot
ggsave("~/Desktop/dummy_plot.png", dummy_plot)  # defaults to 7 x 7 output

[](resources/dummy_plot.png]{fig-alt=“Subplot of weight by gears for 4 cylinders.” width=“100%”}

# Plot of mean weight by gear for each cyl
sub_plots <- mtcars %>% 
  group_by(cyl) %>% nest() %>% ungroup() %>%  # listcol by cyl
  mutate(
    data_mean = map(data, ~group_by(., gear) %>% summarise(mean_wt = mean(wt))),
    plot = map(  # listcol of plots for each cyl
      data_mean,
      ~ggplot(., aes(as.character(gear), mean_wt)) +
        labs(title = "Weight by gears", x = "Gears", y = "Weight") +
        geom_col() +
        theme_grey(base_size = 60)  # trial and error until it looked okay
    ),
    filename = paste0(cyl, "_cyl.png")  # unique filename based on cyl value
  ) %>% 
  arrange(cyl) %>%   # in order of cy number
  mutate(  # pixel values manually added from looking at the dummy plot output!
    width = 141 * 0.084666667,  # conversion from pixels to mm given 300 dpi
    height = c(261, 357, 458) * 0.084666667
  ) %>% 
  select(filename, plot, height, width)

# Save the sub-plots as separate files wth provided dimensions
# These will be read into the main plot
pwalk(sub_plots, ggsave, path = "~/Desktop/")

Subplot of weight by gears for 8 cylinders.

# Plot weight by cylinders, with weight by gear inside
main_plot <- cyl_weights %>% 
  mutate(  # add filepaths for where to find the saved subplots
    filename = case_when(
      cyl == 4 ~ "~/Desktop/4_cyl.png",
      cyl == 6 ~ "~/Desktop/6_cyl.png",
      cyl == 8 ~ "~/Desktop/8_cyl.png",
    )
  ) %>% 
  ggplot(aes(as.character(cyl), mean_wt)) +
  geom_col_pattern(
    aes(pattern_filename = I(filename)),
    pattern = "image",
    pattern_type = "squish"
  ) +
  labs(
    title = "Weight by gears by weight by cylinders",
    caption = "Thanks to {ggpattern} by @coolbutuseless",
    x = "Cylinders", y = "Weight"
  ) +
  theme_grey(base_size = 15)

# Save plot
ggsave("~/Desktop/ggpattern_plot.png", main_plot)  # default 7 x 7 output

In short, you can use arguments pattern = "placeholder" and pattern_type = "dummy" to geom_col_pattern() to produce a plot containing pixel dimensions for each bar.

From there, you can create plots that match those dimensions. Then you can recreate your plot but this time use argument pattern = "image" and provide the filepaths as an aes()thetic.

Bless you, @coolbutuseless.

Environment

Session info
Last rendered: 2024-07-14 20:13:50 BST
R version 4.4.0 (2024-04-24)
Platform: aarch64-apple-darwin20
Running under: macOS Ventura 13.2.1

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.0

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: Europe/London
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
 [1] htmlwidgets_1.6.4 compiler_4.4.0    fastmap_1.2.0     cli_3.6.3.9000   
 [5] tools_4.4.0       htmltools_0.5.8.1 rstudioapi_0.16.0 yaml_2.3.8       
 [9] rmarkdown_2.26    knitr_1.46        jsonlite_1.8.8    xfun_0.43        
[13] digest_0.6.35     rlang_1.1.4.9000  evaluate_0.23    

Footnotes

  1. It can’t be chartjunk if it’s simply more charts, he said, after posting a tweet about it on April 1st.↩︎

Reuse

CC BY-NC-SA 4.0