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
y.sec axes. You can
either specify a guide by name omitting the
"axis", or by calling the guide constructor,
guide_axis(), that allows you to give additional arguments.
Unless you specify the
position argument to guides
x.sec are the bottom and top
axes respectively and likewise
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.
The final place you can specify axes is as an argument to secondary axes in continuous position scales.
The main takeaways that apply also to ggh4x’s position guides are the following:
This position guide has the convenience argument
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"))
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())
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_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())
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"), 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))
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
argument of the position scale to determine where the minor ticks are
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
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.
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
# 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)))
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
to paste together the name of the item and the group it belongs to, with
"." in between. The
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
we use the
delim argument to split the labels based on a
delimiter. The extra tildes (~) are to show where the split happens.
To counter some of the pains with
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.
The looks of the indicator line can be controlled by the
ggh4x.axis.nestline theme element (also
*.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
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.
Lastly, the guide will also stack multiple group memberships.
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
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
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
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)))