Use images in ggplot2

ggimage lets you use images in ggplot2. An image can be a file on disk, a magick image, or a URL, and it can be used as a point glyph, as a background, or as an inset.

Every image layer is an ordinary ggplot2 layer: it takes mapping and data, it respects stat, position and inherit.aes, and it can be combined with any other layer, scale, facet or theme. The geoms in this package differ only in where the image comes from and how it is sized.

Arguments shared by the image layers

The image geoms accept the same arguments as any other ggplot2 geom, plus the ones below. Reading this list once makes the rest of the document short, since the sections that follow only describe what is specific to each geom.

Aesthetics

Layer parameters

Drawing images at data points

geom_image

set.seed(2017-02-21)
img <- list.files(system.file("extdata", package = "ggimage"),
                  pattern = "png", full.names = TRUE)
d <- data.frame(x = rnorm(10),
                y = rnorm(10),
                image = sample(img, size = 10, replace = TRUE))

## one image, used by the sections below
logo <- system.file("extdata/Rlogo.png", package = "ggimage")

ggplot(d, aes(x, y)) + geom_image(aes(image = image), size = .05)

ggplot(d, aes(x, y)) + geom_image(aes(image = image), size = .05, by = "height")

ggplot(d, aes(x, y)) + geom_image(image = d$image[1])

d$size <- seq(.05, .15, length.out = 10)
ggplot(d, aes(x, y)) + geom_image(aes(image = image, size = I(size)))

ggplot(d, aes(x, y)) + geom_image(aes(image = image), color = "firebrick")

geom_image() is the layer everything else builds on. The first plot maps image to a column, so each point gets its own picture; the second asks for the same size measured against the height of the panel instead of its width, which is the right choice when the panel is taller than it is wide. The third passes a single image as a plain argument instead of mapping it, which draws the same picture at every point.

The fourth maps size to a column. Wrapping it in I() matters: without it ggplot2 would build a legend for the size, and the values would be rescaled rather than used as the fractions of the panel you wrote. The last plot sets a fixed colour, which tints every image.

image may also be a column of URLs, or a list of magick images, so an image can come from anywhere without being written to disk first.

size is one way to control the dimensions; width and height are the other. They are read per row, so a single layer can draw images of different shapes. Giving only one of them preserves the aspect ratio of each image; giving both stretches it into the requested box.

d$w <- seq(.02, .1, length.out = 10)
ggplot(d, aes(x, y)) + geom_image(aes(image = image, width = w))

## width and height together stretch the image into the requested box
ggplot(d, aes(x, y)) + geom_image(aes(image = image, width = .12, height = .04))

colour and alpha are applied to each image in turn, which makes it easy to fade an image into the background or to highlight a subset of the rows. Note the scale_alpha_identity() in both plots below: alpha is an ordinary ggplot2 aesthetic, and its default continuous scale would compress the values you give into [0.1, 1].

d$fade <- seq(.2, 1, length.out = 10)
ggplot(d, aes(x, y)) + geom_image(aes(image = image), size = .08, alpha = .5) +
    scale_alpha_identity()

ggplot(d, aes(x, y)) + geom_image(aes(image = image, alpha = fade), size = .08) +
    scale_alpha_identity()

geom_image_repel

geom_image_repel() draws the same images as geom_image() but moves them so that they no longer overlap, which keeps the images readable when the points are close together. The positions in the data are untouched – only the drawing is displaced – so the axes, the scales and any other layer keep their meaning.

d2 <- data.frame(x = c(.50, .52, .51), y = c(.50, .51, .49))
p <- ggplot(d2, aes(x, y, image = logo)) +
    scale_x_continuous(limits = c(0, 1)) +
    scale_y_continuous(limits = c(0, 1))

## as they are, the images overlap
p + geom_image(width = .12)

## ... and are pushed apart by geom_image_repel()
p + geom_image_repel(width = .12)

It takes everything geom_image() takes, plus four parameters that control the displacement:

The displacement is deterministic: the same data and the same parameters give the same layout on every run, and no network access is involved once the images are cached. That also means a plot does not change shape when it is redrawn.

geom_subview

geom_subview() embeds a whole plot – a pie chart, a legend, a small ggplot – as an inset at a data point. Because width and height are in data units, the insets stay the size you asked for whatever the panel is, and they move with the data when the scales change.

library(tibble)
dd <- data.frame(x = LETTERS[1:3], y = 1:3)
pie <- ggplot(dd, aes(x = 1, y, fill = x)) + geom_bar(stat = "identity", width = 1) +
    coord_polar(theta = "y") +
    theme_void() + theme(legend.position = "none") + theme_transparent()

df <- tibble(x = sample(2:9),
             y = sample(2:9),
             width = sample(seq(0.5, 3, length.out = length(x))),
             pie = list(pie))
p <- ggplot(data = data.frame(x = c(0, 10), y = c(0, 10)), aes(x, y)) + geom_blank()
p + geom_subview(aes(x = x, y = y, subview = pie, width = width, height = width), data = df)

The subview goes in a list column, one plot per row, and x, y, width and height are mapped from the data frame as usual. The insets are drawn with a transparent background (theme_transparent(), see Themes), so the plot underneath shows through instead of being hidden by a grey panel.

The subview may also be passed as the first argument, which is the form the published examples use: geom_subview(pie, x = 2, y = 2) draws one inset at (2, 2). In that form width and height default to 0.1.

Sub-views from other graphics systems

geom_subview() accepts anything that ggplotify::as.grob() can turn into a grid grob, so an inset does not have to be a ggplot. A base graphics plot, a recorded plot, a grob, or an image can be placed as a sub-view just as easily, and plot objects from other packages work whenever as.grob() has a method for them.

sub-view how to pass it
base graphics a function() that draws it, or a one-sided formula such as ~ plot(x, y)
recorded plot function() replayPlot(rec)
grid grob the grob itself
ggplot the plot object; subclasses such as ggtree objects work the same
magick the image

A base plot is a side effect on a graphics device rather than an object, so it has to be handed over as something that can be drawn: a function, or a formula whose right-hand side is the plotting call.

set.seed(2026)
svd <- data.frame(x = rep(c(0, 2, 4), 2), y = rep(c(0, 2), each = 3))
svd$hist <- lapply(1:6, function(i) function() {
    hist(rnorm(300, mean = i), main = "", xlab = "", ylab = "",
         col = i, border = "white", axes = FALSE)
})
ggplot(svd, aes(x, y)) + geom_point(size = 2) +
    geom_subview(aes(x = x, y = y, subview = hist), data = svd,
                 width = 1.7, height = 1.7) +
    xlim(-1.2, 5.2) + ylim(-1.2, 3.2) + coord_fixed() + theme_bw()

The same as.grob() conversion runs for every row, so one layer can hold sub-views built in different ways.

svd <- data.frame(x = 1:3, y = 0)
svd$subview <- list(
    function() plot(cos(seq(0, 2 * pi, length.out = 50)), type = "l",
                    axes = FALSE, ann = FALSE),
    ggplot(mtcars, aes(wt, mpg)) + geom_point(),
    magick::image_read(system.file("extdata/Rlogo.png", package = "ggimage"))
)
labels <- c("base graphics", "ggplot", "magick")
names(svd$subview) <- labels

ggplot(svd, aes(x, y)) + geom_blank() +
    geom_subview(aes(x = x, y = y, subview = subview), data = svd,
                 width = .8, height = .8) +
    geom_text(aes(x, y, label = label),
              data = data.frame(x = 1:3, y = -.7, label = labels),
              vjust = 1, size = 3.2) +
    xlim(.5, 3.5) + ylim(-1.2, 1.2) + coord_fixed() + theme_void()

Two things are worth knowing before relying on this.

The panel still needs a scale. geom_subview() draws with annotation_custom(), which carries no x or y aesthetic of its own. When it is the only layer in the plot, the panel keeps its default (0, 1) range and only the insets that fall inside that range are drawn. Add a geom_blank(), a geom_point() or a geom_text() – as above – or set the limits explicitly.

A sub-view given as a function is drawn afresh on every draw. The conversion happens when the plot is drawn, so the function has to be able to run on its own, and the usual closure trap applies: build the list of functions with lapply() rather than a for loop, or every function will use the last value of the loop variable.

For a plot that already exists as a recorded plot, replay it inside a function:

dev.control(displaylist = "enable")   # otherwise the recording comes out empty
plot(1:10)
rec <- recordPlot()

df$subview <- list(function() replayPlot(rec))

recordPlot() returns an empty recording unless the display list was enabled, which is why dev.control() is there.

Images as backgrounds

geom_bgimage

Use an image as the background of the plot panel.

ggplot(iris, aes(Petal.Length, Petal.Width)) +
    geom_bgimage(logo) + geom_point()

The layer is added first, so everything drawn after it appears on top. By default the image is stretched to fill the panel, which is usually what you want for a texture, but it does mean the image is distorted if its ratio differs from the panel’s.

Give pixel to place the image in data coordinates instead: the chosen pixel of the image – c(col, row) counted from the top-left corner – is anchored to the data point (x, y), and scale sets the number of data units per image pixel. The anchoring does not depend on the size of the image or of the panel, so the image keeps its place when the plot is resized or the scales change.

info <- magick::image_info(magick::image_read(logo))

ggplot(iris, aes(Petal.Length, Petal.Width)) +
    geom_bgimage(logo, pixel = c(1, info$height), x = 0, y = 0,
                 scale = 2.5 / info$width) +
    geom_point() +
    scale_x_continuous(limits = c(-0.4, 7), expand = c(0, 0)) +
    scale_y_continuous(limits = c(-0.3, 2.6), expand = c(0, 0)) +
    coord_fixed()

Here the bottom-left corner of the image (col = 1, row = height) is pinned to the origin, and scale = 2.5 / info$width makes the image 2.5 data units wide. Since the anchoring is in data units, coord_fixed() is needed to keep the image from being stretched by unequal axes.

ggbackground

ggbackground() and geom_bgimage() both fill the panel and stretch the image to it. The difference is how they are applied: ggbackground(p, img) takes a plot and returns one, so it can be used on a plot that is already built, whereas geom_bgimage() is a layer and sits behind whatever is added after it.

ggbackground(ggplot(iris, aes(Petal.Length, Petal.Width)) + geom_point(), logo)

The background is inserted as the first layer rather than as a grob wrapping the whole plot, so anything added to the result afterwards is still positioned by the plot’s own coordinate system. Extra arguments are passed on to geom_image(), so ggbackground(p, img, colour = "grey80") tints the background.

Images from remote services

Several geoms take the name of an image and fetch it for you: a Pokemon, an emoji, an icon, a flag, or a taxonomic silhouette. They all use the same image aesthetic and the same sizing arguments as geom_image(), and anything that is a URL can also be given to geom_image() directly.

Downloaded images are cached, so a plot that is redrawn – on a resize, or in a loop of ggsave() calls – does not download them again.

geom_pokemon

ggplot(d, aes(x, y)) + geom_pokemon(aes(image = ifelse(x > 0, "pikachu", "tauros")), size = .1)

The name is the Pokemon’s name in lower case, and list.pokemon() returns the names that are available.

geom_emoji

Emoji are named by code point, e.g. 1f600 for a grinning face.

set.seed(123)
iris2 <- iris[sample(1:nrow(iris), 30), ]
model <- lm(Petal.Length ~ Sepal.Length, data = iris2)
iris2$fitted <- predict(model)

ggplot(iris2, aes(x = Sepal.Length, y = Petal.Length)) +
  geom_linerange(aes(ymin = fitted, ymax = Petal.Length),
                 colour = "purple") +
  geom_abline(intercept = model$coefficients[1],
              slope = model$coefficients[2]) +
  geom_emoji(aes(image = ifelse(abs(Petal.Length - fitted) > 0.5, "1f622", "1f600")))

Because the image can be computed per row, an emoji layer is a compact way to label the outcome of a calculation: here the face depends on the residual of each point.

geom_icon

d$icon <- sample(c("power", "wifi", "pie-chart"), 10, replace = TRUE)
ggplot(d, aes(x, y)) + geom_icon(aes(image = icon), size = .1)

See https://ionic.io/ionicons for the icon names; list.icon() returns the ones this version knows about.

geom_flag

Flags are named by their two-letter code, or by a subdivision code such as GB-ENG for England.

f <- system.file("extdata/medals.txt", package = "ggimage")
medals <- read.table(f, header = TRUE)
p <- ggplot(medals, aes(Country, count)) + geom_col(aes(fill = medal), width = .8)

p + geom_flag(y = -2, aes(image = code)) +
    coord_flip() + expand_limits(y = -2) +
    scale_fill_manual(values = c("Gold" = "gold", "Bronze" = "#cd7f32", "Silver" = "#C0C0C0"))

Note the y = -2 here: it is a fixed position for every flag rather than a mapping, and expand_limits() makes room for it below the bars. list.flag() returns the available codes.

geom_worldcup2018

geom_worldcup2018() is geom_flag() restricted to the 32 teams of the 2018 FIFA World Cup, which is a convenient way to draw a set of flags without looking up any codes. A name outside that list is drawn as nothing, so an unexpected value shows up as a gap rather than as a wrong flag.

wc <- expand.grid(x = 1:8, y = 1:4)
wc$country <- list.worldcup2018()

ggplot(wc, aes(x, y)) +
    geom_worldcup2018(aes(image = country), size = .1) +
    coord_fixed() + theme_void()

list.worldcup2018() returns the 32 codes the geom accepts. It is a fixed vector, so unlike the other list.*() helpers it does not need a network connection.

geom_phylopic

PhyloPic hosts free silhouettes of organisms. geom_phylopic() takes a scientific name, or a PhyloPic uid, and resolves it for you.

sp <- c("Canis lupus", "Ursus maritimus", "Homo sapiens", "Gallus gallus",
        "Bos taurus", "Panthera leo", "Loxodonta africana",
        "Drosophila melanogaster")
species <- data.frame(x = seq_along(sp), y = 1, species = sp)

ggplot(species, aes(x, y)) +
    geom_phylopic(aes(image = species), size = .12) +
    scale_x_continuous(breaks = seq_along(sp), labels = sp) +
    ylim(.7, 1.3) +
    theme_void() + theme(axis.text.x = element_text(size = 8, angle = 20, hjust = 1))

Unlike the other remote geoms, geom_phylopic() has its own default image_fun, which clears the background of the SVG rather than leaving it white. Pass your own image_fun to change that.

phylopic_uid() is the lookup behind it. A name can match several silhouettes, and seed picks one of them, so the same name gives the same image on every run. A name that resolves to nothing comes back as NA, and the layer draws nothing for that row.

phylopic_uid(c("Canis lupus", "Homo sapiens"))

A uid can be used directly, which is useful when a name does not resolve, or when you want a specific drawing.

uid <- "5036f260-5a0d-42c5-a0bd-9eb0729e54e0" # Canis lupus
ggplot(data.frame(x = 1, y = 1), aes(x, y)) +
    geom_phylopic(aes(image = uid), size = .5) +
    xlim(.5, 1.5) + ylim(.5, 1.5) + theme_void()

autocomplete_name() suggests full names from a partial one, which is the quickest way to find a name that will resolve.

head(autocomplete_name("Canis"), 4)

To keep the images, or to draw them without a network connection, download_phylopic() saves them into a directory. The file paths it returns can then be passed to geom_phylopic() just like a uid, and because they are local files the plot no longer depends on PhyloPic being reachable.

dir <- tempfile()
dir.create(dir)
files <- download_phylopic(c("Canis lupus", "Gallus gallus"), destdir = dir)
basename(files)

Listing the images that are available

Each service has a list.*() function that returns the names it accepts, which is easier than guessing. list.pokemon(), list.icon() and list.flag() ask the service for its current files, so the three calls below are shown but not run; list.worldcup2018() is the exception, being a fixed vector. The geoms themselves need no connection once the images are cached.

head(list.pokemon(), 3)
head(list.icon(), 3)
head(list.flag(), 3)

Interactive images

geom_image_interactive() and geom_phylopic_interactive() are the ggiraph counterparts of geom_image() and geom_phylopic(). They add the ggiraph aesthetics – tooltip, data_id, onclick – to every image, so a plot can be embedded in an HTML page and the images become hoverable and clickable.

On their own they draw exactly what geom_image() draws, which is what makes them safe to use in a function that may or may not be rendered to HTML:

d$id <- seq_len(nrow(d))

ggplot(d, aes(x, y)) +
    geom_image_interactive(aes(image = image), size = .08) +
    theme_void()

The interactivity appears once the plot is wrapped in girafe(). The chunk below is not evaluated here, because the ggiraph JavaScript it needs would add several megabytes to this vignette; run it in your own document to get the interactive version, and hover the images to see the tooltips.

library(ggiraph)

girafe(ggobj = ggplot(d, aes(x, y)) +
    geom_image_interactive(aes(image = image, tooltip = round(x, 2), data_id = id),
                           size = .08) +
    theme_void())

Animating frames with gganimate

geom_image() draws one still image per row of its data, and it knows nothing about time: a raster grob is drawn when the plot is drawn, and no layer of this package asks for a second one. Animation therefore comes from gganimate, which owns the states of a plot, the frame rate, and the rendering of the result. ggimage imports nothing from gganimate; the two packages meet only in the data frame you hand to them. The pattern below is a frame-data-driven one: you decide what the frames are, the data frame says which image belongs to which frame, and gganimate walks through them.

The first step happens before any plotting. A source animation – a GIF, a video, a sequence of frames – is a container of still images, and that container has to become files on disk that a row of data can point at.

library(magick)
library(gganimate)

## a small deterministic source animation: four solid-colour frames
frames <- lapply(c("steelblue", "gold", "firebrick", "seagreen"), function(cl) {
    image_blank(200, 200, color = cl) |> image_annotate(cl, size = 24)
})
gif <- image_join(frames)
image_write(gif, "source.gif", format = "gif")   # a multi-frame file, not one PNG

## ... and the same thing read back, one frame per file
src <- image_read("source.gif")
dir.create("frames", showWarnings = FALSE)
paths <- sprintf("frames/frame%02d.png", seq_along(src))
for (i in seq_along(src)) {
    ## magick writes the format of the *object*, not of the file name, so an
    ## explicit format is what keeps these files PNG rather than GIF bytes
    image_write(src[i], paths[i], format = "png")
}
basename(paths)

The frame files are now ordinary paths, so the rest is a data frame. It has one row per (glyph, frame), and a column naming the state – which frame each row belongs to. That column is plain input data. It is not an aesthetic of geom_image(), and nothing in this package reads it: it is the column transition_manual() is given.

## one glyph that stays put, one that moves across the frames
nf <- length(paths)
d <- data.frame(
    glyph = rep(c("fixed", "moving"), each = nf),
    frame = rep(seq_len(nf), times = 2),
    x = c(rep(1, nf), seq(1, 4, length.out = nf)),
    y = rep(1, 2 * nf)
)
d$image <- rep(paths, times = 2)
head(d, 3)

The plot is then built from two kinds of layer. A layer whose data has no frame column cannot depend on the frame at all, so it is drawn in every rendered frame – here a band and some points that give the animation a backdrop. The images come from a geom_image() layer over the frame table, with a local aes() so that the x and image of that table are the ones used.

## no frame column here, so this layer persists across states
static <- data.frame(x = 1:4, y = 1)

p <- ggplot(d, aes(x, y)) +
    geom_rect(aes(xmin = 0.5, xmax = 4.5, ymin = 0.6, ymax = 1.4),
              data = data.frame(x = 1), fill = "grey92", inherit.aes = FALSE) +
    geom_point(data = static, size = 1.5) +
    geom_image(aes(image = image), data = d, size = .12) +
    coord_fixed() +
    theme_void()

## the states come from the frame column, not from a ggimage aesthetic
p <- p + transition_manual(frame)

animate(p, nframes = nf, fps = 4, renderer = magick_renderer())

transition_manual() takes the column directly; there is no ordering to declare, since the values of frame are taken as they come. nframes and fps belong to animate() rather than to the plot, and they choose the output schedule – how long each state is held, and how many frames the result has. magick_renderer() writes the frames through magick, so no other software is needed to produce the file; gganimate also offers renderers built on external tools, which are its business and not this package’s.

What this does not promise

Legend keys

An image layer has no natural legend key: a raster grob is not a shape a legend can draw by default, and the images are usually the data rather than a mapped scale. The global option ggimage.keytype chooses what to draw instead, and accepts four values:

d$group <- rep(c("up", "down"), each = 5)
key_plot <- ggplot(d, aes(x, y, image = image, colour = group)) +
    geom_image(size = .08) + theme_void()

options(ggimage.keytype = "rect")
key_plot

options(ggimage.keytype = "image")
key_plot + guides(colour = guide_legend(override.aes = list(image = img)))

options(ggimage.keytype = NULL)

With "point" and "rect" the key follows colour and alpha, so a colour legend still reads as a colour legend. With "image" the key is one of the pictures, which is the most readable choice when the images are the categories – here the two groups are told apart by their pictures rather than by a colour swatch. Which picture comes from the layer’s image argument: ggplot2 has no scale to look a per-row image up in, so a layer that maps image to a column can only put the same image in every key, and the chunk above hands one image per group to the guide with override.aes. The option is global, so set it once for the session, or restore the previous value with options(old) when you only want it for one plot.

Themes

theme_transparent() makes the panel and the legend background transparent, so that a plot can be placed on top of something else – a background image, another plot, or a slide with its own colour.

p <- ggplot(iris, aes(Petal.Length, Petal.Width, colour = Species)) +
    geom_point()

## the blue rectangle stands in for whatever is behind the plot
draw_on <- function(plot) {
    grid::grid.newpage()
    grid::grid.rect(gp = grid::gpar(fill = "steelblue", col = NA))
    print(plot, newpage = FALSE)
}

draw_on(p)

## the same plot with the theme applied
draw_on(p + theme_transparent())

theme_nothing() keeps only the panel: no axes, no labels, and no margin. It is meant for plots that are drawn inside another plot, such as the insets made by geom_subview(), where axes and titles would only be noise.

ggplot(iris, aes(Petal.Length, Petal.Width)) + geom_point() + theme_nothing()

Reading images

image_read2() reads an image and, by default, trims the empty margin around it. Scans and figures exported from other software often carry a wide white border, which would otherwise shrink the drawing itself to a small part of the glyph.

## put a white margin around the logo, as a scan or an exported figure has
bordered <- magick::image_border(magick::image_read(logo), "white", "40x40")
bf <- tempfile(fileext = ".png")
magick::image_write(bordered, bf)

rbind(before = magick::image_info(magick::image_read(bf))[, c("width", "height")],
      after  = magick::image_info(image_read2(bf))[, c("width", "height")])
# A tibble: 2 × 2
  width height
* <int>  <int>
1   804    641
2   724    561

The trim is based on white pixels on the first three channels, so it also works on images with an alpha channel, and it keeps a matte channel intact. An image that is entirely white, or one with no margin, is returned unchanged rather than being cropped to nothing. Use cut_empty_space = FALSE to skip the trim entirely, and ... to pass extra arguments to magick::image_read(), for instance a density for vector files.

Previewing a plot

ggpreview() saves a plot to a file and draws it back into the current device. The point is to see the plot at the exact size it will be saved, without leaving a file behind: with no filename it uses a temporary file that is cleaned up at the end of the session.

ggpreview(plot = ggplot(iris, aes(Petal.Length, Petal.Width)) + geom_point(),
          width = 4, height = 3)

Give filename to keep the file as well, in which case the remaining arguments are passed to ggsave() – so dpi, bg and the other ggsave() parameters work as usual. It accepts anything ggplotify can convert, so a base graphics plot or a grid object works as well as a ggplot.

The image cache

Reading an image is expensive, and a plot is drawn again on every resize, so ggimage caches prepared images. The cache is unbounded by default, which is fine for a handful of images but not for a plot with thousands of distinct ones. set_image_cache_policy() bounds it.

set_image_cache_policy(capacity = 200, ttl = 60, eviction = "lru")
get_image_cache_policy()[c("base_capacity", "base_ttl", "eviction")]
$base_capacity
[1] 200

$base_ttl
[1] 60

$eviction
[1] "lru"

NULL leaves an option unchanged, so a single call can adjust one setting without disturbing the others. Each value can also be set for the base images and for the transformed ones separately, with base_capacity / transform_capacity and so on, which is useful when a plot maps colour or angle and therefore produces many transformed copies of few base images.

get_image_cache_stats() reports what the cache is doing, which is the way to tell whether a policy is worth its cost.

reset_image_cache_stats()
ggplot(d, aes(x, y)) + geom_image(aes(image = image), size = .05)

get_image_cache_stats()$base
       hits      misses   evictions expirations     entries       bytes 
          2           0           0           0          54   145728272 

The result has a base and a transform entry, each with hits, misses, evictions, expirations, entries and bytes. A high misses count with few entries means the images are not being reused and the cache is not helping; evictions and expirations that climb mean the policy is too tight for the plot. reset_image_cache_stats() clears the counters without dropping the entries; get_image_cache_diagnostics() is the same function under a name that reads better in debugging code.

options(ggimage.image_cache_capacity = NULL,
        ggimage.image_cache_ttl = NULL,
        ggimage.image_cache_eviction = NULL)