Title pretty well covers it.
I have two legends, relating to size and colour, and wish to have one,say, on the top and one within the graph.
Is this possible and, if so, how
TIA
It can be done by extracting separate legends from plots, then arranging the legends in the relevant plot. The code here uses functions from the gtable package to do the extraction, then functions from the gridExtra package to do the arranging. The aim is to have a plot that contains a color legend and a size legend. First, extract the colour legend from a plot that contains the colour legend only. Second, extract the size legend from a plot that contains the size legend only. Third, draw a plot that contains no legend. Fourth, arrange the plot and the two legends into one new plot.
# Some data
df <- data.frame(
x = 1:10,
y = 1:10,
colour = factor(sample(1:3, 10, replace = TRUE)),
size = factor(sample(1:3, 10, replace = TRUE)))
library(ggplot2)
library(gridExtra)
library(gtable)
library(grid)
### Step 1
# Draw a plot with the colour legend
(p1 <- ggplot(data = df, aes(x=x, y=y)) +
geom_point(aes(colour = colour)) +
theme_bw() +
theme(legend.position = "top"))
# Extract the colour legend - leg1
leg1 <- gtable_filter(ggplot_gtable(ggplot_build(p1)), "guide-box")
### Step 2
# Draw a plot with the size legend
(p2 <- ggplot(data = df, aes(x=x, y=y)) +
geom_point(aes(size = size)) +
theme_bw())
# Extract the size legend - leg2
leg2 <- gtable_filter(ggplot_gtable(ggplot_build(p2)), "guide-box")
# Step 3
# Draw a plot with no legends - plot
(plot <- ggplot(data = df, aes(x=x, y=y)) +
geom_point(aes(size = size, colour = colour)) +
theme_bw() +
theme(legend.position = "none"))
### Step 4
# Arrange the three components (plot, leg1, leg2)
# The two legends are positioned outside the plot:
# one at the top and the other to the side.
plotNew <- arrangeGrob(leg1, plot,
heights = unit.c(leg1$height, unit(1, "npc") - leg1$height), ncol = 1)
plotNew <- arrangeGrob(plotNew, leg2,
widths = unit.c(unit(1, "npc") - leg2$width, leg2$width), nrow = 1)
grid.newpage()
grid.draw(plotNew)
# OR, arrange one legend at the top and the other inside the plot.
plotNew <- plot +
annotation_custom(grob = leg2, xmin = 7, xmax = 10, ymin = 0, ymax = 4)
plotNew <- arrangeGrob(leg1, plotNew,
heights = unit.c(leg1$height, unit(1, "npc") - leg1$height), ncol = 1)
grid.newpage()
grid.draw(plotNew)
Using ggplot2and cowplot (= ggplot2 extension).
The approach is similar to Sandy's one as it takes out the legend as seperate objects and lets you do the placement independently. It was primarly designed for multiple legends which belong to two or more plots in a grid of plots.
The idea is as follows:
Create Plot1, Plot2,...,PlotX without legends
Create Plot1, Plot2,...,PlotX with legends
Extract legends from step 1 & 2 into separate objects
Set up legend grid and arrange legends they way you want to
Create grid combining plots and legends
It seems kinda complicated and time/code consuming but set up once, you can adapt and use it for every kind of plot/legend customization.
library(ggplot2)
library(cowplot)
# Some data
df <- data.frame(
Name = factor(rep(c("A", "B", "C"), 12)),
Month = factor(rep(1:12, each = 3)),
Temp = sample(0:40, 12),
Precip = sample(50:400, 12)
)
# 1. create plot1
plot1 <- ggplot(df, aes(Month, Temp, fill = Name)) +
geom_point(
show.legend = F, aes(group = Name, colour = Name),
size = 3, shape = 17
) +
geom_smooth(
method = "loess", se = F,
aes(group = Name, colour = Name),
show.legend = F, size = 0.5, linetype = "dashed"
)
# 2. create plot2
plot2 <- ggplot(df, aes(Month, Precip, fill = Name)) +
geom_bar(stat = "identity", position = "dodge", show.legend = F) +
geom_smooth(
method = "loess", se = F,
aes(group = Name, colour = Name),
show.legend = F, size = 1, linetype = "dashed"
) +
scale_fill_grey()
# 3.1 create legend1
legend1 <- ggplot(df, aes(Month, Temp)) +
geom_point(
show.legend = T, aes(group = Name, colour = Name),
size = 3, shape = 17
) +
geom_smooth(
method = "loess", se = F, aes(group = Name, colour = Name),
show.legend = T, size = 0.5, linetype = "dashed"
) +
labs(colour = "Station") +
theme(
legend.text = element_text(size = 8),
legend.title = element_text(
face = "italic",
angle = -0, size = 10
)
)
# 3.2 create legend2
legend2 <- ggplot(df, aes(Month, Precip, fill = Name)) +
geom_bar(stat = "identity", position = "dodge", show.legend = T) +
scale_fill_grey() +
guides(
fill =
guide_legend(
title = "",
title.theme = element_text(
face = "italic",
angle = -0, size = 10
)
)
) +
theme(legend.text = element_text(size = 8))
# 3.3 extract "legends only" from ggplot object
legend1 <- get_legend(legend1)
legend2 <- get_legend(legend2)
# 4.1 setup legends grid
legend1_grid <- cowplot::plot_grid(legend1, align = "v", nrow = 2)
# 4.2 add second legend to grid, specifying its location
legends <- legend1_grid +
ggplot2::annotation_custom(
grob = legend2,
xmin = 0.5, xmax = 0.5, ymin = 0.55, ymax = 0.55
)
# 5. plot "plots" + "legends" (with legends in between plots)
cowplot::plot_grid(plot1, legends, plot2,
ncol = 3,
rel_widths = c(0.45, 0.1, 0.45)
)
Created on 2019-10-05 by the reprex package (v0.3.0)
Changing the order of the final plot_grid() call moves the legends to the right:
cowplot::plot_grid(plot1, plot2, legends, ncol = 3,
rel_widths = c(0.45, 0.45, 0.1))
From my understanding, basically there is very limited control over legends in ggplot2. Here is a paragraph from the Hadley's book (page 111):
ggplot2 tries to use the smallest possible number of legends that accurately conveys the aesthetics used in the plot. It does this by combining legends if a variable is used with more than one aesthetic. Figure 6.14 shows an example of this for the points geom: if both colour and shape are mapped to the same variable, then only a single legend is necessary. In order for legends to be merged, they must have the same name (the same legend title). For this reason, if you change the name of one of the merged legends, you’ll need to change it for all of them.
Related
I am trying to create a faceted alluvial plot with labels for the stratums on the first axis repelled to the left and left justified and the labels on the right repelled to the right and right justified.
# Small working example
# Install Packages and Libraries
install.packages("ggplot2")
install.packages("ggalluvial")
install.packages("ggrepel")
library(ggplot2)
library(ggalluvial)
library(ggrepel)
# Data Frame with 2 regions, 3 supply sectors and 3 demand sectors
df <- data.frame(region = c("A","A","A","B","B","B"),
supplySector = c("coal","gas","wind","coal","gas","wind"),
demandSector = c("resid","indus","ag","resid","indus","ag"),
value = 10*runif(6)); df
# Faceted plot with ggrepel (nudge_x and hjust assigned for each label) works.
p <- ggplot(df, aes(y = value, axis1 = supplySector, axis2 = demandSector, group=region)) +
ggalluvial::geom_alluvium(aes(fill = supplySector), width = 1/12, color="black", alpha=0.6) +
ggalluvial::geom_stratum(width = 1/12, fill = "grey70", color = "grey10", alpha=1) +
scale_x_discrete(limits = c("supplySector", "demandSector"), expand = c(0.3,0),drop=F) +
facet_wrap(region~.) +
ggrepel::geom_text_repel(stat = "stratum", label.strata = TRUE, direction = "y",
size = 4, segment.color = 'grey50',
nudge_x = rep(c(-3,-3,-3,3,3,3),2),
hjust = rep(c(1,1,1,-1,-1,-1),2)); p
# Faceted plot with ggrepel (nudge_x and hjust assigned for each label)
# does not work when different number of variables in each facet
df1 <- df[-nrow(df),]; df1 # Remove one of the rows from df
# So this gives the following plot with different alluvia in each facet
p1 <- ggplot(df1, aes(y = value, axis1 = supplySector, axis2 = demandSector, group=region)) +
ggalluvial::geom_alluvium(aes(fill = supplySector), width = 1/12, color="black", alpha=0.6) +
ggalluvial::geom_stratum(width = 1/12, fill = "grey70", color = "grey10", alpha=1) +
scale_x_discrete(limits = c("supplySector", "demandSector"), expand = c(0.3,0),drop=F) +
facet_wrap(region~.); p1
# If we try and label these and assigns the nudge and hjust for each axis we get an error
# It expects the same length vector for nudge and hjust for each facet
p1 + ggrepel::geom_text_repel(stat = "stratum", label.strata = TRUE, direction = "y",
size = 4, segment.color = 'grey50',
nudge_x = rep(c(-3,-3,-3,3,3,3),2),
hjust=rep(c(1,1,1,-1,-1,-1),2))
# Gives error: Error: Aesthetics must be either length 1 or the same as the data (10): hjust
# If we adjust the vectors for nudge_x and hjust to 10
p1 + ggrepel::geom_text_repel(stat = "stratum", label.strata = TRUE, direction = "y",
size = 4, segment.color = 'grey50',
nudge_x = c(-3,-3,-3,3,3,3,-3-3,3,3),
hjust = c(1,1,1,-1,-1,-1,1,1,-1,-1))
# Get Error: Error in data.frame(x = data$x + nudge_x, y = data$y + nudge_y) :
# arguments imply differing number of rows: 9, 6
# In addition: Warning message:
# In data$x + nudge_x :
# longer object length is not a multiple of shorter object length
# It can be plotted without specifying the nudge_x and hjust values
p1 + ggrepel::geom_text_repel(stat = "stratum", label.strata = TRUE, direction = "y",
size = 4, segment.color = 'grey50')
In summary, what I am trying to do is:
For plot p1 (with different number of alluvia in different facets)
Label each x axis stratum column
Have axis1 labels repel to the left and be left justified
Have axis2 labels repel to the right and be right justified
This answer suggested the different vector length for labels but it doesn't work for varying facets.
Labelling and theme of ggalluvial plot in R
This is tricky! The nudge_* and *just arguments generally aren't dynamic. One way you could solve for this is to dig into the guts using ggplot_build()
ggplot_build() has all of the "instructions" of how ggplot() builds the chart. You can edit the data and then run plot(ggplot_gtable()) to see the plot with your modifications. I have added comments to help explain these steps.
# here is the base plot + the new layer for labels
plot_and_label <-
p1 +
geom_text_repel(
stat = "stratum", label.strata = TRUE,
direction = "y", size = 4,
segment.color = 'grey50',
nudge_x = 0
)
# this is the plot under the hood
gg_guts <- ggplot_build(plot_and_label)
# the geom_text_repel layer was the 3rd one we added so you can
# access and edit it like this
gg_guts$data[[3]] <-
gg_guts$data[[3]] %>%
mutate(hjust = ifelse(x%%2 == 1, 2, -2))
# once you've made your adjustments, you can plot it again
plot(ggplot_gtable(gg_guts))
I would like to be able to extend my boxplots with additional information. Here is a working example for ggplot2:
library(ggplot2)
ToothGrowth$dose <- as.factor(ToothGrowth$dose)
# Basic box plot
p <- ggplot(ToothGrowth, aes(x=dose, y=len)) +
geom_boxplot()
# Rotate the box plot
p + coord_flip()
I would like to add additional information from a separate data frame. For example:
extra <- data.frame(dose=factor(c(0.5,1,2)), label=c("Label1", "Label2", "Label3"), n=c("n=42","n=52","n=35"))
> extra
dose label n
1 0.5 Label1 n=42
2 1 Label2 n=52
3 2 Label3 n=35
I would like to create the following figure where the information to each dose (factor) is outside the plot and aligns with each of the dose levels (I made this in powerpoint as an example):
EDIT:
I would like to ask advice for an extension of the initial question.
What about this extension where I use fill to split up dose by the two groups?
ToothGrowth$dose <- as.factor(ToothGrowth$dose)
ToothGrowth$group <- head(rep(1:2, 100), dim(ToothGrowth)[1])
ToothGrowth$group <- factor(ToothGrowth$group)
p <- ggplot(ToothGrowth, aes(x=dose, y=len, fill=group)) +
geom_boxplot()
# Rotate the box plot
p + coord_flip()
extra <- data.frame(
dose=factor(rep(c(0.5,1,2), each=2)),
group=factor(rep(c(1:2), 3)),
label=c("Label1A", "Label1B", "Label2A", "Label2B", "Label3A", "Label3B"),
n=c("n=12","n=30","n=20", "n=32","n=15","n=20")
)
Is it possible to align data from the new data frame (extra, 6 rows) with each of the dose/group combinations?
We can use geom_text with clip = "off" inside coord_flip:
ggplot(ToothGrowth, aes(x=dose, y=len)) +
geom_boxplot() +
geom_text(
y = max(ToothGrowth$len) * 1.1,
data = extra,
aes(x = dose, label = sprintf("%s\n%s", label, n)),
hjust = 0) +
coord_flip(clip = "off") +
theme(plot.margin = unit(c(1, 5, 0.5, 0.5), "lines"))
Explanation: We place text outside of the plot area with geom_text and disable clipping with clip = "off" inside coord_flip. Lastly, we increase the plot margin to accommodate the additional labels. You can adjust the vertical y position in the margin (so the horizontal position in the plot because of the coordinate flip) by changing the factor in y = max(ToothGrowth$len) * 1.1.
In response to your edit, here is a possibility
extra <- data.frame(
dose=factor(rep(c(0.5,1,2), each=2)),
group=factor(rep(c(1:2), 3)),
label=c("Label1A", "Label1B", "Label2A", "Label2B", "Label3A", "Label3B"),
n=c("n=12","n=30","n=20", "n=32","n=15","n=20")
)
library(tidyverse)
ToothGrowth %>%
mutate(
dose = as.factor(dose),
group = as.factor(rep(1:2, nrow(ToothGrowth) / 2))) %>%
ggplot(aes(x = dose, y = len, fill = group)) +
geom_boxplot(position = position_dodge(width = 1)) +
geom_text(
data = extra %>%
mutate(
dose = as.factor(dose),
group = as.factor(group),
ymax = max(ToothGrowth$len) * 1.1),
aes(x = dose, y = ymax, label = sprintf("%s\n%s", label, n)),
position = position_dodge(width = 1),
size = 3,
hjust = 0) +
coord_flip(clip = "off", ylim = c(0, max(ToothGrowth$len))) +
theme(
plot.margin = unit(c(1, 5, 0.5, 0.5), "lines"),
legend.position = "bottom")
A few comments:
We ensure that labels match the dodged bars by using position_dodge(with = 1) inside geom_text and geom_boxplot.
It seems that position_dodge does not like a global y (outside of aes). So we include the y position for the labels in extra and use it inside aes. As a result, we need to explicitly limit the range of the y axis. We can do that inside coord_flip with ylim = c(0, max(ToothGrowth$len)).
I am plotting a figure that uses both symbols and linetype to differentiate between groups (points and 95% confidence ellipses, respectively).
Here is an example plot with a similar legend:
bplot<-ggplot(iris,aes(x=Sepal.Length,y=Sepal.Width,group=Species,shape=Species,lty=Species))+
geom_point(size=3)+geom_smooth(method="lm",se=F,color="black")+
theme_minimal()+theme(legend.key.size=unit(1.2,"cm"))
bplot
The problem with this is that the linetypes are hard to see in the legend because they are overlapping the symbols. Is there a way to display the linetype in the same legend as the symbols (with a single label), but below the symbols or in a way that both are readable?
I think the legend is readable if the keys are widened a little.
library(ggplot2)
bplot <- ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width, group = Species,
shape = Species,lty = Species)) +
geom_point(size = 3) +
geom_smooth(method = "lm", se = F, color = "black") +
theme_minimal() +
theme(legend.key.width = unit(1.5, "cm"))
bplot
But if you want to separate the point from the line within each key, then I think you will need to delve into the ggplot grob.
library(grid)
bplot <- ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width, group = Species,
shape = Species,lty = Species)) +
geom_point(size = 3) +
geom_smooth(method = "lm", se = F, color = "black") +
theme_minimal() +
theme(legend.key.width = unit(1.5, "cm"),
legend.key.height = unit(1, "cm"),
legend.key = element_rect(colour = "grey50", size = .5))
# Get the plot grob
g = ggplotGrob(bplot)
# Get the legend
leg = g$grobs[[which(g$layout$name == "guide-box")]]
# Get the relevant keys
pos = grep("key-.-1-1", leg$grobs[[1]]$layout$name)
# pos gets the point; pos+1 gets the line
# Separate the line from the point within each key
for(i in pos) {
leg$grobs[[1]]$grobs[[i]]$y = unit(0.6, "npc")
leg$grobs[[1]]$grobs[[i+1]]$children[[1]]$y0 = unit(0.3, "npc")
leg$grobs[[1]]$grobs[[i+1]]$children[[1]]$y1 = unit(0.3, "npc")
}
# Put the legend back into the plot
g$grobs[[which(g$layout$name == "guide-box")]] = leg
# Draw it
grid.newpage()
grid.draw(g)
Or, if you want separate legends, see #Divi's answer
You need the guides functionality. Please go through the documentation for more customization.
bplot<-ggplot(iris,aes(x=Sepal.Length,y=Sepal.Width,group=Species,shape=Species,lty=Species))+
geom_point(size=3)+geom_smooth(method="lm",se=F,color="black")+
theme_minimal()+theme(legend.key.size=unit(1.2,"cm")) +
guides(
shape = guide_legend(order = 1),
size = guide_legend(order = 2)
)
bplot
One option for editing specific grobs within a ggplot grob is given in one of the posts here. It involves following a pathway through long lists within lists to get to the relevant grob. Sometimes, it is easier (although maybe only marginally so) to edit the relevant grobs using grid editing functions.
library(ggplot2)
library(grid)
# The plot
bplot <- ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width, group = Species,
shape = Species,lty = Species)) +
geom_point(size = 3) +
geom_smooth(method = "lm", se = F, color = "black") +
theme_minimal() +
theme(legend.key.width = unit(1.5, "cm"),
legend.key.height = unit(1, "cm"),
legend.key = element_rect(colour = "grey50", size = .5))
# Get the plot grob
g = ggplotGrob(bplot)
# Get a list of the grobs
grid.ls(grid.force(g))
Look through the list of grobs. The grobs we want to edit are towards the bottom of the list - with names that begin with "key". There are three groups of three - three groups because there are three keys in the legend. Within each group, there are three grobs:
key-3-1-bg.4-2-4-2
key-3-1-1.4-2-4-2
key-3-1-2.4-2-4-2
GRID.segments.819
The first is the background - of no interest here.
The second refers to the point - we want to edit its vertical position.
The third refers to a grob that contains a child - GRID.segments - the line segments. We want to edit the line segment's vertical position.
In the editGrob() command, gPath lists the grob to be edited, but grep=TRUE means that regular expressions can be use, and global=TRUE means all matches will be affected. So, only one command for each edit.
# Edit the 'key' grobs
# Move the point up a little
g = editGrob(grid.force(g), gPath("key-[1-9]-1-1"), grep = TRUE, global = TRUE,
y = unit(0.6, "npc"))
# Move the line segment down a little
g = editGrob(grid.force(g), gPath("GRID.segments"), grep = TRUE, global = TRUE,
y0 = unit(0.3, "npc"), y1 = unit(0.3, "npc"))
# Draw it
grid.newpage()
grid.draw(g)
Title pretty well covers it.
I have two legends, relating to size and colour, and wish to have one,say, on the top and one within the graph.
Is this possible and, if so, how
TIA
It can be done by extracting separate legends from plots, then arranging the legends in the relevant plot. The code here uses functions from the gtable package to do the extraction, then functions from the gridExtra package to do the arranging. The aim is to have a plot that contains a color legend and a size legend. First, extract the colour legend from a plot that contains the colour legend only. Second, extract the size legend from a plot that contains the size legend only. Third, draw a plot that contains no legend. Fourth, arrange the plot and the two legends into one new plot.
# Some data
df <- data.frame(
x = 1:10,
y = 1:10,
colour = factor(sample(1:3, 10, replace = TRUE)),
size = factor(sample(1:3, 10, replace = TRUE)))
library(ggplot2)
library(gridExtra)
library(gtable)
library(grid)
### Step 1
# Draw a plot with the colour legend
(p1 <- ggplot(data = df, aes(x=x, y=y)) +
geom_point(aes(colour = colour)) +
theme_bw() +
theme(legend.position = "top"))
# Extract the colour legend - leg1
leg1 <- gtable_filter(ggplot_gtable(ggplot_build(p1)), "guide-box")
### Step 2
# Draw a plot with the size legend
(p2 <- ggplot(data = df, aes(x=x, y=y)) +
geom_point(aes(size = size)) +
theme_bw())
# Extract the size legend - leg2
leg2 <- gtable_filter(ggplot_gtable(ggplot_build(p2)), "guide-box")
# Step 3
# Draw a plot with no legends - plot
(plot <- ggplot(data = df, aes(x=x, y=y)) +
geom_point(aes(size = size, colour = colour)) +
theme_bw() +
theme(legend.position = "none"))
### Step 4
# Arrange the three components (plot, leg1, leg2)
# The two legends are positioned outside the plot:
# one at the top and the other to the side.
plotNew <- arrangeGrob(leg1, plot,
heights = unit.c(leg1$height, unit(1, "npc") - leg1$height), ncol = 1)
plotNew <- arrangeGrob(plotNew, leg2,
widths = unit.c(unit(1, "npc") - leg2$width, leg2$width), nrow = 1)
grid.newpage()
grid.draw(plotNew)
# OR, arrange one legend at the top and the other inside the plot.
plotNew <- plot +
annotation_custom(grob = leg2, xmin = 7, xmax = 10, ymin = 0, ymax = 4)
plotNew <- arrangeGrob(leg1, plotNew,
heights = unit.c(leg1$height, unit(1, "npc") - leg1$height), ncol = 1)
grid.newpage()
grid.draw(plotNew)
Using ggplot2and cowplot (= ggplot2 extension).
The approach is similar to Sandy's one as it takes out the legend as seperate objects and lets you do the placement independently. It was primarly designed for multiple legends which belong to two or more plots in a grid of plots.
The idea is as follows:
Create Plot1, Plot2,...,PlotX without legends
Create Plot1, Plot2,...,PlotX with legends
Extract legends from step 1 & 2 into separate objects
Set up legend grid and arrange legends they way you want to
Create grid combining plots and legends
It seems kinda complicated and time/code consuming but set up once, you can adapt and use it for every kind of plot/legend customization.
library(ggplot2)
library(cowplot)
# Some data
df <- data.frame(
Name = factor(rep(c("A", "B", "C"), 12)),
Month = factor(rep(1:12, each = 3)),
Temp = sample(0:40, 12),
Precip = sample(50:400, 12)
)
# 1. create plot1
plot1 <- ggplot(df, aes(Month, Temp, fill = Name)) +
geom_point(
show.legend = F, aes(group = Name, colour = Name),
size = 3, shape = 17
) +
geom_smooth(
method = "loess", se = F,
aes(group = Name, colour = Name),
show.legend = F, size = 0.5, linetype = "dashed"
)
# 2. create plot2
plot2 <- ggplot(df, aes(Month, Precip, fill = Name)) +
geom_bar(stat = "identity", position = "dodge", show.legend = F) +
geom_smooth(
method = "loess", se = F,
aes(group = Name, colour = Name),
show.legend = F, size = 1, linetype = "dashed"
) +
scale_fill_grey()
# 3.1 create legend1
legend1 <- ggplot(df, aes(Month, Temp)) +
geom_point(
show.legend = T, aes(group = Name, colour = Name),
size = 3, shape = 17
) +
geom_smooth(
method = "loess", se = F, aes(group = Name, colour = Name),
show.legend = T, size = 0.5, linetype = "dashed"
) +
labs(colour = "Station") +
theme(
legend.text = element_text(size = 8),
legend.title = element_text(
face = "italic",
angle = -0, size = 10
)
)
# 3.2 create legend2
legend2 <- ggplot(df, aes(Month, Precip, fill = Name)) +
geom_bar(stat = "identity", position = "dodge", show.legend = T) +
scale_fill_grey() +
guides(
fill =
guide_legend(
title = "",
title.theme = element_text(
face = "italic",
angle = -0, size = 10
)
)
) +
theme(legend.text = element_text(size = 8))
# 3.3 extract "legends only" from ggplot object
legend1 <- get_legend(legend1)
legend2 <- get_legend(legend2)
# 4.1 setup legends grid
legend1_grid <- cowplot::plot_grid(legend1, align = "v", nrow = 2)
# 4.2 add second legend to grid, specifying its location
legends <- legend1_grid +
ggplot2::annotation_custom(
grob = legend2,
xmin = 0.5, xmax = 0.5, ymin = 0.55, ymax = 0.55
)
# 5. plot "plots" + "legends" (with legends in between plots)
cowplot::plot_grid(plot1, legends, plot2,
ncol = 3,
rel_widths = c(0.45, 0.1, 0.45)
)
Created on 2019-10-05 by the reprex package (v0.3.0)
Changing the order of the final plot_grid() call moves the legends to the right:
cowplot::plot_grid(plot1, plot2, legends, ncol = 3,
rel_widths = c(0.45, 0.45, 0.1))
From my understanding, basically there is very limited control over legends in ggplot2. Here is a paragraph from the Hadley's book (page 111):
ggplot2 tries to use the smallest possible number of legends that accurately conveys the aesthetics used in the plot. It does this by combining legends if a variable is used with more than one aesthetic. Figure 6.14 shows an example of this for the points geom: if both colour and shape are mapped to the same variable, then only a single legend is necessary. In order for legends to be merged, they must have the same name (the same legend title). For this reason, if you change the name of one of the merged legends, you’ll need to change it for all of them.
I am trying to display some information about the data below the plot created in ggplot2. I would like to plot the N variable using the X axis coordinate of the plot but the Y coordinate needs to be 10% from the bottom of the screen . In fact, the desired Y coordinates are already in the data frame as y_pos variable.
I can think of 3 approaches using ggplot2:
1) Create an empty plot below the actual plot, use the same scale and then use geom_text to plot the data over the blank plot. This approach sort of works but is extremely complicated.
2) Use geom_text to plot the data but somehow use y coordinate as percent of the screen (10%). This would force the numbers to be displayed below the plot. I can't figure out the proper syntax.
3) Use grid.text to display the text. I can easily set it at the 10% from the bottom of the screen but I can't figure how set the X coordindate to match the plot. I tried to use grconvert to capture the initial X position but could not get that to work as well.
Below is the basic plot with the dummy data:
graphics.off() # close graphics windows
library(car)
library(ggplot2) #load ggplot
library(gridExtra) #load Grid
library(RGraphics) # support of the "R graphics" book, on CRAN
#create dummy data
test= data.frame(
Group = c("A", "B", "A","B", "A", "B"),
x = c(1 ,1,2,2,3,3 ),
y = c(33,25,27,36,43,25),
n=c(71,55,65,58,65,58),
y_pos=c(9,6,9,6,9,6)
)
#create ggplot
p1 <- qplot(x, y, data=test, colour=Group) +
ylab("Mean change from baseline") +
geom_line()+
scale_x_continuous("Weeks", breaks=seq(-1,3, by = 1) ) +
opts(
legend.position=c(.1,0.9))
#display plot
p1
The modified gplot below displays numbers of subjects, however they are displayed WITHIN the plot. They force the Y scale to be extended. I would like to display these numbers BELOW the plot.
p1 <- qplot(x, y, data=test, colour=Group) +
ylab("Mean change from baseline") +
geom_line()+
scale_x_continuous("Weeks", breaks=seq(-1,3, by = 1) ) +
opts( plot.margin = unit(c(0,2,2,1), "lines"),
legend.position=c(.1,0.9))+
geom_text(data = test,aes(x=x,y=y_pos,label=n))
p1
A different approach of displaying the numbers involves creating a dummy plot below the actual plot. Here is the code:
graphics.off() # close graphics windows
library(car)
library(ggplot2) #load ggplot
library(gridExtra) #load Grid
library(RGraphics) # support of the "R graphics" book, on CRAN
#create dummy data
test= data.frame(
group = c("A", "B", "A","B", "A", "B"),
x = c(1 ,1,2,2,3,3 ),
y = c(33,25,27,36,43,25),
n=c(71,55,65,58,65,58),
y_pos=c(15,6,15,6,15,6)
)
p1 <- qplot(x, y, data=test, colour=group) +
ylab("Mean change from baseline") +
opts(plot.margin = unit(c(1,2,-1,1), "lines")) +
geom_line()+
scale_x_continuous("Weeks", breaks=seq(-1,3, by = 1) ) +
opts(legend.position="bottom",
legend.title=theme_blank(),
title.text="Line plot using GGPLOT")
p1
p2 <- qplot(x, y, data=test, geom="blank")+
ylab(" ")+
opts( plot.margin = unit(c(0,2,-2,1), "lines"),
axis.line = theme_blank(),
axis.ticks = theme_segment(colour = "white"),
axis.text.x=theme_text(angle=-90,colour="white"),
axis.text.y=theme_text(angle=-90,colour="white"),
panel.background = theme_rect(fill = "transparent",colour = NA),
panel.grid.minor = theme_blank(),
panel.grid.major = theme_blank()
)+
geom_text(data = test,aes(x=x,y=y_pos,label=n))
p2
grid.arrange(p1, p2, heights = c(8.5, 1.5), nrow=2 )
However, that is very complicated and would be hard to modify for different data. Ideally, I'd like to be able to pass Y coordinates as percent of the screen.
The current version (>2.1) has a + labs(caption = "text"), which displays an annotation below the plot. This is themeable (font properties,... left/right aligned). See https://github.com/hadley/ggplot2/pull/1582 for examples.
Edited opts has been deprecated, replaced by theme; element_blank has replaced theme_blank; and ggtitle() is used in place of opts(title = ...
Sandy- thank you so much!!!! This does exactly what I want. I do wish we could control the clipping in geom.text or geom.annotate.
I put together the following program if anybody else is interested.
rm(list = ls()) # clear objects
graphics.off() # close graphics windows
library(ggplot2)
library(gridExtra)
#create dummy data
test= data.frame(
group = c("Group 1", "Group 1", "Group 1","Group 2", "Group 2", "Group 2"),
x = c(1 ,2,3,1,2,3 ),
y = c(33,25,27,36,23,25),
n=c(71,55,65,58,65,58),
ypos=c(18,18,18,17,17,17)
)
p1 <- qplot(x=x, y=y, data=test, colour=group) +
ylab("Mean change from baseline") +
theme(plot.margin = unit(c(1,3,8,1), "lines")) +
geom_line()+
scale_x_continuous("Visits", breaks=seq(-1,3) ) +
theme(legend.position="bottom",
legend.title=element_blank())+
ggtitle("Line plot")
# Create the textGrobs
for (ii in 1:nrow(test))
{
#display numbers at each visit
p1=p1+ annotation_custom(grob = textGrob(test$n[ii]),
xmin = test$x[ii],
xmax = test$x[ii],
ymin = test$ypos[ii],
ymax = test$ypos[ii])
#display group text
if (ii %in% c(1,4)) #there is probably a better way
{
p1=p1+ annotation_custom(grob = textGrob(test$group[ii]),
xmin = 0.85,
xmax = 0.85,
ymin = test$ypos[ii],
ymax = test$ypos[ii])
}
}
# Code to override clipping
gt <- ggplot_gtable(ggplot_build(p1))
gt$layout$clip[gt$layout$name=="panel"] <- "off"
grid.draw(gt)
Updated opts() has been replaced with theme()
In the code below, a base plot is drawn, with a wider margin at the bottom of the plot. The textGrob is created, then inserted into the plot using annotation_custom(). Except the text is not visible because it is outside the plot panel - the output is clipped to the panel. But using baptiste's code from here, the clipping can be overrridden. The position is in terms of data units, and both text labels are centred.
library(ggplot2)
library(grid)
# Base plot
df = data.frame(x=seq(1:10), y = seq(1:10))
p = ggplot(data = df, aes(x = x, y = y)) + geom_point() + ylim(0,10) +
theme(plot.margin = unit(c(1,1,3,1), "cm"))
p
# Create the textGrobs
Text1 = textGrob(paste("Largest x-value is", round(max(df$x), 2), sep = " "))
Text2 = textGrob(paste("Mean = ", mean(df$x), sep = ""))
p1 = p + annotation_custom(grob = Text1, xmin = 4, xmax = 4, ymin = -3, ymax = -3) +
annotation_custom(grob = Text2, xmin = 8, xmax = 8, ymin = -3, ymax = -3)
p1
# Code to override clipping
gt <- ggplotGrob(p1)
gt$layout$clip[gt$layout$name=="panel"] <- "off"
grid.draw(gt)
Or, using grid functions to create and position the label.
p
grid.text((paste("Largest x-value is", max(df$x), sep = " ")),
x = unit(.2, "npc"), y = unit(.1, "npc"), just = c("left", "bottom"),
gp = gpar(fontface = "bold", fontsize = 18, col = "blue"))
Edit
Or, add text grob using gtable functions.
library(ggplot2)
library(grid)
library(gtable)
# Base plot
df = data.frame(x=seq(1:10), y = seq(1:10))
p = ggplot(data = df, aes(x = x, y = y)) + geom_point() + ylim(0,10)
# Construct the text grob
lab = textGrob((paste("Largest x-value is", max(df$x), sep = " ")),
x = unit(.1, "npc"), just = c("left"),
gp = gpar(fontface = "bold", fontsize = 18, col = "blue"))
gp = ggplotGrob(p)
# Add a row below the 2nd from the bottom
gp = gtable_add_rows(gp, unit(2, "grobheight", lab), -2)
# Add 'lab' grob to that row, under the plot panel
gp = gtable_add_grob(gp, lab, t = -2, l = gp$layout[gp$layout$name == "panel",]$l)
grid.newpage()
grid.draw(gp)
Actually the best answer and easiest solution is to use the cowplot package.
Version 0.5.0 of the cowplot package (on CRAN) handles ggplot2 subtitles using the add_sub function.
Use it like so:
diamondsCubed <-ggplot(aes(carat, price), data = diamonds) +
geom_point() +
scale_x_continuous(trans = cuberoot_trans(), limits = c(0.2, 3),
breaks = c(0.2, 0.5, 1, 2, 3)) +
scale_y_continuous(trans = log10_trans(), limits = c(350, 15000),
breaks = c(350, 1000, 5000, 10000, 15000)) +
ggtitle('Price log10 by Cube-Root of Carat') +
theme_xkcd()
ggdraw(add_sub(diamondsCubed, "This is an annotation.\nAnnotations can span multiple lines."))