Reputation: 4609
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:
I would prefer something more like this:
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!
Upvotes: 92
Views: 130483
Reputation: 6277
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 = I(1), # Set text's position to the right end of the plot
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
Thanks to @captain-hat for the suggestion to use I(1)
in geom_text(x = I(1)
to set the text position to the far right end of the plot.
Upvotes: 97
Reputation: 125772
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()
)
Upvotes: 0
Reputation: 9536
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
Upvotes: 1
Reputation: 41601
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)
Upvotes: 2
Reputation: 1368
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"))
Upvotes: 6
Reputation: 32859
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)
Upvotes: 68