Skip to content

Instantly share code, notes, and snippets.

@oousmane
Created February 14, 2026 23:04
Show Gist options
  • Select an option

  • Save oousmane/c03cfb0f8603a7f5517ff5129e315018 to your computer and use it in GitHub Desktop.

Select an option

Save oousmane/c03cfb0f8603a7f5517ff5129e315018 to your computer and use it in GitHub Desktop.
Function to consistently brand ggplot2 object
#' Add a logo to a ggplot panel
#'
#' Places a raster image (logo, stamp, watermark) inside a ggplot panel using
#' [ggplot2::annotation_custom()]. Placement is controlled either by a corner
#' shorthand or by explicit coordinates, and sizing uses absolute [grid::unit()]
#' values for predictable results across plot dimensions.
#'
#' @param p A [ggplot2::ggplot()] object.
#' @param logo A path or URL to an image file, a `magick-image` object, or any
#' object accepted by [magick::image_read()].
#' @param corner Placement corner: one of `"tl"` (top-left), `"tr"` (top-right),
#' `"bl"` (bottom-left), `"br"` (bottom-right). Set to `NULL` to use manual
#' `x`/`y` coordinates. Default is `"br"`.
#' @param x Horizontal position when `corner = NULL`. Either a numeric value in
#' `[0, 1]` (interpreted as `npc`) or a [grid::unit()] object.
#' @param y Vertical position when `corner = NULL`. Either a numeric value in
#' `[0, 1]` (interpreted as `npc`) or a [grid::unit()] object.
#' @param just Justification of the grob anchor point, as a length-2 character
#' vector (e.g. `c("left", "bottom")`). Inferred from `corner` when supplied;
#' defaults to `c("centre", "centre")` in manual mode. Exposed primarily for
#' use by [add_logos()].
#' @param width Logo width as a [grid::unit()] object, or a bare number
#' interpreted as centimetres. Default is `grid::unit(2, "cm")`.
#' @param aspect_ratio Height-to-width ratio of the logo. If `NULL` (default),
#' computed automatically from the image dimensions via [magick::image_info()].
#' @param alpha Opacity of the logo, between `0` (fully transparent) and `1`
#' (fully opaque). Useful for watermark-style placement. Default is `1`.
#' @param margin Distance between the logo and the panel edge, as a
#' [grid::unit()] object or a bare number interpreted as millimetres.
#' Default is `grid::unit(3, "mm")`.
#'
#' @return The input ggplot object `p` with an [ggplot2::annotation_custom()]
#' layer added. No theme elements are modified.
#'
#' @note If the logo is placed very close to the panel edge and appears clipped,
#' add `+ ggplot2::coord_cartesian(clip = "off")` to the plot.
#'
#' @seealso [add_logos()] for placing multiple logos in one call.
#'
#' @importFrom magick image_read image_info
#' @importFrom grid rasterGrob unit is.unit gpar
#' @importFrom ggplot2 annotation_custom
#'
#' @examples
#' library(ggplot2)
#'
#' logo <- "https://raw.githubusercontent.com/tidyverse/ggplot2/refs/heads/main/man/figures/logo.png"
#'
#' p <- ggplot(mtcars, aes(wt, mpg)) + geom_point()
#'
#' # Bottom-right corner (default)
#' add_logo(p, logo)
#'
#' # Top-left, larger
#' add_logo(p, logo, corner = "tl", width = grid::unit(3, "cm"))
#'
#' # Semi-transparent watermark, centred manually
#' add_logo(p, logo, corner = NULL, x = 0.5, y = 0.5, alpha = 0.15)
#'
#' @export
add_logo <- function(
p,
logo,
corner = "br",
x = NULL,
y = NULL,
just = NULL,
width = grid::unit(2, "cm"),
aspect_ratio = NULL,
alpha = 1,
margin = grid::unit(3, "mm")
) {
if (!inherits(logo, "magick-image")) logo <- magick::image_read(logo)
if (is.null(aspect_ratio)) {
info <- magick::image_info(logo)
aspect_ratio <- info$height / info$width
}
if (!grid::is.unit(width)) width <- grid::unit(width, "cm")
if (!grid::is.unit(margin)) margin <- grid::unit(margin, "mm")
height <- width * aspect_ratio
if (!is.null(corner)) {
just <- switch(corner,
bl = c("left", "bottom"), br = c("right", "bottom"),
tl = c("left", "top"), tr = c("right", "top"),
stop("`corner` must be one of 'tl', 'tr', 'bl', 'br'.")
)
x <- switch(corner,
bl = , tl = margin,
br = , tr = grid::unit(1, "npc") - margin
)
y <- switch(corner,
bl = , br = margin,
tl = , tr = grid::unit(1, "npc") - margin
)
} else {
if (is.null(x) || is.null(y)) stop("Supply `corner` or both `x` and `y`.")
if (is.null(just)) just <- c("centre", "centre")
if (!grid::is.unit(x)) x <- grid::unit(x, "npc")
if (!grid::is.unit(y)) y <- grid::unit(y, "npc")
}
logo_grob <- grid::rasterGrob(
image = logo,
x = x, y = y,
width = width, height = height,
just = just,
gp = grid::gpar(alpha = alpha)
)
p + ggplot2::annotation_custom(
grob = logo_grob,
xmin = -Inf, xmax = Inf,
ymin = -Inf, ymax = Inf
)
}
#' Add multiple logos to a ggplot panel
#'
#' A vectorised wrapper around [add_logo()] that places several logos in one
#' call. Logos assigned to the same corner are automatically laid out
#' side-by-side, proceeding inward from the edge, separated by `gap`.
#'
#' @param p A [ggplot2::ggplot()] object.
#' @param logos A list of named argument lists, each corresponding to a single
#' call to [add_logo()]. Any argument accepted by [add_logo()] can be
#' included. The `corner` element defaults to `"br"` if omitted.
#' @param gap Space between consecutive logos sharing the same corner, as a
#' [grid::unit()] object or a bare number interpreted as millimetres.
#' Default is `grid::unit(2, "mm")`.
#'
#' @return The input ggplot object `p` with one [ggplot2::annotation_custom()]
#' layer added per logo. No theme elements are modified.
#'
#' @note Logos are laid out in the order they appear in `logos`. To control
#' which logo is closest to the corner, put it first in the list.
#'
#' @seealso [add_logo()] for placing a single logo.
#'
#' @examples
#' library(ggplot2)
#'
#' logo_a <- "https://raw.githubusercontent.com/tidyverse/ggplot2/refs/heads/main/man/figures/logo.png"
#' logo_b <- "https://raw.githubusercontent.com/tidyverse/ggplot2/refs/heads/main/man/figures/logo.png"
#'
#' p <- ggplot(mtcars, aes(wt, mpg)) + geom_point()
#'
#' # Two logos side-by-side in the bottom-right corner
#' add_logos(p, list(
#' list(logo = logo_a, corner = "br"),
#' list(logo = logo_b, corner = "br", width = grid::unit(1.5, "cm"))
#' ))
#'
#' # Logos in different corners
#' add_logos(p, list(
#' list(logo = logo_a, corner = "br"),
#' list(logo = logo_b, corner = "tl", alpha = 0.4)
#' ))
#'
#' @export
add_logos <- function(p, logos, gap = grid::unit(2, "mm")) {
if (!grid::is.unit(gap)) gap <- grid::unit(gap, "mm")
corner_accum <- list()
resolved <- lapply(logos, function(args) {
corner <- args$corner %||% "br"
width <- args$width %||% grid::unit(2, "cm")
margin <- args$margin %||% grid::unit(3, "mm")
if (!grid::is.unit(width)) width <- grid::unit(width, "cm")
if (!grid::is.unit(margin)) margin <- grid::unit(margin, "mm")
accum <- corner_accum[[corner]] %||% grid::unit(0, "mm")
args$x <- switch(corner,
bl = , tl = margin + accum,
br = , tr = grid::unit(1, "npc") - margin - accum
)
args$y <- switch(corner,
bl = , br = margin,
tl = , tr = grid::unit(1, "npc") - margin
)
args$just <- switch(corner,
bl = c("left", "bottom"), br = c("right", "bottom"),
tl = c("left", "top"), tr = c("right", "top")
)
corner_accum[[corner]] <<- accum + width + gap
args$corner <- NULL
args
})
Reduce(
f = function(plot, args) do.call(add_logo, c(list(p = plot), args)),
x = resolved,
init = p
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment