How to add captions outside the plot on individual facets in ggplot2? - r

I am trying to add a caption in each facet (I am using facet_grid). I have seen these approach and this one: but nothing gives me what I need. Also, the first approach returns a warning message that I didn't find any solution:
Warning message:
Vectorized input to `element_text()` is not officially supported.
Results may be unexpected or may change in future versions of ggplot2.
My example:
library(ggplot2)
library(datasets)
mydf <- CO2
a <- ggplot(data = mydf, aes(x = conc)) + geom_histogram(bins = 15, alpha = 0.75) +
labs(y = "Frequency") + facet_grid(Type ~ Treatment)
a
caption_df <- data.frame(
cyl = c(4,6),
txt = c("1st=4", "2nd=6")
)
a + coord_cartesian(clip="off", ylim=c(0, 3)) +
geom_text(
data=caption_df, y=1, x=100,
mapping=aes(label=txt), hjust=0,
fontface="italic", color="red"
) +
theme(plot.margin = margin(b=25))
The idea is to have 1 caption per plot, but with this approach it repeats the caption and it is overwritten.
Is it possible to have something like this? (caption OUTSIDE the plot) (but without the previous warning)
a + labs(caption = c("nonchilled=4", "chilled=6")) + theme(plot.caption = element_text(hjust=c(0, 1)))
NOTE: This is only an example, but I may need to put long captions (sentences) for each plot.
Example:
a + labs(caption = c("This is my first caption that maybe it will be large. Color red, n= 123", "This is my second caption that maybe it will be large. Color blue, n= 22")) +
theme(plot.caption = element_text(hjust=c(1, 0)))
Does anyone know how to do it?
Thanks in advance

You need to add the same faceting variable to your additional caption data frame as are present in your main data frame to specify the facets in which each should be placed. If you want some facets unlabelled, simply have an empty string.
caption_df <- data.frame(
cyl = c(4, 6, 8, 10),
conc = c(0, 1000, 0, 1000),
Freq = -1,
txt = c("1st=4", "2nd=6", '', ''),
Type = rep(c('Quebec', 'Mississippi'), each = 2),
Treatment = rep(c('chilled', 'nonchilled'), 2)
)
a + coord_cartesian(clip="off", ylim=c(0, 3), xlim = c(0, 1000)) +
geom_text(data = caption_df, aes(y = Freq, label = txt)) +
theme(plot.margin = margin(b=25))

Related

Is it possible to align x axis title to a value of the axis?

Having a tibble and a simple scatterplot:
p <- tibble(
x = rnorm(50, 1),
y = rnorm(50, 10)
)
ggplot(p, aes(x, y)) + geom_point()
I get something like this:
I would like to align (center, left, right, as the case may be) the title of the x-axis - here rather blandly x - with a specific value on the axis, say the off-center 0 in this case. Is there a way to do that declaratively, without having to resort to the dumb (as in "free of context") trial-and-error element_text(hjust=??). The ?? are rather appropriate here because every value is a result of experimentation (my screen and PDF export in RStudio never agree on quite some plot elements). Any change in the data or the dimensions of the rendering may (or may not) invalidate the hjust value and I am looking for a solution that graciously repositions itself, much like the axes do.
Following the suggestions in the comments by #tjebo I dug a little deeper into the coordinate spaces. hjust = 0.0 and hjust = 1.0 clearly align the label with the Cartesian coordinate system extent (but magically left-aligned and right-aligned, respectively) so when I set specific limits, calculation of the exact value of hjust is straightforward (aiming for 0 and hjust = (0 - -1.5) / (3.5 - -1.5) = 0.3):
ggplot(p, aes(x, y)) +
geom_point() +
coord_cartesian(ylim = c(8, 12.5), xlim = c(-1.5, 3.5), expand=FALSE) +
theme(axis.title.x = element_text(hjust = 0.3))
This gives an acceptable result for a label like x, but for longer labels the alignment is off again:
ggplot(p %>% mutate(`Longer X label` = x), aes(x = `Longer X label`, y = y)) +
geom_point() +
coord_cartesian(ylim = c(8, 12.5), xlim = c(-1.5, 3.5), expand=FALSE) +
theme(axis.title.x = element_text(hjust = 0.3))
Any further suggestions much appreciated.
Another option (different enough hopefully to justify the second answer) is as already mentioned to create the annotation as a separate plot. This removes the range problem. I like {patchwork} for this.
library(tidyverse)
library(patchwork)
p <- tibble( x = rnorm(50, 1), y = rnorm(50, 10))
p1 <- tibble( x = rnorm(50, 1), y = 100*rnorm(50, 10))
## I like to define constants outside my ggplot call
mylab <- "longer_label"
x_demo <- c(-1, 2)
demo_fct <- function(p){
p1 <- ggplot(p, aes(x, y)) +
geom_point() +
labs(x = NULL) +
theme(plot.margin = margin())
p2 <- ggplot(p, aes(x, y)) +
## you need that for your correct alignment with the first plot
geom_blank() +
annotate(geom = "text", x = x_demo, y = 1,
label = mylab, hjust = 0) +
theme_void() +
# you need that for those annoying margin reasons
coord_cartesian(clip = "off")
p1 / p2 + plot_layout(heights = c(1, .05))
}
demo_fct(p) + plot_annotation(title = "demo1 with x at -1 and 2")
demo_fct(p1) + plot_annotation(title = "demo2 with larger data range")
Created on 2021-12-04 by the reprex package (v2.0.1)
I still think you will fair better and easier with custom annotation. There are typically two ways to do that. Either direct labelling with a text layer (for single labels I prefer annotate(geom = "text"), or you create a separate plot and stitch both together, e.g. with patchwork.
The biggest challenge is the positioning in y dimension. For this I typically take a semi-automatic approach where I only need to define one constant, and set the coordinates relative to the data range, so changes in range should in theory not matter much. (they still do a bit, because the panel dimensions also change). Below showing examples of exact label positioning for two different data ranges (using the same constant for both)
library(tidyverse)
# I only need patchwork for demo purpose, it is not required for the answer
library(patchwork)
p <- tibble( x = rnorm(50, 1), y = rnorm(50, 10))
p1 <- tibble( x = rnorm(50, 1), y = 100*rnorm(50, 10))
## I like to define constants outside my ggplot call
y_fac <- .1
mylab <- "longer_label"
x_demo <- c(-1, 2)
demo_fct <- function(df, x) {map(x_demo,~{
## I like to define constants outside my ggplot call
ylims <- range(df$y)
ggplot(df, aes(x, y)) +
geom_point() +
## set hjust = 0 for full positioning control
annotate(geom = "text", x = ., y = min(ylims) - y_fac*mean(ylims),
label = mylab, hjust = 0) +
coord_cartesian(ylim = ylims, clip = "off") +
theme(plot.margin = margin(b = .5, unit = "in")) +
labs(x = NULL)
})
}
demo_fct(p, x_demo) %>% wrap_plots() + plot_annotation(title = "demo 1, label at x = -1 and x = 2")
demo_fct(p1, x_demo) %>% wrap_plots() + plot_annotation(title = "demo 2 - different data range")
Created on 2021-12-04 by the reprex package (v2.0.1)

ggplot2 facet labels - second line is not displayed

I have a script that used to produce a facetted plot with strip text on multiple lines. But this does not work anymore. Below is a MWE where the strip text should be parsed from, e.g. "bold(A)\nreally~long~extra" to:
A
really long extra
The second line is cut off as you can see via the debug function. I even increased the margins but to no avail...
Any ideas what is the issue?
exmpl = data.frame(a = 1:100,
b = rep(1:5, 20),
f = factor(rep(LETTERS[1:5], each = 20))) %>%
as_tibble() %>%
mutate(f2 = paste0("bold(",f, ")\nreally~long~extra"))
ggplot(exmpl, aes(x = b, y = a)) +
facet_grid(. ~ f2, labeller = label_parsed) +
geom_point() +
theme(strip.text.x = element_text(size = 10, hjust = 0, margin = margin(.5, 0, .5, 0, "cm"), debug = T))
EDIT:
And while we are at it, I only came up with this workaround because my previous solution of using label_bquote() does not work anymore. Please have a look at this other question, maybe you can help me with this, too?
Not sure wether this works for you. But one way to achieve the desired result would be to make use of the ggtext package, which allows you to style your facet labels using HTML and CSS. To this end ggtext introduces a new theme element element_markdown. Try this:
library(ggplot2)
library(dplyr)
exmpl = data.frame(a = 1:100,
b = rep(1:5, 20),
f = factor(rep(LETTERS[1:5], each = 20))) %>%
as_tibble() %>%
mutate(f2 = paste0("<b>", f, "</b><br>", "really long extra"))
ggplot(exmpl, aes(x = b, y = a)) +
facet_grid(. ~ f2) +
geom_point() +
theme(strip.text.x = ggtext::element_markdown(size = 10, hjust = 0))
And for the second question in your former post a solution might look like so:
mylabel <- function(x) {
mutate(x, Species = paste0(letters[Species], " <i>", Species, "</i>"))
}
p <- ggplot(iris, aes(Sepal.Length, Sepal.Width)) + geom_point()
p + facet_grid(. ~ Species, labeller = mylabel) +
theme(strip.text.x = ggtext::element_markdown())

Adding reference lines to a bar-plot with ggplot in R

This is a minimal example that shows the plots I am trying to make.
Data looks like this:
plot1 = data.frame(
Factor1 = as.factor(rep('A', 4)),
Factor2 = as.factor(rep(c('C', 'D'), 2)),
Factor3 = as.factor(c( rep('E', 2), rep('F', 2))),
Y = c(0.225490, 0.121958, 0.218182, 0.269789)
)
plot2 = data.frame(
Factor1 = as.factor(rep('B', 4)),
Factor2 = as.factor(rep(c('C', 'D'), 2)),
Factor3 = as.factor(c( rep('E', 2), rep('F', 2))),
Y = c(-0.058585, -0.031686, 0.013141, 0.016249)
)
While the basic code for plotting looks like this:
require(ggplot2)
require(grid)
p1 <- ggplot(data=plot1, aes(x=Factor2, y=Y, fill=factor(Factor3))) +
ggtitle('Type: A') +
coord_cartesian(ylim = c(-0.10, 0.30)) +
geom_bar(position=position_dodge(.9), width=0.5, stat='identity') +
scale_x_discrete(name='Regime',
labels=c('C', 'D')) +
scale_y_continuous('Activations') +
scale_fill_brewer(palette='Dark2', name='Background:',
breaks=c('E','F'),
labels=c('E','F')) +
theme(axis.text=element_text(size=11),
axis.title.x=element_text(size=13, vjust=-0.75),
axis.title.y=element_text(size=13, vjust=0.75),
legend.text=element_blank(),
legend.title=element_blank(),
legend.position='none',
plot.title=element_text(hjust=0.5))
p2 <- ggplot(data=plot2, aes(x=Factor2, y=Y, fill=factor(Factor3))) +
ggtitle('Type: B') +
coord_cartesian(ylim = c(-0.10, 0.30)) +
geom_bar(position=position_dodge(.9), width=0.5, stat='identity') +
scale_x_discrete(name='Regime',
labels=c('C', 'D')) +
scale_y_continuous('Activations') +
scale_fill_brewer(palette='Dark2', name='Background:',
breaks=c('E','F'),
labels=c('E','F')) +
theme(axis.text=element_text(size=11),
axis.title.x=element_text(size=13, vjust=-0.75),
axis.title.y=element_blank(),
legend.text=element_text(size=11),
legend.title=element_text(size=13),
plot.title=element_text(hjust=0.5))
pushViewport(viewport(
layout=grid.layout(1, 2, heights=unit(4, 'null'),
widths=unit(c(1,1.17), 'null'))))
print(p1, vp=viewport(layout.pos.row=1, layout.pos.col=1))
print(p2, vp=viewport(layout.pos.row=1, layout.pos.col=2))
And the figure looks like this:
However, I would need something like this:
Thick black lines are the reference values. They are constant and the Figure presents that "reference situation". However, in other plots that I need to produce bars will change but the reference values should remain the same to make the comparisons straightforward and easy. I know I should be using geom_segment() but those lines in my attempts to make this work are just missing the bars.
Any help/advice? Thanks!
I was able to do this using geom_errorbarh. For instance, with the second figure:
p1 +
geom_errorbarh(
aes(xmin = as.numeric(Factor2)-.2,xmax = as.numeric(Factor2)+.2), #+/-.2 for width
position = position_dodge(0.9), size = 2, height = 0
)
OUTPUT:
And, if I understand the other plots you describe, you can specify the reference data in those, eg data = plot1
If your references are not going to be changed, you can create a second dataset and merge it to the dataset you are going to plot.
Here, I first add plot1 and plot2. Then, I create a new dataset that will be the reference dataset.
library(dplyr)
new_df = rbind(plot1, plot2)
ref_plot = new_df
ref_plot <- ref_plot %>% rename(Ref_value = Y)
Then, now you have the new_df which is the dataset to be plot and ref_plot that contains references values for each conditions.
Instead of using grid and create two different plot that I will merge after, I preferred to use facet_wrap which put all plots on the same figure. It is much more convenient and don't require to write twice the same thing.
As mentioned by #AHart few minutes before me, you can use geom_errorbar to define your reference values on the plot. The difference is I prefere to use geom_errorbar instead of geom_errobarh.
Here is for the plot:
library(ggplot2)
new_df %>% left_join(ref_plot) %>%
ggplot(aes(x = Factor2, y = Y, fill = Factor3))+
geom_bar(stat = "identity", position = position_dodge())+
geom_errorbar(aes(ymin = Ref_value-0.00001, ymax = Ref_value+0.0001, group = Factor3), position = position_dodge(.9),width = 0.2)+
facet_wrap(.~Factor1, labeller = labeller(Factor1 = c(A = "Type A", B = "Type B"))) +
scale_x_discrete(name='Regime',
labels=c('C', 'D')) +
scale_fill_brewer(palette='Dark2', name='Background:',
breaks=c('E','F'),
labels=c('E','F')) +
theme(axis.text=element_text(size=11),
axis.title.x=element_text(size=13, vjust=-0.75),
axis.title.y=element_blank(),
legend.text=element_text(size=11),
legend.title=element_text(size=13),
plot.title=element_text(hjust=0.5))

How can I plot 2 related variables on the same axis using ggplot? [duplicate]

Edit: This question has been marked as duplicated, but the responses here have been tried and did not work because the case in question is a line chart, not a bar chart. Applying those methods produces a chart with 5 lines, 1 for each year - not useful. Did anyone who voted to mark as duplicate actually try those approaches on the sample dataset supplied with this question? If so please post as an answer.
Original Question:
There's a feature in Excel pivot charts which allows multilevel categorical axes.I'm trying to find a way to do the same thing with ggplot (or any other plotting package in R).
Consider the following dataset:
set.seed(1)
df=data.frame(year=rep(2009:2013,each=4),
quarter=rep(c("Q1","Q2","Q3","Q4"),5),
sales=40:59+rnorm(20,sd=5))
If this is imported to an Excel pivot table, it is straightforward to create the following chart:
Note how the x-axis has two levels, one for quarter and one for the grouping variable, year. Are multilevel axes possible with ggplot?
NB: There is a hack with facets that produces something similar, but this is not what I'm looking for.
library(ggplot2)
ggplot(df) +
geom_line(aes(x=quarter,y=sales,group=year))+
facet_grid(.~year,scales="free")
New labels are added using annotate(geom = "text",. Turn off clipping of x axis labels with clip = "off" in coord_cartesian.
Use theme to add extra margins (plot.margin) and remove (element_blank()) x axis text (axis.title.x, axis.text.x) and vertical grid lines (panel.grid.x).
library(ggplot2)
ggplot(data = df, aes(x = interaction(year, quarter, lex.order = TRUE),
y = sales, group = 1)) +
geom_line(colour = "blue") +
annotate(geom = "text", x = seq_len(nrow(df)), y = 34, label = df$quarter, size = 4) +
annotate(geom = "text", x = 2.5 + 4 * (0:4), y = 32, label = unique(df$year), size = 6) +
coord_cartesian(ylim = c(35, 65), expand = FALSE, clip = "off") +
theme_bw() +
theme(plot.margin = unit(c(1, 1, 4, 1), "lines"),
axis.title.x = element_blank(),
axis.text.x = element_blank(),
panel.grid.major.x = element_blank(),
panel.grid.minor.x = element_blank())
See also the nice answer by #eipi10 here: Axis labels on two lines with nested x variables (year below months)
The suggested code by Henrik does work and helped me a lot! I think the solution has a high value. But please be aware, that there is a small misstake in the first line of the code, which results in a wrong order of the data.
Instead of
... aes(x = interaction(year,quarter), ...
it should be
... aes(x = interaction(quarter,year), ...
The resulting graphic has the data in the right order.
P.S. I suggested an edit (which was rejected until now) and, due to a small lack of reputation, I am not allowed to comment, what I rather would have done.
User Tung had a great answer on this thread
library(tidyverse)
library(lubridate)
library(scales)
set.seed(123)
df <- tibble(
date = as.Date(41000:42000, origin = "1899-12-30"),
value = c(rnorm(500, 5), rnorm(501, 10))
)
# create year column for facet
df <- df %>%
mutate(year = as.factor(year(date)))
p <- ggplot(df, aes(date, value)) +
geom_line() +
geom_vline(xintercept = as.numeric(df$date[yday(df$date) == 1]), color = "grey60") +
scale_x_date(date_labels = "%b",
breaks = pretty_breaks(),
expand = c(0, 0)) +
# switch the facet strip label to the bottom
facet_grid(.~ year, space = 'free_x', scales = 'free_x', switch = 'x') +
labs(x = "") +
theme_classic(base_size = 14, base_family = 'mono') +
theme(panel.grid.minor.x = element_blank()) +
# remove facet spacing on x-direction
theme(panel.spacing.x = unit(0,"line")) +
# switch the facet strip label to outside
# remove background color
theme(strip.placement = 'outside',
strip.background.x = element_blank())
p

Different font faces and sizes within label text entries in ggplot2

I am building charts that have two lines in the axis text. The first line contains the group name, the second line contains that group population. I build my axis labels as a single character string with the format "LINE1 \n LINE2". Is it possible to assign different font faces and sizes to LINE1 and LINE2, even though they are contained within a single character string? I would like LINE1 to be large and bolded, and LINE2 to be small and unbolded.
Here's some sample code:
Treatment <- rep(c('T','C'),each=2)
Gender <- rep(c('Male','Female'),2)
Response <- sample(1:100,4)
test_df <- data.frame(Treatment, Gender, Response)
xbreaks <- levels(test_df$Gender)
xlabels <- paste(xbreaks,'\n',c('POP1','POP2'))
hist <- ggplot(test_df, aes(x=Gender, y=Response, fill=Treatment, stat="identity"))
hist + geom_bar(position = "dodge") + scale_y_continuous(limits = c(0,
100), name = "") + scale_x_discrete(labels=xlabels, breaks = xbreaks) +
opts(
axis.text.x = theme_text(face='bold',size=12)
)
I tried this, but the result was one large, bolded entry, and one small, unbolded entry:
hist + geom_bar(position = "dodge") + scale_y_continuous(limits = c(0,
100), name = "") + scale_x_discrete(labels=xlabels, breaks = xbreaks) +
opts(
axis.text.x = theme_text(face=c('bold','plain'),size=c('15','10'))
)
Another possible solution is to create separate chart elements, but I don't think that ggplot2 has a 'sub-axis label' element available...
Any help would be very much appreciated.
Cheers,
Aaron
I also think that I could not to make the graph by only using ggplot2 features.
I would use grid.text and grid.gedit.
require(ggplot2)
Treatment <- rep(c('T','C'), each=2)
Gender <- rep(c('Male','Female'), 2)
Response <- sample(1:100, 4)
test_df <- data.frame(Treatment, Gender, Response)
xbreaks <- levels(test_df$Gender)
xlabels <- paste(xbreaks,'\n',c('',''))
hist <- ggplot(test_df, aes(x=Gender, y=Response, fill=Treatment,
stat="identity"))
hist + geom_bar(position = "dodge") +
scale_y_continuous(limits = c(0, 100), name = "") +
scale_x_discrete(labels=xlabels, breaks = xbreaks) +
opts(axis.text.x = theme_text(face='bold', size=12))
grid.text(label="POP1", x = 0.29, y = 0.06)
grid.text(label="POP2", x = 0.645, y = 0.06)
grid.gedit("GRID.text", gp=gpar(fontsize=8))
Please try to tune a code upon according to your environment (e.g. the position of sub-axis labels and the fontsize).
I found another simple solution below:
require(ggplot2)
Treatment <- rep(c('T','C'),each=2)
Gender <- rep(c('Male','Female'),2)
Response <- sample(1:100,4)
test_df <- data.frame(Treatment, Gender, Response)
xbreaks <- levels(test_df$Gender)
xlabels[1] <- expression(atop(bold(Female), scriptstyle("POP1")))
xlabels[2] <- expression(atop(bold(Male), scriptstyle("POP2")))
hist <- ggplot(test_df, aes(x=Gender, y=Response, fill=Treatment,
stat="identity"))
hist +
geom_bar(position = "dodge") +
scale_y_continuous(limits = c(0, 100), name = "") +
scale_x_discrete(label = xlabels, breaks = xbreaks) +
opts(
axis.text.x = theme_text(size = 12)
)
All,
Using Triad's cheat this is the closest I was able to get to solution on this one. Let me know if you have any questions:
library(ggplot2)
spacing <- 0 #We can adjust how much blank space we have beneath the chart here
labels1= paste('Group',c('A','B','C','D'))
labels2 = rep(paste(rep('\n',spacing),collapse=''),length(labels1))
labels <- paste(labels1,labels2)
qplot(1:4,1:4, geom="blank") +
scale_x_continuous(breaks=1:length(labels), labels=labels) + xlab("")+
opts(plot.margin = unit(c(1, 1, 3, 0.5), "lines"),
axis.text.x = theme_text(face='bold', size=14))
xseq <- seq(0.15,0.9,length.out=length(labels)) #Assume for now that 0.15 and 0.9 are constant plot boundaries
sample_df <- data.frame(group=rep(labels1,each=2),subgroup=rep(c('a','b'),4),pop=sample(1:10,8))
popLabs <- by(sample_df,sample_df$group,function(subData){
paste(paste(subData$subgroup,' [n = ', subData$pop,']',sep=''),collapse='\n')
})
gridText <- paste("grid.text(label='\n",popLabs,"',x=",xseq,',y=0.1)',sep='')
sapply(gridText, function(x){ #Evaluate parsed character string for each element of gridText
eval(parse(text=x))
})
grid.gedit("GRID.text", gp=gpar(fontsize=12))
Cheers,
Aaron

Resources