6

**Edit, there are two great solutions here, one is marked as the answer, but @hrbrmstr provides a great solution combining two ggplots which works well for this simple plot.*

Here's the code

breaks.major <- c(0,15,37.5,52.5,67.5,82.5,95,100) #defines the midpoints of the categories (label locations)
breaks.minor <- c(30,45,60,75,90) #defines the edges of the categories (second label set I need)
labels.minor <- c("","Extremely \nDissatisfied","Dissatisfied","Uncertain","Satisfied","Very \nSatisfied","Extremely \nSatisfied","")
lims =c(0,100)
g <- ggplot(mpg, aes(class))+
  geom_bar()+
  coord_flip()+
  scale_y_continuous(limit = lims, minor_breaks = breaks.minor, breaks = breaks.major, labels = labels.minor) +
  theme(panel.grid.major.x = element_blank()) +
  theme(panel.grid.major.y = element_blank()) +
  theme(axis.ticks.x=element_blank()) +
  theme(axis.title= element_blank()) 

It produces this plot:

enter image description here

I need to have two sets of X-axis labels, one showing the category names (i.e. the "satisfied" etc. that are already there via labels.minor), and one showing the values at the breaks.minor locations (corresponding to the category limits, i.e. the vertical panel grid lines). I need the current labels.minor labels to be below the required additional labels.

I currently do this with line breaks so that the numbers and categories are all in one long string, but the spacing gets funny with plot resizes.I could do this with text boxes (I assume), is there a way within ggplot?

Extra points if you get my current labels in the centre of their sections (e.g. "Extremely Satisfied" is off-centre)

This is my desired output (pardon my 'mspaint')

enter image description here

Alex
  • 971
  • 4
  • 15
  • 28
  • You seem to want the `labels.minor` labels to be below the values at the `breaks.minor` locations, but the former has 6 elements while the latter has 5. Am I missing something? – Weihuang Wong Sep 27 '16 at 06:43
  • @Weihuang Wong 6 and 7 actually (including 0 and 100), these are category boundaries and category mid points. See updated example of desired output. – Alex Sep 27 '16 at 09:24

3 Answers3

7

I think this does what you're looking for:

library(ggplot2)
library(grid)
library(gtable)
library(gridExtra)

breaks.major <- c(0, 15, 37.5, 52.5, 67.5, 82.5, 95, 100)
breaks.minor <- c(30, 45, 60, 75, 90)
labels.minor <- c("", "Extremely\nDissatisfied", "Dissatisfied", "Uncertain",
                  "Satisfied", "Very\nSatisfied", "Extremely\nSatisfied", "")
lims <- c(0, 100)

# build the main plot with the text axis
gg1 <- ggplot(mpg, aes(class))
gg1 <- gg1 + geom_bar()
gg1 <- gg1 + scale_y_continuous(expand=c(0,0), limit=lims, 
                                minor_breaks=breaks.minor, 
                                breaks=breaks.major,
                                labels=labels.minor)
gg1 <- gg1 + coord_flip()
gg1 <- gg1 + theme(panel.grid.major.x=element_blank())
gg1 <- gg1 + theme(panel.grid.major.y=element_blank())
gg1 <- gg1 + theme(axis.ticks.x=element_blank())
gg1 <- gg1 + theme(axis.title=element_blank())

# let ggplot2 do the work of building the second axis
gg2 <- ggplot(mpg, aes(class))
gg2 <- gg2 + scale_y_continuous(expand=c(0,0), limit=lims, 
                                breaks=c(0, breaks.minor, 100))
gg2 <- gg2 + coord_flip()
gg2 <- gg2 + theme(axis.ticks.x=element_blank())
gg2 <- gg2 + theme(axis.text.x=element_text(hjust=c(0, 0.5, 0.5, 0.5, 0.5, 0.5, 1)))

gt1 <- ggplot_gtable(ggplot_build(gg1))
gt2 <- ggplot_gtable(ggplot_build(gg2))
axis2 <- grid.arrange(gt2$grobs[[5]])

gt <- gtable_add_rows(gt1, unit(0.1, "null"), 4)
grid.arrange(gtable_add_grob(gt, axis2, t=5, l=4, b=5, r=4))

enter image description here

hrbrmstr
  • 77,368
  • 11
  • 139
  • 205
  • Excellent solution that works very well with this example code. Sadly my actual plot code is much longer, and has conditional error bars and other complicated formatting. Effectively doubling the code for the plotting routine would blow things out. Can I mark two answers as correct?Because this technically is. – Alex Sep 27 '16 at 22:42
  • I'm fairly certain this can be made a bit more minimalistic. I'll give it a go later this eve. – hrbrmstr Sep 27 '16 at 22:47
  • Hey @hrbrmstr did you figure out how to condense this? – Alex Oct 03 '16 at 06:12
6

Something like this, perhaps. Note the setting of expand for both axes to deal with proper spacing, and positions of the category names.

The labels in your figure aren't really off-centre, they are in the centre of their category boundary. It's just that by default the axes are expanded a bit further.

If you want to get more fancy, you can draw outside of the plotting area too, but it requires a bit more fiddeling. This question should get you started.

ggplot(mpg, aes(class))+
  geom_bar()+
  geom_text(data = data.frame(br = breaks.minor), aes(y = br, label = br, x = 7.75),
            size = 4, col = 'grey30') +
  coord_flip()+
  scale_y_continuous(limit = lims, minor_breaks = breaks.minor, 
                     breaks = breaks.major, labels = labels.minor, 
                     expand = c(0, 0)) +
  scale_x_discrete(expand = c(0.05, 0)) +
  theme(panel.grid.major.x = element_blank()) +
  theme(panel.grid.major.y = element_blank()) +
  theme(axis.ticks.x=element_blank()) +
  theme(axis.title= element_blank())

enter image description here

Community
  • 1
  • 1
Axeman
  • 32,068
  • 8
  • 81
  • 94
  • Awesome @Axeman, thanks for the effort and the link I'll continue on. I do, unfortunately, need the numbers between the plot and the 'satisfied' etc however (see updated figure in op). I probably wasn't clear on that, though your suggestion might also have been an alternative approach given limitations in ggplot to do this natively. – Alex Sep 27 '16 at 08:28
  • Just replace `x` in `geom_text` with something smaller to move the numbers down to get them close. – Axeman Sep 27 '16 at 08:33
  • You could also just add all of those as `breaks`, and use line breaks (`\n`) to move some labels up or down. – Axeman Sep 27 '16 at 08:35
  • of course RE changing `x`, silly oversight. And as to using `breaks`, that was how this code was originally, as one long character string with spaces and breaks to line everything up. Sadly it needs to be adjusted as the plot changes size occasionally. – Alex Sep 27 '16 at 09:23
  • Hey mate, i'm getting weird issues when using this when I split my bars using `fill` in the `ggplot` call. Unless the length of `breaks.minor` is the same as the length of the labels for the y axis (after `coord_flip`) I get an error `Error: Aesthetics must be either length 1 or the same as the data (12): x, y, label, fill`. So bizarre, how can I have the `geom_text` act independently of the data in the plot? It feels like this should be easy!!! – Alex Oct 03 '16 at 06:09
4

I wrote all labels as major-labels.

# OP's
breaks.major <- c(0,15,37.5,52.5,67.5,82.5,95,100) #defines the midpoints of the categories (label locations)
breaks.minor <- c(30,45,60,75,90) #defines the edges of the categories (second label set I need)
labels.minor <- c("","Extremely \nDissatisfied","Dissatisfied","Uncertain","Satisfied","Very \nSatisfied","Extremely \nSatisfied","")
lims =c(0,100)

breaks.major2 <- c(0,15,37.5,52.5,67.5,82.5,95)
breaks.minor2 <- c(30,45,60,75,90,100)       # put 100 into minor from major

breaks.comb <- sort(c(breaks.major2, breaks.minor2 - 1.0E-6))  # avoid the just same value as minor
label.comb <- c(0, "\nExtremely \nDissatisfied", 30, "\nDissatisfied", 45, "\nUncertain", 60, 
                "\nSatisfied", 75, "\nVery \nSatisfied", 90, "\nExtremely \nSatisfied", 100)

library(ggplot2)
g <- ggplot(mpg, aes(class))+
  geom_bar()+
  coord_flip()+
  scale_y_continuous(limit = lims, minor_breaks = breaks.minor2, breaks = breaks.comb, 
                     labels = label.comb, expand = c(0,0)) +
  theme(panel.grid.major.x = element_blank()) +
  theme(panel.grid.major.y = element_blank()) +
  theme(axis.ticks.x=element_blank()) +
  theme(axis.title= element_blank()) +
  theme(plot.margin = unit(c(0.5, 0.5, 0.5, 0.5), "lines"))

enter image description here

cuttlefish44
  • 6,586
  • 2
  • 17
  • 34