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
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.sec are the bottom and top axes respectively and likewise
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.
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
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"))
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
minor_breaks argument of the position scale to determine where the minor ticks are drawn.
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 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)))
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.
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") )
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
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
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)))