I'm drawing a plot with annotation of y-axis labels using ggplot2 and ggh4x in R.
how to fixed the position of axis_nested in a facet plot with scales="free_y"
Here is the data:
ggd = data.frame(
y=c("short1","short2", "loooooooooooooooooooooong"),
x=c(1,2,4),
g=c("A", "A", "B")
)
I can get a fixed position axis_nested with:
ggplot(ggd, aes(x,interaction(y,g)))+
geom_bar(stat="identity")+
labs(y=NULL)+
guides(y="axis_nested")
If I facet the plot:
ggplot(ggd, aes(x,interaction(y,g)))+
geom_bar(stat="identity")+
labs(y=NULL)+
guides(y="axis_nested")+
facet_nested(g~., scales="free_y", space = "free")
I get this:
But what I want is this:
The group labels in fixed position in x-direction and breaks in different groups in y-direction
If there are other solutions?
I'm drawing a plot with annotation of y-axis labels using ggplot2 and ggh4x in R.
how to fixed the position of axis_nested in a facet plot with scales="free_y"
Here is the data:
ggd = data.frame(
y=c("short1","short2", "loooooooooooooooooooooong"),
x=c(1,2,4),
g=c("A", "A", "B")
)
I can get a fixed position axis_nested with:
ggplot(ggd, aes(x,interaction(y,g)))+
geom_bar(stat="identity")+
labs(y=NULL)+
guides(y="axis_nested")
If I facet the plot:
ggplot(ggd, aes(x,interaction(y,g)))+
geom_bar(stat="identity")+
labs(y=NULL)+
guides(y="axis_nested")+
facet_nested(g~., scales="free_y", space = "free")
I get this:
But what I want is this:
The group labels in fixed position in x-direction and breaks in different groups in y-direction
If there are other solutions?
Share Improve this question edited Apr 1 at 6:29 Edward 20.1k3 gold badges16 silver badges35 bronze badges asked Apr 1 at 3:11 NiceBNiceB 431 silver badge3 bronze badges New contributor NiceB is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct. 1 |2 Answers
Reset to default 3I'm afraid there's no easy way to achieve this. The underlying problem is that the width of the axis text grobs is calculated per panel. Instead, one option would be to manipulate the `gtable', i.e. in the code below I set the width of the axis text grobs for all panels equal to the panel with the maximum width.
To make the example a bit more interesting, I added a third panel and used a non-standard font to show that the approach works for more general cases, whereas padding with spaces will only work for some fonts (see below). Finally, note that I switched to legendry::guide_axis_nested
as ggh4x::guide_axis_nested
is deprecated and the warnings suggest to switch to legendry
.)
ggd <- data.frame(
y = c("short1", "short2", "loooooooooooooooooooong", "shorter", "looooooooooooong"),
x = c(1, 2, 4, 5, 6),
g = c("A", "A", "B", "C", "C")
)
library(ggplot2)
library(ggh4x)
library(glue)
gg <- ggplot(ggd, aes(x, interaction(y, g))) +
geom_bar(stat = "identity") +
labs(y = NULL) +
guides(
y = legendry::guide_axis_nested(
drop_zero = FALSE
)
) +
facet_nested(g ~ ., scales = "free_y", space = "free") +
theme(axis.text.y.left = element_text(size = 12, family = "Arial Black"))
gt <- ggplotGrob(gg)
# Indices of left axes in the layout
ix_axis_l <- which(grepl("^axis-l", gt$layout$name))
# Index of the axis with the maximum axis text width
ix_max_width <- ix_axis_l[
which.max(sapply(ix_axis_l, \(x) gt$grobs[[x]]$widths[[1]]))
]
# Set the widths for the axis text grobs according to the max width
# ... and reset the viewports
ix_adjust <- setdiff(ix_axis_l, ix_max_width)
gt$grobs[ix_adjust] <- lapply(
gt$grobs[ix_adjust],
\(x) {
x$vp <- grid::viewport()
# 3 = Axis Text Grob
x$children$layout$grobs[[3]]$vp <- grid::viewport()
x$children$layout$widths <- gt$grobs[[ix_max_width]]$children$layout$widths
x$children$layout$grobs[[3]]$children$layout$widths <-
gt$grobs[[ix_max_width]]$children$layout$grobs[[3]]$children$layout$widths
x
}
)
plot(gt)
While padding with spaces works for monospaced fonts, it will not work in general, e.g. for the non-standard font the lines and the top level axis text are no longer aligned:
ggd$width <- strwidth(ggd$y, units = "inches")
ggd$pads <- round((max(ggd$width) - ggd$width) /
strwidth(" ", units = "inches"))
ggd$y <- paste0(strrep(" ", ggd$pads), ggd$y)
ggplot(ggd, aes(x, interaction(y, g))) +
geom_bar(stat = "identity") +
labs(y = NULL) +
guides(y = "axis_nested") +
facet_nested(g ~ ., scales = "free_y", space = "free") +
theme(axis.text.y.left = element_text(size = 12, family = "Arial Black"))
Created on 2025-04-01 with reprex v2.1.1
Option 1 - use thin spaces to pad all labels to the same length
One workaround could be to left-pad all labels from ggd$y
to have the same width as the longest string.
Edit: Since I've been told this "does not work for monospaced fonts" - this guesses a number of spaces to fill up the shorter strings. If not all letters of this font have the same width, the vertical line will be not perfectly aligned by +- 1 space - Courier is a monospaced font, using it will completely remove this issue. Another way to to mitigate it is to you use a thin space
UTF-8 like "\u2009"
character instead of the space.
library(ggplot2)
library(ggh4x)
ggd$width <- strwidth(ggd$y, units = "inches")
ggd$pads <- round((max(ggd$width) - ggd$width) /
strwidth("\u2009", units = "inches"))
ggd$y <- paste0(strrep("\u2009", ggd$pads), ggd$y)
ggplot(ggd, aes(x,interaction(y,g)))+
geom_bar(stat="identity")+
labs(y=NULL)+
scale_y_discrete(guide = guide_axis_nested()) +
facet_nested(g~., scales="free_y", space = "free") +
theme(axis.text.y.left = element_text(size = 12, family = "Arial Black")) # also used a non-monospaced font
giving
Option 2 - use wordwrap
If your labels are really long though, it might be best to use a string wrap with str_replace_all
. One could also use str_wrap
ggd = data.frame(
y=c("short1","short2", "This is a really really really long string"),
x=c(1,2,4),
g=c("A", "A", "B")
)
ggd$y <- stringr::str_replace_all(ggd$y, sprintf("(.{%s})", min(nchar(ggd$y))), "\\1\n")
ggplot(ggd, aes(x,interaction(y,g)))+
geom_bar(stat="identity")+
labs(y=NULL)+
guides(y="axis_nested")+
facet_nested(g~., scales="free_y", space = "free")
Option 3 - use inv = true
To mitigate this problem entirely, you can invert the order of the group letter and the label using inv = TRUE
library(ggplot2)
library(ggh4x)
ggd = data.frame(
y=c("short1","short2", "loooooooooooooooooooooong"),
x=c(1,2,4),
g=c("A", "A", "B")
)
ggplot(ggd, aes(x,interaction(y,g)))+
geom_bar(stat="identity")+
labs(y=NULL)+
scale_y_discrete(guide = guide_axis_nested(inv=T)) +
facet_nested(g~., scales="free_y", space = "free") +
theme(axis.text.y.left = element_text(size = 12, family = "Arial Black")) # also used a non-monospaced font
giving
ggd = data.frame(y=c(paste0(strrep(" ",39),"short1"),paste0(strrep(" ",39),"short2"), "loooooooooooooooooooooong"),x=c(1,2,4),g=c("A", "A", "B"))
– Tim G Commented Apr 1 at 11:11