51

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 image description here

What I would like to have is something like this:

enter image description here

Any ideas?

Henrik
  • 65,555
  • 14
  • 143
  • 159
pawels
  • 1,070
  • 1
  • 10
  • 16
  • 1
    To actually put the labels outside the panel as you picture will require some serious `grid` graphics wizardry. However, if you can accept having them inside the panel, `geom_text` can give you a solution. – Drew Steen Aug 10 '13 at 20:19
  • I'm on my phone, but this question has been asked several times. I'm sure a duplicate could be found by an enterprising Googler. – joran Aug 10 '13 at 20:31
  • 1
    @joran I can't find the duplicate question. SO I hope i haven't over-complicated the solution. – agstudy Aug 10 '13 at 23:55
  • Thanks Frank, but that's not what I was looking for. Fantastic job agstudy, I also tried to find the duplicate (again, without success) and use Drew Steen suggestion and it kind of worked, but your solution is perfect! – pawels Aug 11 '13 at 06:25
  • 1
    `xmax = Inf` should do the trick for annotation_custom (better would be `annotate("segment", ...)` or `annotate("hline", ...)`) – baptiste Aug 11 '13 at 13:34

7 Answers7

76

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 image description here

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 image description here

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())
aosmith
  • 34,856
  • 9
  • 84
  • 118
  • The effect is gorgeous and solution so simple. Although I don't think it would be fair to change the accepted answer right now. Great answer nonetheless. – pawels Apr 01 '16 at 07:05
19

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

enter image description here

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")
agstudy
  • 119,832
  • 17
  • 199
  • 261
9

I know this question is old but i would like to intoduce the cleanest way to do this with ggh4x

library(ggplot2)
library(ggh4x)

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)

# Only one more line of code with the function 'guide_axis_nested'
# and changing the data from x axis to interaction(Group,Category, sep = "!")

ggplot(data = data, aes(x = interaction(Group,Category, sep = "!"), y = Value, fill = Group)) + 
  geom_col(position = 'dodge', show.legend = FALSE) +
  geom_text(aes(label = paste(Value, "%")), 
            position = position_dodge(width = 0.9), vjust = -0.25) +
  scale_x_discrete(guide = guide_axis_nested(delim = "!"), name = "Category")

enter image description here

M Aurélio
  • 830
  • 5
  • 13
8

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 faceting to provide secondary label

AndrewMinCH
  • 710
  • 5
  • 8
7

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 image description here

baptiste
  • 75,767
  • 19
  • 198
  • 294
  • +10! Nice idea to create the `textGrob` by ggplot2( avoid viewports low level). Why are you using an alias? and you can also remove the legend ( no more need). – agstudy Aug 11 '13 at 02:32
  • i believe it should be named `gtable_add_grobs` for consistency with add_rows, etc., and if the change is made in the future all these previous answers would have to be edited. – baptiste Aug 11 '13 at 12:07
4

@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 image description here

pawels
  • 1,070
  • 1
  • 10
  • 16
  • 1
    Nice solution :), it might be a little better if you were to just include the code which is specific for the problem only. Additional code can often confuse people. :) – nathaneastwood Jan 19 '15 at 10:05
  • You'll have to replace geom_bar(position = 'dodge') with geom_bar(position= 'dodge', stat='identity') to make it work. – vpicaver Feb 05 '15 at 16:54
2

Here's another solution using a package I'm working on for grouped bar charts (ggNestedBarChart):

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)

devtools::install_github("davedgd/ggNestedBarChart")
library(ggNestedBarChart)
library(scales)

p1 <- ggplot(data, aes(x = Category, y = Value/100, fill = Category), stat = "identity") +
  geom_bar(stat = "identity") +
  facet_wrap(vars(Category, Group), strip.position = "top", scales = "free_x", nrow = 1) +
  theme_bw(base_size = 13) +
  theme(panel.spacing = unit(0, "lines"),
        strip.background = element_rect(color = "black", size = 0, fill = "grey92"),
        strip.placement = "outside",
        axis.text.x = element_blank(),
        axis.ticks.x = element_blank(),
        panel.grid.major.y = element_line(colour = "grey"),
        panel.grid.major.x = element_blank(),
        panel.grid.minor = element_blank(),
        panel.border = element_rect(color = "black", fill = NA, size = 0),
        panel.background = element_rect(fill = "white"),
        legend.position = "none") + 
  scale_y_continuous(expand = expand_scale(mult = c(0, .1)), labels = percent) + 
  geom_text(aes(label = paste0(Value, "%")), position = position_stack(0.5), color = "white", fontface = "bold")

ggNestedBarChart(p1)

ggsave("p1.png", width = 10, height = 5)

example plot

Note that ggNestedBarChart can group as many levels as necessary and isn't limited to just two (i.e., Category and Group in this example). For instance, using data(mtcars):

deep nesting/grouping

Code for this example is on the GitHub page.

Community
  • 1
  • 1
davedgd
  • 387
  • 2
  • 6
  • Hi could you please help me to install your ggNestedBarChart package as I'm trying to use it but I cannot install it. – Marwah Al-kaabi Feb 18 '23 at 23:44
  • 1
    Happy to help with that. The instructions above will work, but only if you install devtools first. Try running this before the instructions above: `install.packages("devtools")` I'd also install the other dependencies like scales to ensure you have them: `install.packages("devtools", "scales", "ggplot2")` – davedgd Feb 22 '23 at 00:43
  • Thank you so much, your help is appreciated. I installed the package and used the examples and it worked perfectly. but for my data when I use ```ggNestedBarChart(p1)``` , I get this error: ```Error in while (nrow(buildReplacementFacet) + 1 < j * 2) buildReplacementFacet <- rbind(buildReplacementFacet, : argument is of length zero``` Can you please help me with this? Many thanks. Its really nice packge and do exactly what I want. Thank you so much for developing it. – Marwah Al-kaabi Feb 24 '23 at 00:57
  • @MarwahAl-kaabi, Ah, I see: That sounds a bit more complex to diagnose and potentially an issue with the package code. Would you mind opening an issue on GitHub with as much detail as you can provide, and then I can help you diagnose from there? – davedgd Feb 24 '23 at 20:59