ggplot2 - annotate outside of plot
Asked Answered
F

6

89

I would like to associate sample size values with points on a plot. I can use geom_text to position the numbers near the points, but this is messy. It would be much cleaner to line them up along the outside edge of the plot.

For instance, I have:

df=data.frame(y=c("cat1","cat2","cat3"),x=c(12,10,14),n=c(5,15,20))

ggplot(df,aes(x=x,y=y,label=n))+geom_point()+geom_text(size=8,hjust=-0.5)

Which produces this plot: enter image description here

I would prefer something more like this: enter image description here

I know I can create a second plot and use grid.arrange (a la this post) but it would be tedious to determine the spacing of the textGrobs to line up with the y-axis. Is there an easier way to do this? Thanks!

Farrish answered 13/9, 2012 at 15:38 Comment(5)
This could be done with secondary axis which I think it's under development. But if you want to give it a try follow this link groups.google.com/forum/?fromgroups=#!topic/ggplot2/_3Pm-JEoCqEDecarlo
Hmm interesting...I was wondering if Hadley was going to implement this. However, I'm getting some weird errors trying to load devtools: call: if (!version_match) { error: argument is of length zero.Farrish
All I can say is that devtools works for me. You should try posting a question if you cannot solve it.Decarlo
I worked around it by installing ggplot2 0.9.2.1 from the .zip on CRAN. Now the code provided in the link by @LucianoSelzer doesn't run (multiple arguments to the guide_axis). Maybe too much for tonight? I'll sleep on it and see if I can't figure it out in the morningFarrish
see also https://mcmap.net/q/103966/-how-to-place-grobs-with-annotation_custom-at-precise-areas-of-the-plot-regionSimplicity
E
67

You don't need to be drawing a second plot. You can use annotation_custom to position grobs anywhere inside or outside the plotting area. The positioning of the grobs is in terms of the data coordinates. Assuming that "5", "10", "15" align with "cat1", "cat2", "cat3", the vertical positioning of the textGrobs is taken care of - the y-coordinates of your three textGrobs are given by the y-coordinates of the three data points. By default, ggplot2 clips grobs to the plotting area but the clipping can be overridden. The relevant margin needs to be widened to make room for the grob. The following (using ggplot2 0.9.2) gives a plot similar to your second plot:

library (ggplot2)
library(grid)

df=data.frame(y=c("cat1","cat2","cat3"),x=c(12,10,14),n=c(5,15,20))

p <- ggplot(df, aes(x,y)) + geom_point() +            # Base plot
     theme(plot.margin = unit(c(1,3,1,1), "lines"))   # Make room for the grob

for (i in 1:length(df$n))  {
p <- p + annotation_custom(
      grob = textGrob(label = df$n[i], hjust = 0, gp = gpar(cex = 1.5)),
      ymin = df$y[i],      # Vertical position of the textGrob
      ymax = df$y[i],
      xmin = 14.3,         # Note: The grobs are positioned outside the plot area
      xmax = 14.3)
 }    

# Code to override clipping
gt <- ggplot_gtable(ggplot_build(p))
gt$layout$clip[gt$layout$name == "panel"] <- "off"
grid.draw(gt)

enter image description here

Errhine answered 14/9, 2012 at 2:48 Comment(8)
isn't it easier to have one geom_text layer at x=Inf, hjust>=1, and turn off clipping?Simplicity
@jslefche, you should note that the solution offered by @Simplicity is much simpler. p = p + geom_text(aes(label = n, x = Inf, y = y), hjust = -1). Then turn off the clipping. Although the alignment can be off slightly.Errhine
and how does one turn off clipping?Hanako
@ThomasBrowne To turn off clipping, see the last three lines of code above.Errhine
What if I want to put the labels on the left axis in a plot with scale_x_log10 (so x cannot be less than zero)? I tried with geom_text(aes(label = label, x = 0, fontface = label.style), size = data$label.size, hjust = 1), but this put the labels attached to the border of the panel border. to move them a bit more to the left I added white space to the labels text. It works fine if all the labels are styled the same, but since I have different sizes for some of them, the white spaces size also is different, offsetting some labels more.Palestine
@Palestine Did you try hjust greater then 1?Errhine
I tried, using values other than 1, 0.5, 0 produce quite unpredictable alignments (didn't investigate to understand the pattern). Instead I discovered that to overcome the lower limit problem you just have to put the x argument of geom_text outside aes. Then errors are not raised anymore so you can move the text wherever you want!Palestine
Unfortunately the x position at which you need to position the text change with the range of the x axis. So you need to use a value of x that is relative to the axis range. Here's my solution. I get the x ggplot computed axis range with xlim.range <- ggplot_build(plot)$panel$ranges[[1]]$x.range. Then I use this as x position: x = xlim.range[1] - diff(xlim.range)/10 and it works!Palestine
F
94

This is now straightforward with ggplot2 3.0.0, since now clipping can be disabled in plots by using the clip = 'off' argument in coordinate functions such as coord_cartesian(clip = 'off') or coord_fixed(clip = 'off'). Here's an example below.

    # Generate data
    df <- data.frame(y=c("cat1","cat2","cat3"),
                     x=c(12,10,14),
                     n=c(5,15,20))

    # Create the plot
    ggplot(df,aes(x=x,y=y,label=n)) +
      geom_point()+
      geom_text(x = 14.25, # Set the position of the text to always be at '14.25'
                hjust = 0,
                size = 8) +
      coord_cartesian(xlim = c(10, 14), # This focuses the x-axis on the range of interest
                      clip = 'off') +   # This keeps the labels from disappearing
      theme(plot.margin = unit(c(1,3,1,1), "lines")) # This widens the right margin

enter image description here

Finedrawn answered 12/7, 2018 at 18:52 Comment(6)
How can you do this for x-axis of a box-plot?Hoyos
This is definitely the easiest option, thanks! @Hoyos maybe you sorted this by now, but you would use exactly the same argument but specifying ylim instead or xlimHearttoheart
For completeness, clip = off can also be called @ coord_flip (and I assume coord_x as well). Adding coord_cartesian(clip = 'off') was not a solution for me as I required coord_flip.Viaticum
That's a good point, thanks @LeroyTyrone. I just updated the answer to reflect that.Finedrawn
It's worth noting that this does not appear to work with coord_polarLegalize
Does anyone know how the code should be if the x-axis have dates? I was trying with as.Date("2020-09-01") but get the message "Error in as.Date() : 'origin' must be supplied"Awestricken
E
67

You don't need to be drawing a second plot. You can use annotation_custom to position grobs anywhere inside or outside the plotting area. The positioning of the grobs is in terms of the data coordinates. Assuming that "5", "10", "15" align with "cat1", "cat2", "cat3", the vertical positioning of the textGrobs is taken care of - the y-coordinates of your three textGrobs are given by the y-coordinates of the three data points. By default, ggplot2 clips grobs to the plotting area but the clipping can be overridden. The relevant margin needs to be widened to make room for the grob. The following (using ggplot2 0.9.2) gives a plot similar to your second plot:

library (ggplot2)
library(grid)

df=data.frame(y=c("cat1","cat2","cat3"),x=c(12,10,14),n=c(5,15,20))

p <- ggplot(df, aes(x,y)) + geom_point() +            # Base plot
     theme(plot.margin = unit(c(1,3,1,1), "lines"))   # Make room for the grob

for (i in 1:length(df$n))  {
p <- p + annotation_custom(
      grob = textGrob(label = df$n[i], hjust = 0, gp = gpar(cex = 1.5)),
      ymin = df$y[i],      # Vertical position of the textGrob
      ymax = df$y[i],
      xmin = 14.3,         # Note: The grobs are positioned outside the plot area
      xmax = 14.3)
 }    

# Code to override clipping
gt <- ggplot_gtable(ggplot_build(p))
gt$layout$clip[gt$layout$name == "panel"] <- "off"
grid.draw(gt)

enter image description here

Errhine answered 14/9, 2012 at 2:48 Comment(8)
isn't it easier to have one geom_text layer at x=Inf, hjust>=1, and turn off clipping?Simplicity
@jslefche, you should note that the solution offered by @Simplicity is much simpler. p = p + geom_text(aes(label = n, x = Inf, y = y), hjust = -1). Then turn off the clipping. Although the alignment can be off slightly.Errhine
and how does one turn off clipping?Hanako
@ThomasBrowne To turn off clipping, see the last three lines of code above.Errhine
What if I want to put the labels on the left axis in a plot with scale_x_log10 (so x cannot be less than zero)? I tried with geom_text(aes(label = label, x = 0, fontface = label.style), size = data$label.size, hjust = 1), but this put the labels attached to the border of the panel border. to move them a bit more to the left I added white space to the labels text. It works fine if all the labels are styled the same, but since I have different sizes for some of them, the white spaces size also is different, offsetting some labels more.Palestine
@Palestine Did you try hjust greater then 1?Errhine
I tried, using values other than 1, 0.5, 0 produce quite unpredictable alignments (didn't investigate to understand the pattern). Instead I discovered that to overcome the lower limit problem you just have to put the x argument of geom_text outside aes. Then errors are not raised anymore so you can move the text wherever you want!Palestine
Unfortunately the x position at which you need to position the text change with the range of the x axis. So you need to use a value of x that is relative to the axis range. Here's my solution. I get the x ggplot computed axis range with xlim.range <- ggplot_build(plot)$panel$ranges[[1]]$x.range. Then I use this as x position: x = xlim.range[1] - diff(xlim.range)/10 and it works!Palestine
A
6

Simplier solution based on grid

require(grid)

df = data.frame(y = c("cat1", "cat2", "cat3"), x = c(12, 10, 14), n = c(5, 15, 20))

p <- ggplot(df, aes(x, y)) + geom_point() + # Base plot
theme(plot.margin = unit(c(1, 3, 1, 1), "lines"))

p

grid.text("20", x = unit(0.91, "npc"), y = unit(0.80, "npc"))
grid.text("15", x = unit(0.91, "npc"), y = unit(0.56, "npc"))
grid.text("5", x = unit(0.91, "npc"), y = unit(0.31, "npc"))
Autostability answered 5/9, 2017 at 12:8 Comment(1)
Much simpler, at first glance but ... font doesn't match ggplot2 defaults so you then have to fiddle with those settings, and harder to position the text due to using npc units. Probably ends up just as complex.Overstride
I
2

Another option could be using annotate from ggplot2 which is almost the same as using geom_text:

library(ggplot2)
df=data.frame(y=c("cat1","cat2","cat3"),x=c(12,10,14),n=c(5,15,20))
ggplot(df,aes(x=x,y=y)) + 
  geom_point() + 
  annotate("text", x = max(df$x) + 0.5, y = df$y, label = df$n, size = 8) +
  coord_cartesian(xlim = c(min(df$x), max(df$x)), clip = "off") +
  theme(plot.margin = unit(c(1,3,1,1), "lines"))

Created on 2022-08-14 by the reprex package (v2.0.1)

Innervate answered 14/8, 2022 at 10:1 Comment(0)
O
1

This particular example might be a case for ggh4x::guide_axis_manual

# remotes::install_github("teunbrand/ggh4x")
library(ggplot2)

df <- data.frame(y=c("cat1","cat2","cat3"), x=c(12,10,14), n=c(5,15,20))

ggplot(df, aes(x=x, y=y)) +
  geom_point() +
  guides(y.sec=ggh4x::guide_axis_manual(title = element_blank(), breaks = df$y, labels = paste0("n=",df$n)))

Created on 2023-08-24 with reprex v2.0.2

Oven answered 24/8, 2023 at 8:10 Comment(0)
B
0

Another option which is similar in spirit to the approach by @jan-glx but using just vanilla ggplot2 would be to use the so-called secondary axis trick which means to use a secondary or duplicated axis to add the annotations.

However, in the case of a discrete scale this is slightly more involved as a discrete scale does not allow for a secondary axis. Hence, we have to switch to a continuous scale first by converting the discrete y axis variable to a numeric using e.g. as.numeric(factor(...)).

library(ggplot2)

df$y_num <- as.numeric(factor(df$y))

ggplot(df, aes(x = x, y = y_num, label = n)) +
  geom_point() +
  scale_y_continuous(
    # Fix the breaks
    breaks = unique(df$y_num),
    labels = df$y,
    # Set default discrete scale amount of expansion
    expand = c(0, .6),
    sec.axis = dup_axis(
      breaks = unique(df$y_num),
      labels = paste0("n = ", df$n)
    )
  ) +
  theme(
    # Multiply by `.pt` to convert to `geom_text` font size
    # (the latter is measured in "mm", while the axis text uses "pt")
    axis.text.y.right = element_text(size = 8 * .pt),
    axis.ticks.y.right = element_blank(),
    axis.title.y.right = element_blank()
  )

Bilberry answered 17/12, 2023 at 9:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.