Created
February 14, 2026 23:04
-
-
Save oousmane/c03cfb0f8603a7f5517ff5129e315018 to your computer and use it in GitHub Desktop.
Function to consistently brand ggplot2 object
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #' 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