rggplot2plotggh4x

how to fixed axis_nested in a facet plot?


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")

enter image description here

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:

enter image description here

But what I want is this:

enter image description here

The group labels in fixed position in x-direction and breaks in different groups in y-direction

If there are other solutions?


Solution

  • 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