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
image (or the image argument for a single image). The value may be a file path, a URL, or a magick image, and it may be mapped per row, so different points can use different images. A character column holds one path or URL per row; a list-column holds one magick image per row, as in df$image <- list(img1, img2), and mapping it with aes(image = image) draws each row’s own image. A row whose image is NA is drawn as nothing rather than as an error.
size. How large the image is, as a fraction of the panel – size = .05 is five percent of the width of the panel by default. Because it is relative to the panel, the same code gives the same visual result whatever the data range or the size of the output device.
width and height. An explicit size, in the same panel units as size. Either can be mapped per row. Giving one of them keeps the aspect ratio of the image; giving both draws the image in exactly that box, which distorts it if the box has a different ratio. They take precedence over size.
colour and alpha. Applied per row: colour tints the image and alpha sets its opacity. alpha is an ordinary ggplot2 aesthetic, so its default continuous scale maps the data range onto [0.1, 1]; add scale_alpha_identity() to pass your values through unchanged.
angle. Rotates the image, in degrees, counter-clockwise.
Layer parameters
by. Whether size refers to the "width" (the default) or the "height" of the image. It has no effect on width and height.
nudge_x, nudge_y. Shift the images by a fixed amount, in data units. The data are not changed, so this is the way to offset an image from the point it belongs to.
hjust. Where the image sits horizontally relative to its x. The default, 0.5, centres it; 0 (or "left") puts its left edge on the point and 1 (or "right") its right edge.
asp. Divides the width-to-height ratio of an image sized by size, so the image is drawn taller by that factor. The default, 1, keeps the proportions the image has on disk.
use_cache. Whether to reuse prepared images across draws (the default). See The image cache for how to bound it.
image_fun. A function applied to the magick image before it is drawn, for instance to flatten a transparent background or to recolour it.
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 belowlogo <-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), 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.
## width and height together stretch the image into the requested boxggplot(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].
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 overlapp +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:
max.iter (default 100). How many displacement iterations to run. Setting it to 0 keeps the original coordinates, which is useful to check how much of the layout the repulsion is actually responsible for.
force (default 0.1). The fraction of the overlap that is resolved in one iteration: each image of an overlapping pair moves by force * overlap / 2. Larger values converge in fewer iterations but produce bigger jumps, which can look arbitrary.
box.padding (default 0). Extra space kept around every image box, in the same units as width. Raise it when the images are legible but still feel cramped.
direction (default "both"). Which axes the images may move along, one of "both", "x" or "y". Overlap is always detected on both axes; the parameter only restricts the movement, so direction = "x" slides images horizontally until they clear each other.
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) <- labelsggplot(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 emptyplot(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.
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.
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.
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.
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.
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.
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.
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.
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:
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.
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 framesframes <-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 filesrc <-image_read("source.gif")dir.create("frames", showWarnings =FALSE)paths <-sprintf("frames/frame%02d.png", seq_along(src))for (i inseq_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 bytesimage_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 framesnf <-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 statesstatic <-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 aestheticp <- 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
You preprocess the frames. Turning a source animation into still files, and choosing which of them become states, is done by the magick calls above. ggimage reads the paths it is given and nothing else; it never opens a GIF as a sequence.
gganimate drives the states and the rendering. The transitions, the frame rate, the renderer and the output file are all its API. This package supplies the images and no part of the animation.
A static plot still draws one image per row.geom_image(image = "sprite.gif") draws a still image – magick gives the first frame of the file – and is not playback. Nothing in a plain ggplot advances between frames, because a raster grob has no clock.
The timing of the source is not preserved. The per-frame delays, the loop count and the disposal method of the original file are not read and not carried over; the schedule you pass to animate() is the schedule you get. Frame replacement is discrete, so an image changes from one state to the next with nothing in between – there is no easing of the images themselves, whereas the position of a glyph follows whichever transition you chose.
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:
"point" (the default when the option is unset) draws a filled point.
"rect" draws a filled rectangle.
"image" draws an image instead of a shape, tinted by colour and faded by alpha like the images in the panel.
"blank" draws no key at all, which is the behaviour of image layers before the key was implemented.
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
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 plotdraw_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 applieddraw_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.
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 hasbordered <- 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")])
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.
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.
ttl is a lifetime in seconds, after which an entry is treated as absent.
bytes is an approximate size cap, measured with object.size() plus a pixel-based lower bound for magick images. It is a guardrail rather than a measurement of process memory, since the native image buffers are larger than the estimate.
eviction is one of "lru", "fifo" or "none"; the last one disables capacity and byte eviction, leaving only ttl.
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.
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.