Multirow axis labels with nested grouping variable

2019-01-01 00:49发布

问题:

I would like the levels of two different nested grouping variables to appear on separate lines below the plot, and not in the legend. What I have right now is this code:

data <- read.table(text = \"Group Category Value
    S1 A   73
    S2 A   57
    S1 B   7
    S2 B   23
    S1 C   51
    S2 C   87\", header = TRUE)

ggplot(data = data, aes(x = Category, y = Value, fill = Group)) + 
  geom_bar(position = \'dodge\') +
  geom_text(aes(label = paste(Value, \"%\")), 
            position = position_dodge(width = 0.9), vjust = -0.25)

\"enter

What I would like to have is something like this:

\"enter

Any ideas?

回答1:

You can create a custom element function for axis.text.x.

\"enter

library(ggplot2)
library(grid)

## create some data with asymmetric fill aes to generalize solution 
data <- read.table(text = \"Group Category Value
                   S1 A   73
                   S2 A   57
                   S3 A   57
                   S4 A   57
                   S1 B   7
                   S2 B   23
                   S3 B   57
                   S1 C   51
                   S2 C   57
                   S3 C   87\", header=TRUE)

# user-level interface 
axis.groups = function(groups) {
  structure(
    list(groups=groups),
    ## inheritance since it should be a element_text
    class = c(\"element_custom\",\"element_blank\")  
  )
}
# returns a gTree with two children: 
# the categories axis
# the groups axis
element_grob.element_custom <- function(element, x,...)  {
  cat <- list(...)[[1]]
  groups <- element$group
  ll <- by(data$Group,data$Category,I)
  tt <- as.numeric(x)
  grbs <- Map(function(z,t){
    labs <- ll[[z]]
    vp = viewport(
             x = unit(t,\'native\'), 
             height=unit(2,\'line\'),
             width=unit(diff(tt)[1],\'native\'),
             xscale=c(0,length(labs)))
    grid.rect(vp=vp)
    textGrob(labs,x= unit(seq_along(labs)-0.5,
                                \'native\'),
             y=unit(2,\'line\'),
             vp=vp)
  },cat,tt)
  g.X <- textGrob(cat, x=x)
  gTree(children=gList(do.call(gList,grbs),g.X), cl = \"custom_axis\")
}

## # gTrees don\'t know their size 
grobHeight.custom_axis = 
  heightDetails.custom_axis = function(x, ...)
  unit(3, \"lines\")

## the final plot call
ggplot(data=data, aes(x=Category, y=Value, fill=Group)) + 
  geom_bar(position = position_dodge(width=0.9),stat=\'identity\') +
  geom_text(aes(label=paste(Value, \"%\")),
            position=position_dodge(width=0.9), vjust=-0.25)+
  theme(axis.text.x = axis.groups(unique(data$Group)),
        legend.position=\"none\")


回答2:

The strip.position argument in facet_wrap() and switch argument in facet_grid() since ggplot2 2.2.0 now makes the creation of a simple version of this plot fairly straightforward via faceting. To give the plot the uninterrupted look, set the panel.spacing to 0.

Here\'s the example using the dataset with a different number of Groups per Category from @agtudy\'s answer.

  • I used scales = \"free_x\" to drop the extra Group from the Categories that don\'t have it, although this won\'t always be desirable.
  • The strip.position = \"bottom\" argument moves the facet labels to the bottom. I removed the strip background all together with strip.background, but I could see that leaving the strip rectangle would be useful in some situations.
  • I used width = 1 to make the bars within each Category touch - they\'d have spaces between them by default.

I also use strip.placement and strip.background in theme to get the strips on the bottom and remove the strip rectangle.

The code for versions of ggplot2_2.2.0 or newer:

ggplot(data = data, aes(x = Group, y = Value, fill = Group)) + 
    geom_bar(stat = \"identity\", width = 1) +
    geom_text(aes(label = paste(Value, \"%\")), vjust = -0.25) +
    facet_wrap(~Category, strip.position = \"bottom\", scales = \"free_x\") +
    theme(panel.spacing = unit(0, \"lines\"), 
         strip.background = element_blank(),
         strip.placement = \"outside\")

\"enter

You could use space= \"free_x\" in facet_grid() if you wanted all the bars to be the same width regardless of how many Groups per Category. Note that this uses switch = \"x\" instead of strip.position. You also might want to change the label of the x axis; I wasn\'t sure what it should be, maybe Category instead of Group?

ggplot(data = data, aes(x = Group, y = Value, fill = Group)) + 
    geom_bar(stat = \"identity\", width = 1) +
    geom_text(aes(label = paste(Value, \"%\")), vjust = -0.25) +
    facet_grid(~Category, switch = \"x\", scales = \"free_x\", space = \"free_x\") +
    theme(panel.spacing = unit(0, \"lines\"), 
         strip.background = element_blank(),
         strip.placement = \"outside\") + 
    xlab(\"Category\")

\"enter

Older code versions

The code for ggplot2_2.0.0, when this feature was first introduced, was a little different. I\'ve saved it below for posterity:

ggplot(data = data, aes(x = Group, y = Value, fill = Group)) + 
    geom_bar(stat = \"identity\") +
    geom_text(aes(label = paste(Value, \"%\")), vjust = -0.25) +
    facet_wrap(~Category, switch = \"x\", scales = \"free_x\") +
    theme(panel.margin = unit(0, \"lines\"), 
         strip.background = element_blank())


回答3:

An alternative to agstudy\'s method is to edit the gtable and insert an \"axis\" calculated by ggplot2,

p <- ggplot(data=data, aes(x=Category, y=Value, fill=Group)) + 
  geom_bar(position = position_dodge(width=0.9),stat=\'identity\') +
  geom_text(aes(label=paste(Value, \"%\")),
            position=position_dodge(width=0.9), vjust=-0.25)

axis <- ggplot(data=data, aes(x=Category, y=Value, colour=Group)) +
  geom_text(aes(label=Group, y=0),
            position=position_dodge(width=0.9))

annotation <- gtable_filter(ggplotGrob(axis), \"panel\", trim=TRUE)
annotation[[\"grobs\"]][[1]][[\"children\"]][c(1,3)] <- NULL #only keep textGrob

library(gtable)
g <- ggplotGrob(p)
gtable_add_grobs <- gtable_add_grob # let\'s use this alias
g <- gtable_add_rows(g, unit(1,\"line\"), pos=4)
g <- gtable_add_grobs(g, annotation, t=5, b=5, l=4, r=4)
grid.newpage()
grid.draw(g)

\"enter



回答4:

A very simple solution which gives a similar (though not identical) result is to use faceting. The downside is that the Category label is above rather than below.

ggplot(data=data, aes(x=Group, y=Value, fill=Group)) +
  geom_bar(position = \'dodge\', stat=\"identity\") +
  geom_text(aes(label=paste(Value, \"%\")), position=position_dodge(width=0.9), vjust=-0.25) + 
  facet_grid(. ~ Category) + 
  theme(legend.position=\"none\")

\"Using



回答5:

@agstudy already answered this question and I\'m going to use it myself, but if you\'d accept something uglier, but simpler, this is what I came with before his answer:

data <- read.table(text = \"Group Category Value
    S1 A   73
    S2 A   57
    S1 B   7
    S2 B   23
    S1 C   51
    S2 C   87\", header=TRUE)

p <- ggplot(data=data, aes(x=Category, y=Value, fill=Group))
p + geom_bar(position = \'dodge\') +
  geom_text(aes(label=paste(Value, \"%\")), position=position_dodge(width=0.9),   vjust=-0.25) +
  geom_text(colour=\"darkgray\", aes(y=-3, label=Group),  position=position_dodge(width=0.9), col=gray) +
  theme(legend.position = \"none\", 
    panel.background=element_blank(),
    axis.line = element_line(colour = \"black\"),
    axis.line.x = element_line(colour = \"white\"),
    axis.ticks.x = element_blank(),
    panel.grid.major = element_blank(),
    panel.grid.minor = element_blank(),
    panel.border = element_blank(),
    panel.background = element_blank()) +
  annotate(\"segment\", x = 0, xend = Inf, y = 0, yend = 0)

Which will give us:

\"enter