Title: Publication-Quality 'ggplot2' Annotation
Version: 0.1.0
Description: Annotation helper functions for publication-quality 'ggplot2' visualisation. These functions make it easier to annotate plots in a way that stays consistent with the set theme.
License: MIT + file LICENSE
URL: https://github.com/davidhodge931/ggscribe, https://davidhodge931.github.io/ggscribe/
BugReports: https://github.com/davidhodge931/ggscribe/issues
Depends: R (≥ 4.1.0)
Imports: farver, ggplot2 (≥ 4.0.0), glue, grid, rlang, scales
Encoding: UTF-8
Language: en-GB
RoxygenNote: 7.3.3
Suggests: blends, dplyr, flexoki, ggrefine, ggwidth, jumble, knitr, rmarkdown, spelling, stringr
Config/testthat/edition: 3
NeedsCompilation: no
Packaged: 2026-04-29 10:09:30 UTC; david
Author: David Hodge ORCID iD [aut, cre, cph]
Maintainer: David Hodge <davidhodge931@gmail.com>
Repository: CRAN
Date/Publication: 2026-05-02 09:20:02 UTC

ggscribe: Publication-Quality 'ggplot2' Annotation

Description

logo

Annotation helper functions for publication-quality 'ggplot2' visualisation. These functions make it easier to annotate plots in a way that stays consistent with the set theme.

Author(s)

Maintainer: David Hodge davidhodge931@gmail.com (ORCID) [copyright holder]

See Also

Useful links:


A mapped aesthetic for text colour on fill

Description

Modifies a mapped colour (or fill) aesthetic for contrast against the fill (or colour) aesthetic.

Function can be spliced into ggplot2::aes with rlang::!!!.

Usage

aes_contrast(..., dark = NULL, light = NULL, aesthetic = "colour")

Arguments

...

Require named arguments (and support trailing commas).

dark

A dark colour. If NULL, derived from theme text or panel background.

light

A light colour. If NULL, derived from theme text or panel background.

aesthetic

The aesthetic to be modified for contrast. Either "colour" (default) or "fill".

Value

A ggplot2 aesthetic in ggplot2::aes.

See Also

splice

Examples

library(ggplot2)
library(dplyr)
library(stringr)

set_theme(
 ggrefine::theme_light(
    panel_heights = rep(unit(50, "mm"), 100),
    panel_widths = rep(unit(75, "mm"), 100),
 )
)

ggwidth::set_equiwidth(equiwidth = 1.75)

mtcars |>
  count(cyl, am) |>
  mutate(
    am = if_else(am == 0, "Automatic", "Manual"),
    cyl = as.factor(cyl)
  ) |>
  ggplot(aes(x = am, y = n, colour = cyl, fill = cyl, label = n)) +
  geom_col(
    position = position_dodge2(preserve = "single", padding = 0.05),
    width = ggwidth::get_width(n = 2, n_dodge = 3),
  ) +
  scale_fill_discrete(palette = jumble::jumble) +
  scale_colour_discrete(palette = blends::multiply(jumble::jumble)) +
  geom_text(
    mapping = ggscribe::aes_contrast(), # or aes(!!!ggscribe::aes_contrast()),
    position = position_dodge2(
      width = ggwidth::get_width(n = 2, n_dodge = 3),
      padding = 0.05,
      preserve = "single"),
    vjust = 1.33,
    show.legend = FALSE,
  ) +
  scale_y_continuous(expand = expansion(c(0, 0.05))) +
  ggrefine::modern(x_type = "discrete")

mtcars |>
  count(cyl, am) |>
  mutate(
    am = if_else(am == 0, "automatic", "manual"),
    am = stringr::str_to_sentence(am),
    cyl = as.factor(cyl)
  ) |>
  ggplot(aes(y = am, x = n, colour = cyl, fill = cyl, label = n)) +
  geom_col(
    position = position_dodge2(preserve = "single", padding = 0.05),
    width = ggwidth::get_width(n = 2, n_dodge = 3, orientation = "y"),
  ) +
  scale_fill_discrete(palette = jumble::jumble) +
  scale_colour_discrete(palette = blends::multiply(jumble::jumble)) +
  geom_text(
    mapping = ggscribe::aes_contrast(), # or aes(!!!ggscribe::aes_contrast()),
    position = position_dodge2(
      width = ggwidth::get_width(n = 2, n_dodge = 3, orientation = "y"),
      preserve = "single",
      padding = 0.05,
    ),
    hjust = 1.25,
    show.legend = FALSE,
  ) +
  scale_x_continuous(expand = expansion(c(0, 0.05))) +
  ggrefine::modern(y_type = "discrete")


Annotate an axis bracket

Description

Draws a bracket spanning min(breaks) to max(breaks) along an axis edge or at a floating data position. The bar uses the same rendering path as axis_line(); the caps use the same path as axis_ticks(). Requires coord_cartesian(clip = "off").

Usage

axis_bracket(
  ...,
  position = NULL,
  xintercept = NULL,
  yintercept = NULL,
  breaks,
  colour = NULL,
  linewidth = NULL,
  linetype = NULL,
  length = ggplot2::rel(1)
)

Arguments

...

Not used. Forces named arguments.

position

One of "top", "bottom", "left", or "right". Inferred from xintercept or yintercept if not provided.

xintercept

For "left"/"right" axes: float the bracket to this x position in data coordinates instead of the panel edge.

yintercept

For "top"/"bottom" axes: float the bracket to this y position in data coordinates instead of the panel edge.

breaks

A numeric vector of length >= 2. The bar spans min(breaks) to max(breaks); caps are drawn at every break value.

colour

Inherits from axis.ticks in the set theme (falling back through axis.line and line).

linewidth

Inherits from axis.ticks in the set theme. Supports rel().

linetype

Inherits from axis.ticks in the set theme.

length

Length of the bracket caps as a grid unit. Supports rel(). Negative values flip the cap direction. Defaults to rel(1) (outward at theme tick length).

Value

A list of ggplot2 annotation layers.

See Also

axis_line(), axis_ticks(), axis_text(), reference_line(), panel_shade(), sec_axis_text()

Examples

library(ggplot2)
library(dplyr)

set_theme(
  ggrefine::theme_grey(
    panel_heights = rep(unit(50, "mm"), 100),
    panel_widths = rep(unit(75, "mm"), 100),
  )
)

mtcars |>
  ggplot(aes(x = wt, y = mpg, colour = as.factor(gear), fill = as.factor(gear))) +
  scale_colour_discrete(palette = blends::multiply(get_theme()$palette.colour.discrete)) +
  #clip = "off" is required for axis_text, axis_ticks and axis_bracket
  coord_cartesian(clip = "off") +
  #reference lines and shade
  ggscribe::reference_line(xintercept = 2.4) +
  ggscribe::reference_line(yintercept = 12)  +
  ggscribe::panel_shade(
    xmin = 4,
    xmax = 5,
  ) +
  #top axis
  scale_x_continuous(
    sec.axis = ggscribe::sec_axis_text(
      breaks = c(mean(c(4, 5))),
      labels = c("Range"),
      guide = ggscribe::guide_sec_axis_text(
        angle = 90,
      )
    )
  ) +
  ggscribe::axis_bracket(
    position = "top",
    breaks = c(4, 5),
  ) +
  ggscribe::axis_text(
    position = "top",
    breaks = c(2.4),
    labels = c("Threshold"),
  ) +
  #right axis
  ggscribe::axis_text(
    position = "right",
    breaks = 12,
    labels = "Threshold",
  ) +
  #'geom
  geom_point() +
  #annotations fit plot
  theme(plot.background = element_rect(colour = "grey92"))


Annotate an axis line

Description

Draws a line along an axis edge, with style defaults taken from the axis.line element of the set theme. Requires coord_cartesian(clip = "off").

Usage

axis_line(
  ...,
  position = NULL,
  xintercept = NULL,
  yintercept = NULL,
  colour = NULL,
  linewidth = NULL,
  linetype = NULL
)

Arguments

...

Not used. Forces named arguments.

position

One of "top", "bottom", "left", or "right". Inferred from xintercept or yintercept if not provided.

xintercept

For "left"/"right" axes: float the axis to this x position in data coordinates instead of the panel edge.

yintercept

For "top"/"bottom" axes: float the axis to this y position in data coordinates instead of the panel edge.

colour

Inherits from axis.line in the set theme.

linewidth

Inherits from axis.line in the set theme. Supports rel().

linetype

Inherits from axis.line in the set theme.

Value

A list of ggplot2 annotation layers.

See Also

axis_ticks(), axis_text(), axis_bracket(), reference_line(), panel_shade(), sec_axis_text()


Annotate axis text

Description

Draws text labels at specified break positions along an axis, with style defaults taken from the axis.text element of the set theme. Requires coord_cartesian(clip = "off").

Usage

axis_text(
  ...,
  position = NULL,
  xintercept = NULL,
  yintercept = NULL,
  breaks,
  labels = NULL,
  colour = NULL,
  size = NULL,
  family = NULL,
  hjust = NULL,
  vjust = NULL,
  angle = 0,
  length = ggplot2::rel(1)
)

Arguments

...

Not used. Forces named arguments.

position

One of "top", "bottom", "left", or "right". Inferred from xintercept or yintercept if not provided.

xintercept

For "left"/"right" axes: float the axis to this x position in data coordinates instead of the panel edge.

yintercept

For "top"/"bottom" axes: float the axis to this y position in data coordinates instead of the panel edge.

breaks

A numeric vector of break positions.

labels

One of:

  • NULL (default) to use break values as labels

  • A character vector the same length as breaks

  • A function taking break values and returning labels

colour

Inherits from axis.text in the set theme.

size

Inherits from axis.text in the set theme.

family

Inherits from axis.text in the set theme.

hjust, vjust

Justification. Auto-calculated from position if NULL.

angle

Text rotation angle. Defaults to 0.

length

Offset from the axis edge including tick length and margin. Supports rel(). Negative values place labels inside the panel. Defaults to rel(1) (theme tick length + text margin).

Value

A list of ggplot2 annotation layers.

See Also

axis_line(), axis_ticks(), axis_bracket(), reference_line(), panel_shade(), sec_axis_text()

Examples

library(ggplot2)
library(dplyr)

set_theme(
  ggrefine::theme_grey(
    panel_heights = rep(unit(50, "mm"), 100),
    panel_widths = rep(unit(75, "mm"), 100),
  )
)

mtcars |>
  ggplot(aes(x = wt, y = mpg, colour = as.factor(gear), fill = as.factor(gear))) +
  scale_colour_discrete(palette = blends::multiply(get_theme()$palette.colour.discrete)) +
  #clip = "off" is required for axis_text, axis_ticks and axis_bracket
  coord_cartesian(clip = "off") +
  #reference lines and shade
  ggscribe::reference_line(xintercept = 2.4) +
  ggscribe::reference_line(yintercept = 12)  +
  ggscribe::panel_shade(
    xmin = 4,
    xmax = 5,
  ) +
  #top axis
  scale_x_continuous(
    sec.axis = ggscribe::sec_axis_text(
      breaks = c(mean(c(4, 5))),
      labels = c("Range"),
      guide = ggscribe::guide_sec_axis_text(
        angle = 90,
      )
    )
  ) +
  ggscribe::axis_bracket(
    position = "top",
    breaks = c(4, 5),
  ) +
  ggscribe::axis_text(
    position = "top",
    breaks = c(2.4),
    labels = c("Threshold"),
  ) +
  #right axis
  ggscribe::axis_text(
    position = "right",
    breaks = 12,
    labels = "Threshold",
  ) +
  #'geom
  geom_point() +
  #annotations fit plot
  theme(plot.background = element_rect(colour = "grey92"))


Annotate axis ticks

Description

Draws axis ticks at specified break positions, with style defaults taken from the axis.ticks element of the set theme. Requires coord_cartesian(clip = "off").

Usage

axis_ticks(
  ...,
  position = NULL,
  xintercept = NULL,
  yintercept = NULL,
  breaks,
  minor = FALSE,
  colour = NULL,
  linewidth = NULL,
  length = ggplot2::rel(1)
)

Arguments

...

Not used. Forces named arguments.

position

One of "top", "bottom", "left", or "right". Inferred from xintercept or yintercept if not provided.

xintercept

For "left"/"right" axes: float the axis to this x position in data coordinates instead of the panel edge.

yintercept

For "top"/"bottom" axes: float the axis to this y position in data coordinates instead of the panel edge.

breaks

A numeric vector of break positions.

minor

Logical. If TRUE, uses minor tick theme defaults. Defaults to FALSE.

colour

Inherits from axis.ticks in the set theme.

linewidth

Inherits from axis.ticks in the set theme. Supports rel().

length

Total tick length as a grid unit. Supports rel(). Negative values flip the tick direction (inward). Defaults to rel(1) (outward at theme tick length).

Value

A list of ggplot2 annotation layers.

See Also

axis_line(), axis_text(), axis_bracket(), reference_line(), panel_shade(), sec_axis_text()


Guide optimised for secondary axis text annotations

Description

A wrapper around ggplot2::guide_axis() that defaults to using theme_sec_axis_text(). This guide is designed to strip away standard axis furniture (like lines and ticks) while preserving text, making it ideal for secondary axes used as margin labels.

Usage

guide_sec_axis_text(..., theme = theme_sec_axis_text())

Arguments

...

Additional arguments passed to ggplot2::guide_axis(), such as title, check.overlap, or angle.

theme

A theme object to style the guide. Defaults to theme_sec_axis_text(), which suppresses ticks and lines.

Value

A guide object to be used in a scale's guide argument or within sec_axis_text().

See Also

sec_axis_text(), theme_sec_axis_text()

Examples

library(ggplot2)
library(dplyr)

set_theme(
  ggrefine::theme_grey(
    panel_heights = rep(unit(50, "mm"), 100),
    panel_widths = rep(unit(75, "mm"), 100),
  )
)

mtcars |>
  ggplot(aes(x = wt, y = mpg, colour = as.factor(gear), fill = as.factor(gear))) +
  scale_colour_discrete(palette = blends::multiply(get_theme()$palette.colour.discrete)) +
  #clip = "off" is required for axis_text, axis_ticks and axis_bracket
  coord_cartesian(clip = "off") +
  #reference lines and shade
  ggscribe::reference_line(xintercept = 2.4) +
  ggscribe::reference_line(yintercept = 12)  +
  ggscribe::panel_shade(
    xmin = 4,
    xmax = 5,
  ) +
  #top axis
  scale_x_continuous(
    sec.axis = ggscribe::sec_axis_text(
      breaks = c(mean(c(4, 5))),
      labels = c("Range"),
      guide = ggscribe::guide_sec_axis_text(
        angle = 90,
      )
    )
  ) +
  ggscribe::axis_bracket(
    position = "top",
    breaks = c(4, 5),
  ) +
  ggscribe::axis_text(
    position = "top",
    breaks = c(2.4),
    labels = c("Threshold"),
  ) +
  #right axis
  ggscribe::axis_text(
    position = "right",
    breaks = 12,
    labels = "Threshold",
  ) +
  #'geom
  geom_point() +
  #annotations fit plot
  theme(plot.background = element_rect(colour = "grey92"))


Annotate a shaded panel region

Description

Draws a filled rectangle over the panel with colour defaults taken from the set theme. Defaults to a subtle overlay across the full panel, with the fill automatically adapting to light or dark panel backgrounds. Should be placed before geom layers.

Usage

panel_shade(
  ...,
  xmin = -Inf,
  xmax = Inf,
  ymin = -Inf,
  ymax = Inf,
  fill = "#878580",
  alpha = 0.25,
  colour = "transparent",
  linewidth = NULL,
  linetype = NULL
)

Arguments

...

Not used. Allows trailing commas and named-argument style calls.

xmin, xmax

Left and right edges of the rectangle. Defaults to -Inf and Inf. Use I() for normalized coordinates (0-1).

ymin, ymax

Bottom and top edges of the rectangle. Defaults to -Inf and Inf. Use I() for normalized coordinates (0-1).

fill

Fill colour. Defaults to a neutral grey.

alpha

Opacity of the rectangle. Defaults to 0.25.

colour

Border colour. Defaults to "transparent".

linewidth

Inherits from panel.border in the set theme. Supports rel().

linetype

Border linetype. Defaults to 1.

Value

A list containing an annotation layer.

Examples

library(ggplot2)
library(dplyr)

set_theme(
  ggrefine::theme_grey(
    panel_heights = rep(unit(50, "mm"), 100),
    panel_widths = rep(unit(75, "mm"), 100),
  )
)

mtcars |>
  ggplot(aes(x = wt, y = mpg, colour = as.factor(gear), fill = as.factor(gear))) +
  scale_colour_discrete(palette = blends::multiply(get_theme()$palette.colour.discrete)) +
  #clip = "off" is required for axis_text, axis_ticks and axis_bracket
  coord_cartesian(clip = "off") +
  #reference lines and shade
  ggscribe::reference_line(xintercept = 2.4) +
  ggscribe::reference_line(yintercept = 12)  +
  ggscribe::panel_shade(
    xmin = 4,
    xmax = 5,
  ) +
  #top axis
  scale_x_continuous(
    sec.axis = ggscribe::sec_axis_text(
      breaks = c(mean(c(4, 5))),
      labels = c("Range"),
      guide = ggscribe::guide_sec_axis_text(
        angle = 90,
      )
    )
  ) +
  ggscribe::axis_bracket(
    position = "top",
    breaks = c(4, 5),
  ) +
  ggscribe::axis_text(
    position = "top",
    breaks = c(2.4),
    labels = c("Threshold"),
  ) +
  #right axis
  ggscribe::axis_text(
    position = "right",
    breaks = 12,
    labels = "Threshold",
  ) +
  #'geom
  geom_point() +
  #annotations fit plot
  theme(plot.background = element_rect(colour = "grey92"))


Annotate a reference line

Description

Draws a reference line within the panel, with style defaults taken from the axis.line element of the set theme.

Usage

reference_line(
  ...,
  xintercept = NULL,
  yintercept = NULL,
  colour = NULL,
  linewidth = NULL,
  linetype = "dashed"
)

Arguments

...

Not used. Forces named arguments.

xintercept

Draw a vertical reference line at this x position.

yintercept

Draw a horizontal reference line at this y position.

colour

Inherits from axis.line in the set theme.

linewidth

Inherits from axis.line in the set theme. Supports rel().

linetype

Defaults to "dashed".

Value

A list of ggplot2 annotation layers.

See Also

axis_line(), axis_ticks(), axis_text(), axis_bracket(), panel_shade(), sec_axis_text()

Examples

library(ggplot2)
library(dplyr)

set_theme(
  ggrefine::theme_grey(
    panel_heights = rep(unit(50, "mm"), 100),
    panel_widths = rep(unit(75, "mm"), 100),
  )
)

mtcars |>
  ggplot(aes(x = wt, y = mpg, colour = as.factor(gear), fill = as.factor(gear))) +
  scale_colour_discrete(palette = blends::multiply(get_theme()$palette.colour.discrete)) +
  #clip = "off" is required for axis_text, axis_ticks and axis_bracket
  coord_cartesian(clip = "off") +
  #reference lines and shade
  ggscribe::reference_line(xintercept = 2.4) +
  ggscribe::reference_line(yintercept = 12)  +
  ggscribe::panel_shade(
    xmin = 4,
    xmax = 5,
  ) +
  #top axis
  scale_x_continuous(
    sec.axis = ggscribe::sec_axis_text(
      breaks = c(mean(c(4, 5))),
      labels = c("Range"),
      guide = ggscribe::guide_sec_axis_text(
        angle = 90,
      )
    )
  ) +
  ggscribe::axis_bracket(
    position = "top",
    breaks = c(4, 5),
  ) +
  ggscribe::axis_text(
    position = "top",
    breaks = c(2.4),
    labels = c("Threshold"),
  ) +
  #right axis
  ggscribe::axis_text(
    position = "right",
    breaks = 12,
    labels = "Threshold",
  ) +
  #'geom
  geom_point() +
  #annotations fit plot
  theme(plot.background = element_rect(colour = "grey92"))


Secondary axis optimised for text annotations

Description

Secondary axis optimised for text annotations

Usage

sec_axis_text(
  breaks = ggplot2::waiver(),
  labels = ggplot2::derive(),
  name = NULL,
  guide = ggplot2::guide_axis(theme = theme_sec_axis_text()),
  ...
)

Arguments

breaks

One of:

  • NULL for no breaks

  • ggplot2::waiver() (default) to inherit breaks from the primary axis

  • A numeric vector of break positions

  • A function that takes the scale limits as input and returns break positions (e.g. ⁠\(x) mean(c(x[2], 32))⁠)

labels

One of:

  • ggplot2::derive() (default) to derive labels from breaks

  • A character vector of labels, the same length as breaks

  • A function that takes break positions as input and returns labels

name

The name of the secondary axis. Use ggplot2::waiver() to derive the name from the primary axis, or NULL (default) for no name.

guide

A guide object used to render the axis. Defaults to guide_sec_axis_text(), which uses theme_sec_axis_text() to make transparent ticks and lines by default.

...

Additional arguments passed to ggplot2::dup_axis().

Value

A AxisSecondary object for use in the sec.axis argument of scale_x_continuous() or scale_y_continuous().

See Also

guide_sec_axis_text(), theme_sec_axis_text(), axis_text()

Examples

library(ggplot2)
library(dplyr)

set_theme(
  ggrefine::theme_grey(
    panel_heights = rep(unit(50, "mm"), 100),
    panel_widths = rep(unit(75, "mm"), 100),
  )
)

mtcars |>
  ggplot(aes(x = wt, y = mpg, colour = as.factor(gear), fill = as.factor(gear))) +
  scale_colour_discrete(palette = blends::multiply(get_theme()$palette.colour.discrete)) +
  #clip = "off" is required for axis_text, axis_ticks and axis_bracket
  coord_cartesian(clip = "off") +
  #reference lines and shade
  ggscribe::reference_line(xintercept = 2.4) +
  ggscribe::reference_line(yintercept = 12)  +
  ggscribe::panel_shade(
    xmin = 4,
    xmax = 5,
  ) +
  #top axis
  scale_x_continuous(
    sec.axis = ggscribe::sec_axis_text(
      breaks = c(mean(c(4, 5))),
      labels = c("Range"),
      guide = ggscribe::guide_sec_axis_text(
        angle = 90,
      )
    )
  ) +
  ggscribe::axis_bracket(
    position = "top",
    breaks = c(4, 5),
  ) +
  ggscribe::axis_text(
    position = "top",
    breaks = c(2.4),
    labels = c("Threshold"),
  ) +
  #right axis
  ggscribe::axis_text(
    position = "right",
    breaks = 12,
    labels = "Threshold",
  ) +
  #'geom
  geom_point() +
  #annotations fit plot
  theme(plot.background = element_rect(colour = "grey92"))


Theme adjustments optimised for secondary axis text annotations

Description

Theme adjustments optimised for secondary axis text annotations

Usage

theme_sec_axis_text(
  axis = NULL,
  axis_ticks_to = "transparent",
  axis_line_to = "transparent",
  axis_text_to = "keep",
  axis_title_to = "keep"
)

Arguments

axis

Character. "x", "y", or NULL (defaults to both).

axis_ticks_to

Action for ticks: "transparent", "blank", or "keep".

axis_line_to

Action for lines: "transparent", "blank", or "keep".

axis_text_to

Action for text: "transparent", "blank", or "keep".

axis_title_to

Action for titles: "transparent", "blank", or "keep".

Value

A ggplot2 theme object.

See Also

sec_axis_text(), guide_sec_axis_text()

axis_ticks(), axis_line(), axis_text(), reference_line()