Skip to contents
library(gguidance)
#> Loading required package: ggplot2

example <- ggplot(mpg, aes(displ, hwy)) +
  geom_point() +
  labs(
    x = "Engine displacement",
    y = "Highway miles per gallon"
  ) +
  theme(axis.line = element_line())

Warning This vignette is a work in progress and is incomplete.

A primer on position guides

An axis guide can be specified at two places: in the scale, or in the guides() function. You can use a string naming the type of guide by omitting the "guide_" prefix from the function, i.e. "axis". If you want to customise the axis, you can use the function itself.

Below is example code for setting the axis in the scale.

example +
  scale_x_continuous(guide = "axis") +
  scale_y_continuous(guide = guide_axis(angle = 45))

If you don’t need to set any scale parameters, it is shorter to use the guides() function.

example +
  guides(x = "axis", y = guide_axis(angle = 45))

Setting secondary axes works much the same way. The sec_axis() and dup_axis() function have a guide argument that can be used. When you use the guides() function, you can set the x.sec or y.sec guides.

example +
  scale_x_continuous(sec.axis = dup_axis(guide = guide_axis(angle = 45))) +
  guides(y.sec = "axis")

The axis extensions in {gguidance} are specified identically to vanilla {ggplot2}. The naming pattern of the functions is generally guide_axis_{type}(). Therefore, to use these axes as a string, you can use "axis_{type}".

Nested axes

The guide_axis_nested() function constructs axis guides that can indicate ranges of values. There are two ways to use these: (1) the ranges can be inferred from the labels or (2) you can manually set the ranges of interest.

Ranges from labels

The default behaviour of guide_axis_nested() is to infer ranges from the axis labels. These are inferred by trying to split the labels based on a separator. The interaction() function is convenient here, because it sorts the levels by the outer variable and automatically inserts "." as a separator, which works well with the behaviour of guide_axis_nested().

ggplot(mpg, aes(y = displ, x = interaction(year, drv))) +
  geom_jitter(width = 0.2) +
  labs(
    x = "Year / Drive Train",
    y = "Engine Displacement"
  ) +
  guides(x = "axis_nested")

However, beware that the default separator is any non-alphanumeric series of characters. This can give problems when any of the labels already contain such a character. In the example below, interaction() is inserting a "." as separator, but also the decimal notation from the dose variable contains a ".".

ggplot(ToothGrowth, aes(interaction(dose, supp), y = len)) +
  geom_jitter(width = 0.2) +
  labs(
    y = "Tooth Length",
    x = "Dose / Supplement type"
  ) +
  guides(x = "axis_nested")
#> Warning: Not all labels in `guide_axis_nested()` can be split into equal lengths.
#>  Is "[^[:alnum:]]+" the correct `sep` argument?

The way to solve this issue is to change the interaction() separator from the decimal notation, and instruct the axis to use that separator.

ggplot(ToothGrowth, aes(interaction(dose, supp, sep = ";"), y = len)) +
  geom_jitter(width = 0.2) +
  labs(
    y = "Tooth Length",
    x = "Dose / Supplement type"
  ) +
  guides(x = guide_axis_nested(sep = ";"))

Manual ranges

The alternative to inferring the ranges from the labels, is to manually set the ranges. You can use this as general annotation. Every range is parametrised by a range_start and range_end location. Optionally, you can specify range_name to make the labels. Below, we’re using the presidential dataset to provide annotations for the economics dataset.

econ <- ggplot(economics, aes(date, unemploy)) +
  geom_line() +
  labs(x = "Date", y = "Unemployment") +
  # To prevent the last label from being cut off
  theme(plot.margin = margin(5.5, 11, 5.5, 5.5))

econ +
  guides(x = guide_axis_nested(
    range_start = presidential$start,
    range_end   = presidential$end,
    range_name  = presidential$name
  ))

Alternatively, you can use the range_data and range_mapping arguments in a similar way you would with layers.

econ +
  guides(x = guide_axis_nested(
    range_data    = presidential,
    range_mapping = aes(start = start, end = end, name = name)
  ))

You can see in the example above that the ranges are dodged. This automatically occurs when ranges overlap with one another. To override the dodging of ranges, one can set the level variable in the range_mapping, or use range_level directly.

econ +
  guides(x = guide_axis_nested(
    range_data    = presidential,
    range_mapping = aes(
      start = start, end = end, name = name, 
      level = rep(3:1, length.out = nrow(presidential))
    )
  ))

Styling

Brackets

The plain line is not the only available option for displaying the ranges. There is a selection of brackets to choose from, and one of them is the "square" bracket. The width or height that a bracket occupies, can be set with the bracket_size argument.

econ +
  guides(x = guide_axis_nested(
    range_data    = presidential,
    range_mapping = aes(start = start, end = end, name = name),
    bracket       = "square",
    bracket_size  = unit(1, "mm")
  ))

These brackets can be specified in three ways: (1) they can be constructed by a bracket function, (2) they can be a string naming the function without the bracket_-prefix or (3) you can give a 2-column matrix as line coordinates. In option (2), some functions take parameters that influence for example the curvature of the brackets, so again, you can use the function instead of the string if you need to tweak. Below, the brackets that come with {gguidance} are displayed.

# Listing all the bracket names and their functions
bracket_list <- list(
  "sigmoid" = bracket_sigmoid(),
  "atan"    = bracket_atan(),
  "curvy"   = bracket_curvy(),
  "line"    = bracket_line(),
  "round"   = bracket_round(),
  "chevron" = bracket_chevron(),
  "square"  = bracket_square()
)

# Reshaping data
type     <- rep.int(names(bracket_list), lengths(bracket_list) / 2)
brackets <- as.data.frame(do.call(rbind, bracket_list))
brackets <- transform(
  brackets,
  type = type,
  x = (x / 2) + match(type, names(bracket_list)) - 0.25
)

# Plotting brackets
ggplot(brackets, aes(y, x, group = type)) +
  geom_line() +
  scale_y_continuous(
    breaks = seq_along(bracket_list),
    labels = names(bracket_list)
  ) +
  theme(panel.grid.major = element_blank())

For option (3), the example below shows how you can describe your own bracket with a 2-column matrix. Note that for horizontal axes, the 1st column is the y-direction and the 2nd column is the x-direction. For vertical axes, this is flipped.

zigzag <- seq(0, 1, length.out = 10)
zigzag <- cbind(x = seq_along(zigzag) %% 2, y = zigzag)

econ +
  guides(x = guide_axis_nested(
    range_data    = presidential,
    range_mapping = aes(start = start, end = end, name = name),
    bracket = zigzag,
    bracket_size = unit(1, "mm")
  ))

Theming

The styling of the brackets inherit from the relevant axis.ticks.{x/y}.{position} theme element. The styling of the text inherit from the relevant axis.text.{x/y}.{position} theme element. To style these elements separately from their parent, you can use the bracket_theme and deep_text arguments.

econ +
  guides(x = guide_axis_nested(
    range_data    = presidential,
    range_mapping = aes(start = start, end = end, name = name),
    deep_text     = element_text(colour = "red"),
    bracket_theme = element_line(linetype = "dotted")
  ))

Moreover, you can provide a list of elements to deep_text or bracket_theme to style these elements on a per-level basis for even greater control. Worth mentioning: the hjust parameter of this text is relative to the range it occupies.

econ +
  guides(x = guide_axis_nested(
    range_data    = presidential,
    range_mapping = aes(start = start, end = end, name = name),
    deep_text     = list(element_text(colour = "red", hjust = 0),  # 1st level
                         element_text(face = "bold",  hjust = 1)), # 2nd level
    bracket_theme = list(element_line(linetype = "dotted"), # 1st level
                         element_line(linewidth = 1))       # 2nd level
  ))