Warning The lifecycle of position guides is ‘questioning’. This is the case because guide extensions might get a new home after ggplot2 starts using a new guide system.
A primer on position guides
Position guides are ticks, labels and lines drawn at the x- and
y-axes. Vanilla ggplot2 comes with two position guides:
guide_axis()
, which draws axes, and
guide_none()
, which skips drawing anything. The ggh4x
package has a few additional axes described further on. A convenient way
to specify what guides should be drawn where is the
guides()
function, that let’s you draw x
,
x.sec
, y
and y.sec
axes. You can
either specify a guide by name omitting the guide_*
prefix,
e.g. "axis"
, or by calling the guide constructor,
guide_axis()
, that allows you to give additional arguments.
Unless you specify the position
argument to guides
yourself, x
and x.sec
are the bottom and top
axes respectively and likewise y
and y.sec
are
the left and right axes.
g <- ggplot(mtcars, aes(wt, mpg)) +
geom_point() +
theme(axis.line = element_line(colour = "black"))
g + guides(
x = guide_none(title = "x"),
x.sec = guide_axis(title = "x.sec"),
y = "none",
y.sec = "axis"
)
An alternative way to setting the position guides is the use the
guide
argument in the position scales. Note that both the
position scale and the guide have a position
argument. The
guide’s position overrules the scale’s position, but if the guide’s
position is unspecified, it inherits the scale’s position.
g + scale_x_continuous(
position = "bottom",
guide = guide_axis(position = "top")
)
The final place you can specify axes is as an argument to secondary axes in continuous position scales.
g + scale_x_continuous(
sec.axis = dup_axis(breaks = 2:4 + 0.5,
guide = guide_axis(angle = 45))
)
The main takeaways that apply also to ggh4x’s position guides are the following:
- Position guides can be set in the
guides()
function or as an argument to the position scales. - One can use the name of the guides (
"axis"
) to take the defaults, or use a constructor function such asguide_axis()
to pass specific arguments.
Coloured axis
This position guide has the convenience argument colour
to set the colour of all axis elements at once. This simply circumvents
having to set three separate theme elements with the
theme()
function. The colour
argument is
available in all of ggh4x’s position guides.
# Setting all theme elements is a mild inconvenience.
g + theme(
axis.line.x = element_line(colour = "forestgreen"),
axis.ticks.x = element_line(colour = "forestgreen"),
axis.text.x = element_text(colour = "forestgreen")
)
# A little bit easier
g + guides(x = guide_axis_colour(colour = "forestgreen"))
#> Warning: The S3 guide system was deprecated in ggplot2 3.5.0.
#> ℹ It has been replaced by a ggproto system that can be extended.
#> This warning is displayed once every 8 hours.
#> Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
#> generated.
The motivation for this axis is that it used to be a mild inconvenience to set the colour of all these theme elements, if you wanted a colour correspondence between data on a secondary axis.
ggplot(economics, aes(date)) +
geom_line(aes(y = unemploy)) +
geom_line(aes(y = pop / 30), colour = "red") +
scale_y_continuous(
sec.axis = sec_axis(
~ .x * 30, name = "pop",
guide = guide_axis_colour(colour = "red"))
) +
theme(axis.line.y = element_line())
Truncated axes
The truncated axes is not a very special axis: it just makes the axis line shorter. By default, it trims the axis line to the outermost break positions.
g + guides(x = "axis_truncated")
You can control how far the axis line is trimmed by setting the
trunc_lower
and trunc_upper
options in the
guide function. These truncation arguments are supported in all other
position guides described further on too.
# Using grid units to specify data-independent truncation points.
g + guides(x = guide_axis_truncated(trunc_lower = unit(0.1, "npc"),
trunc_upper = unit(0.9, "npc")))
# Using atomic vectors are interpreted as data points that should be mapped.
g + guides(x = guide_axis_truncated(trunc_lower = 2.5,
trunc_upper = 4.5))
Besides scalar truncation points, the truncation points can also take vectors that give the impression of a discontinuous axis. Alternatively, you can also give functions, or formulas, that take the existing break positions and return truncation points.
g + guides(x = guide_axis_truncated(trunc_lower = c(2, 4),
trunc_upper = c(3, 5)),
y = guide_axis_truncated(trunc_lower = ~ .x - 1,
trunc_upper = ~ .x + 1))
A particular use-case I’ve found in the wilds of the internet, is to label divergent bar charts with separated axes.
df <- data.frame(x = seq(-3, 3, length.out = 6), y = LETTERS[1:6])
ggplot(df, aes(x, y)) +
geom_col() +
scale_x_continuous(
breaks = -3:0, guide = "axis_truncated",
sec.axis = dup_axis(
breaks = 0:3, guide = "axis_truncated"
)
) +
theme(axis.line.x = element_line())
Manual axes
The idea behind manual axes is to allow greater freedom in how breaks and labels are defined. In particular, one can use grid units to set the position of the breaks, as well as atomic data that is mapped to the scale, and set the label attributes as vectors.
g + guides(y.sec = guide_axis_manual(
breaks = unit(c(1, 3), "cm"),
labels = expression("treshold"^2, "annotation"[3]),
label_colour = c("red", "blue"), label_size = c(8, 12)
))
While this all might not seem immediately exiting, consider for a moment that discrete position scales don’t support secondary axes. Manual axis guides extend the capabilities of discrete scales by offering a way to specify secondary axes. One way to use this is to provide additional information about categories.
tab <- table(diamonds$cut)
ggplot(diamonds, aes(cut, price)) +
geom_violin() +
guides(x.sec = guide_axis_manual(
breaks = names(tab),
labels = paste0("n = ", tab)
))
Another use case is to highlight a few select cases of an otherwise crowded axis. More information on the dendrogram axis follows further below.
highlight <- c("New York", "California", "Alabama", "Hawaii")
clust <- hclust(dist(USArrests))
# Melting USArrests
df <- data.frame(
state = rownames(USArrests)[as.vector(row(USArrests))],
crime = colnames(USArrests)[as.vector(col(USArrests))],
value = as.vector(as.matrix(USArrests)),
row.names = NULL
)
ggplot(df, aes(crime, state, fill = value)) +
geom_raster() +
scale_y_dendrogram(hclust = clust, labels = NULL) +
guides(y.sec = guide_axis_manual(breaks = highlight, labels = highlight))
Minor ticks
Perhaps a more useful addition to the axis guide is to also place tick marks at places where the minor breaks are.
g + guides(x = "axis_minor", y = "axis_minor")
The guide constructor, guide_axis_minor()
has no unique
arguments, because the positions of the minor breaks are taken directly
from the scale. Therefore, you can use the minor_breaks
argument of the position scale to determine where the minor ticks are
drawn.
g + scale_x_continuous(
minor_breaks = seq(2, 4, by = 0.2),
guide = "axis_minor"
)
The length of the minor ticks are controlled by the
ggh4x.axis.ticks.length.minor
theme element, and are
specified relative to the major ticks using the rel()
function.
Logarithmic ticks
A variation on the minor ticks is placing ticks for logarithmic axes. A thing to note is that these work best log10 transformations. The ticks now come in three lengths, the major length, the minor length and what is referred to ‘mini’ length. Like with the minor ticks, the mini ticks are also defined relative to the major ticks.
pres <- ggplot(pressure, aes(temperature, pressure)) +
geom_line()
pres + scale_y_log10(guide = "axis_logticks") +
theme(axis.ticks.length.y = unit(0.5, "cm"),
ggh4x.axis.ticks.length.minor = rel(0.5),
ggh4x.axis.ticks.length.mini = rel(0.2))
A difference with the annotation_logticks
function is
that by default, ticks are placed on the outside of the panel and are
controlled by the theme. To get good looking log-ticks on the outside,
you’d have to set clipping off and dodge the axis text. Whereas using
log ticks as a guide takes care of most of the extra steps
naturally.
# Using annotation log-ticks
pres + scale_y_log10() +
annotation_logticks(sides = 'l', outside = TRUE) +
coord_cartesian(clip = "off") +
theme(axis.text.y = element_text(margin = margin(r = 10)))
# Using axis_logticks, setting tick length equivalently
pres + scale_y_log10(guide = "axis_logticks") +
theme(axis.ticks.length.y = unit(0.3, "cm"))
Inversely, you can also place the axis ticks on the inside with
negative values for the axis.ticks.length
, but you would
need to dodge the axis text again.
pres + scale_y_log10(guide = "axis_logticks") +
theme(axis.ticks.length.y = unit(-0.3, "cm"),
axis.text.y = element_text(margin = margin(r = 10)))
Nested relations
Discrete variables that have some kind of categories or interactions to them can be laid out in a nested fashion. This can be convenient to indicate for example group membership.
In the example below we use the interaction()
function
to paste together the name of the item and the group it belongs to, with
a "."
in between. The guide_axis_nested()
function tries to split the labels on the "."
symbol to
tease apart the item and their group membership.
df <- data.frame(
item = c("Coffee", "Tea", "Apple", "Pear", "Car"),
type = c("Drink", "Drink", "Fruit", "Fruit", ""),
amount = c(5, 1, 2, 3, 1),
stringsAsFactors = FALSE
)
ggplot(df, aes(interaction(item, type), amount)) +
geom_col() +
guides(x = "axis_nested")
Note above that the ordering of the x-axis is different than the
order of the input data. An alternative that can be tried is using the
paste0()
function to paste together names. In the case
below, the names are automatically ordered alphabetically, so the group
membership information is disorganised with paste0()
. Here,
we use the delim
argument to split the labels based on a
delimiter. The extra tildes (~) are to show where the split happens.
ggplot(df, aes(paste0(item, "~nonsense~", type), amount)) +
geom_col() +
guides(x = guide_axis_nested(delim = "nonsense"))
To counter some of the pains with interaction()
and
paste0()
in ordering the items, ggh4x has the
weave_factors()
convenience function that attempts to
preserve the natural order of factor levels in which they occur.
ggplot(df, aes(weave_factors(item, type), amount)) +
geom_col() +
guides(x = "axis_nested")
The looks of the indicator line can be controlled by the
ggh4x.axis.nestline
theme element (also *.x
and *.y
variants), which by default takes values from the
axis ticks. In the example below, the indicator line is red because it
takes the colour from the ticks, but is thicker because its own size is
set larger. Also the text underneath the indicators can be made
different with the ggh4x.axis.nesttext.(x/y)
elements.
ggplot(df, aes(weave_factors(item, type), amount)) +
geom_col() +
guides(x = "axis_nested") +
theme(
axis.ticks = element_line(colour = "red"),
ggh4x.axis.nestline.x = element_line(size = 2),
ggh4x.axis.nesttext.x = element_text(colour = "blue")
)
#> Warning: The `size` argument of `element_line()` is deprecated as of ggplot2 3.4.0.
#> ℹ Please use the `linewidth` argument instead.
#> This warning is displayed once every 8 hours.
#> Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
#> generated.
Lastly, the guide will also stack multiple group memberships.
Dendrograms
Another thing people might want to indicate are hierarchical
clustering relationships among variables. To accommodate this, there is
scale_(x/y)_dendrogram()
, which uses the
ggdendro package convert a hclust
object
into a dendrogram. Since any set of scales should only be dependent on
their aesthetics and cannot see other aesthetics, the dendrogram scales
requires you to provide it with the result of a hierarchical clustering.
By using the scale, it reorders the variables for you according to the
clustering result, and places a dendrogram over the labels.
clusters <- hclust(dist(USArrests), "ave")
# reshaping USArrests
df <- data.frame(
State = rownames(USArrests)[row(USArrests)],
variable = colnames(USArrests)[col(USArrests)],
value = unname(do.call(c, USArrests))
)
g <- ggplot(df, aes(variable, State, fill = value)) +
geom_raster()
g + scale_y_dendrogram(hclust = clusters)
Since the dendrogram replaces the axis ticks, it uses these as theme
element from which it takes the looks. The size of the dendrogram is
also controlled by the tick length: it is ten times the
axis.tick.length
element.
g + scale_y_dendrogram(hclust = clusters) +
theme(
axis.ticks.y = element_line(size = 2, lineend = "round"),
axis.ticks.length.y = unit(10, "pt")
)
There also is the guide_dendro()
function to tune some
of the guide behaviour. It is intended to be used within a
scale_(x/y)_dendrogram()
function.
g + scale_y_dendrogram(guide = guide_dendro(position = "right"),
hclust = clusters)
Using guide_dendro()
outside dendrogram scales is not
recommended, because that would break the relationship between the axis
order and the dendrogram that is displayed. To discourage this use, it
has been made slightly more inconvenient to use this function outside
dendrogram scales: you manually have to supply the digested dendrogram.
Notice below that the y-axis order and the dendrogram don’t match and is
their implied relationship is thus false.
# Don't do this
ggplot(df, aes(variable, State, fill = value)) +
geom_raster() +
guides(y = guide_dendro(dendro = ggdendro::dendro_data(clusters)))