最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

r - how to fixed axis_nested in a facet plot? - Stack Overflow

programmeradmin5浏览0评论

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
  • 1 you can try something hacky haha 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
Add a comment  | 

2 Answers 2

Reset to default 3

I'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

发布评论

评论列表(0)

  1. 暂无评论