Trouble with ggplot2 multiple legend alignment with grid.arrange - r

I'm trying to add two legends to the same ggplot2 graph and I'm having a terrible time with the alignment. I've got points representing some data and then fitted regression lines as well, so I want the legend for colors to be split up so that it's clear which are just for the points and which are for the fitted lines. Here's my best attempt so far:
library(ggplot2)
library(gridExtra)
StudyResults <- data.frame(TreatmentArm = rep(c("A", "B"), each = 10),
SubjectID = rep(1:10, each = 2),
Glucose = rnorm(20, 50, 10),
Insulin = rnorm(20, 0.15, 0.05),
StudyDay = rep(c("SD1", "SD2"), 10))
Trend <- data.frame(Gender = rep(c("F", "M"), each = 50),
Glucose = seq(20, 80, length = 50),
Insulin = NA)
Trend$Insulin[Trend$Gender == "F"] <- 2/Trend$Glucose[Trend$Gender == "F"]
Trend$Insulin[Trend$Gender == "M"] <- 5/Trend$Glucose[Trend$Gender == "M"]
PlotTrend <- ggplot(Trend, aes(x = Glucose, y = Insulin, color = Gender)) +
geom_line() + scale_color_manual(values = c("red", "blue"))
PlotStudy <- ggplot(StudyResults, aes(x = Glucose, y = Insulin, shape = StudyDay,
color = TreatmentArm, group = SubjectID)) +
geom_point() + geom_line() +
scale_color_manual(values = c("green", "black"))
g_legend <- function(a.gplot){
tmp <- ggplot_gtable(ggplot_build(a.gplot))
leg <- which(sapply(tmp$grobs, function(x) x$name) == "guide-box")
legend <- tmp$grobs[[leg]]
return(legend)}
LegendTrend <- g_legend(PlotTrend)
LegendStudy <- g_legend(PlotStudy)
PlotMain <- ggplot(StudyResults, aes(x = Glucose, y = Insulin, shape = StudyDay,
color = TreatmentArm, group = SubjectID)) +
geom_point() + geom_line() +
scale_color_manual(values = c("green", "black")) +
geom_line(data = Trend[Trend$Gender == "F", ],
aes(x = Glucose, y = Insulin),
inherit.aes = FALSE, color = "red") +
geom_line(data = Trend[Trend$Gender == "M", ],
aes(x = Glucose, y = Insulin),
inherit.aes = FALSE, color = "blue") +
theme(legend.position = "none")
grid.arrange(PlotMain,
arrangeGrob(LegendStudy, LegendTrend, nrow = 2,
heights = c(unit(0.5, "npc"),
unit(0.5, "npc")),
widths = unit(0.5, "npc")),
ncol = 2, widths = c(10, 3))
But the positioning of the legends is TERRIBLE:
I don't know how to make the legend for "Gender" be aligned on the left with the other two, and I don't want there to be so much white space in the middle.
I also fiddled with gtable and grid packages' commands like viewPort but am completely clueless about how to use them. (Suggestions for decent tutorials would be much appreciated; I haven't ever found any.) I tried
library(gtable)
library(grid)
grid.newpage()
pushViewport(vp = viewport())
vp1 <- viewport(width = 0.8, height = 1, x = 0, y = 0)
grid.draw(PlotMain)
vp2 <- viewport(width = 0.2, x = 0.9, y = 0.6)
pushViewport(vp2)
grid.draw(LegendStudy)
pushViewport(vp3 = viewport(width = 0.2, x = 0.9, y = 0.4))
grid.draw(LegendTrend)
but I clearly have no idea how to use this because the legends overlapped the main plot and were positioned in what seems to me a completely random (i.e., unrelated to the x and y coordinates I thought I was specifying) way.

it's unclear what the OP wants but the next option would be adding the second legend's gtable to the first,
library(gtable)
leg2 <- LegendTrend$grobs[[1]]
leg <- gtable_add_rows(LegendStudy, pos = nrow(LegendStudy) - 1,
heights = sum(leg2$heights))
leg <- gtable_add_grob(leg, leg2, t = nrow(leg) - 1, l = 3)
grid.arrange(PlotMain, right = leg)
For a tutorial on grid viewports, there's the R graphics book, but ggplot2's design has drifted substantially from base grid with the introduction of gtable as an intermediate framework to place graphic elements. ggplot2 legends are a complex structure of nested gtables, that few people understand. gtable is not documented, and its development stopped early on, so the best source of information is the code itself.

leg <- gtable_rbind(LegendStudy, LegendTrend, size = "first")
grid.arrange(PlotMain, right = leg)

Related

Display more than three variables using different aesthetics in a ggplot2 bar chart

Can the following chart be generated using ggplot2:
There are two variables mapped onto the axises, one variable (Region) mapped onto the colour (using grouped bars) and one variable (Product) mapped onto some other aethetics (alpha, pattern, line style)
How would that be possible? An example using R is welcome.
Update
In my original question I did not think about facets. Of course with facets you are able to display four variables. The question should be reformulated as Display more than four variables using different aesthetics in a ggplot2 bar chart ...
Here is an approach by abusing facets to serve as an x-axis so you can both stack and "dodge" the data. You can look into the ggpattern package, but I'm not fluent in its use.
library(ggplot2)
df <- expand.grid(
region = c("North", "East", "South", "West"),
product = c("Red wine", 'White wine'),
year = 2013:2015
)
set.seed(42)
df$value <- runif(nrow(df))
ggplot(df, aes(region, value)) +
geom_col(aes(alpha = product, fill = region), width = 1) +
# Expand x axis to control the width of 'dodging'
scale_x_discrete(expand = c(0.5, 0), breaks = NULL, name = NULL) +
scale_alpha_manual(values = c(0.6, 1)) +
facet_grid(~ year, switch = "x") +
# 0 spacing gives impression it is a single panel
theme(panel.spacing.x = unit(0, "pt"))
Created on 2021-09-24 by the reprex package (v2.0.1)
EDIT: An alternative without using facets, but with use of a helper function to position everything on the x-axis:
helper <- function(center, offset, width = 0.6) {
if (!is.numeric(center)) {
center <- match(center, sort(unique(center)))
}
offset <- match(offset, sort(unique(offset)))
offset <- scales::rescale(offset, to = c(-0.5, 0.5) * width)
center + offset
}
ggplot(df, aes(helper(year, region), value)) +
geom_col(aes(alpha = product, fill = region), width = 0.15) +
scale_alpha_manual(values = c(0.6, 1)) +
scale_x_continuous(breaks = scales::breaks_width(1)) +
theme(panel.spacing.x = unit(0, "pt"))
Here's a base version that gets most of the way there
set.seed(1)
d <- replicate(15, rpois(2, 10))
s <- replace(rep(0.1, 15), 1:2 * 5 + 1, 1)
op <- par(mar = c(5, 4, 2, 7), las = 1)
bp <- barplot(colSums(d), space = s, col = 2:6)
barplot(d, space = s, add = TRUE, density = c(0, 10), col = 'black', border = 'black')
abline(h = 0)
axis(1L, bp[1:3 * 5 - 2], 13:15 + 2000, lwd = 0)
title(xlab = 'Year', cex.lab = 1.5)
l <- list(
list(
title = 'Region', fill = 2:6,
legend = c('North', 'South', 'East', 'West', 'Center')
),
list(
title = 'Product', density = c(20, 0),
legend = c('Red wine', 'White wine')
)
)
lg <- legend('topright', legend = '', bty = 'n', inset = c(-0.025, 0))
for (ii in seq_along(l)) {
lg <- do.call('legend', c(
list(x = lg$rect$left, y = lg$rect$top - lg$rect$h,
xpd = NA, bty = 'n', title.adj = 0), l[[ii]]
))
}
par(op)

ggforce facet_zoom error with ggplot2 on R

I have a data.frame in R 4.0.2 with a continuous variable in one column and two possible values of a categorical variable (variable 'type': known or novel) in another, which I use to color them differently (using a palette from ggsci 2.9 package). I represent an histogram (stat_bin) with ggplot2 3.3.2 and I want to use the facet_zoom function of ggforce 0.3.2 to zoom only the data belonging to one of the 'types' (using the option zoom.data, as it is done in the volcano example on http://cran.univ-paris1.fr/web/packages/ggforce/vignettes/Visual_Guide.html#contextual-zoom), however I get this error:
Error: Aesthetics must be either length 1 or the same as the data (2000): x
Reproducible example:
library(ggplot2)
library(ggsci)
library(ggforce)
testdata <- as.data.frame(sort(rnorm(1000)))
testdata$type <- "known"
testdata[501:1000,2] <- "novel"
# Working code
ggplot(testdata) +
stat_bin(aes(x=testdata[,1], fill = type), binwidth = 1, color="white") +
scale_fill_npg() + theme_light() +
facet_zoom(xlim = c(0, 4), ylim = c(0, 300), horizontal = TRUE, zoom.size = 0.3)
# Desired code
ggplot(testdata) +
stat_bin(aes(x=testdata[,1], fill = type), data = cbind(testdata, zoom = FALSE), binwidth = 1, color="white") +
stat_bin(aes(x=testdata[testdata$type == "novel",1]), data = cbind(testdata, zoom = TRUE), binwidth = 0.5) +
scale_fill_npg() + theme_light() +
facet_zoom(xlim = c(0, 4), ylim = c(0, 300), horizontal = TRUE, zoom.size = 0.3, zoom.data = zoom)
Thanks!
The issue is that you pass the whole dataset as data in the second stat_bin. Simply pass the subsetted df instead of trying to subset in aes():
BTW: I also renamed the first variable in your data as x.
library(ggplot2)
library(ggsci)
library(ggforce)
set.seed(42)
testdata <- data.frame(x = sort(rnorm(1000)))
testdata$type <- "known"
testdata[501:1000,2] <- "novel"
# Desired code
ggplot(testdata) +
stat_bin(aes(x = x, fill = type), data = cbind(testdata, zoom = FALSE), binwidth = 1, color="white") +
stat_bin(aes(x = x), data = cbind(testdata[testdata$type == "novel", ], zoom = TRUE), binwidth = 0.5) +
scale_fill_npg() + theme_light() +
facet_zoom(xlim = c(0, 4), ylim = c(0, 300), horizontal = TRUE, zoom.size = 0.3, zoom.data = zoom)
To only show the type == "novel" data in the zoomed plot, try this:
library(tidyverse)
library(ggsci)
library(ggforce)
testdata <- data.frame(values = sort(rnorm(1000)))
testdata$type <- "known"
testdata[501:1000,2] <- "novel"
# Desired code
ggplot(testdata) +
stat_bin(aes(x = values, fill = type),
binwidth = 1, color="white") +
scale_fill_npg() + theme_light() +
facet_zoom(zoom.data = ifelse(type == "novel", NA, FALSE),
xlim = c(0, 4), ylim = c(0, 300),
horizontal = TRUE)

Lines on top and bottom of ggplot plot area

I want to add a line on the top and bottom of my plots (bottom line below the x label and axis) created using ggplot2. So far I have added a rectangle around the plot, but I do not want the lines on the sides.
x <- 1:10
y <- rnorm(10,mean = x)
df <- data.frame(x,y)
library(ggplot2)
ggplot(data = df, mapping = aes(x,y)) + geom_point() +
theme(plot.background = element_rect(size = 1, color = 'blue'))
I hope you guys have a solution.
Will something similar to this work?
x <- 1:10
y <- rnorm(10,mean = x)
df <- data.frame(x,y)
ggplot(data = df, mapping = aes(x,y)) + geom_point() +
annotate(geom = 'segment',
y = Inf,
yend = Inf,
x = -Inf,
xend = Inf,
size = 2) +
theme(axis.line.x = element_line(size = 1))
Not a perfect, but working solution. You have to plot huge "-" (size = 1000) outside plot area. This solution is not perfect as you have to manually adjust position of "-" on the y-axis.
df <- data.frame(x = 1:10, y = 1:10)
library(ggplot2)
ggplot(df, aes(x, y)) +
geom_point() +
# Y position adjusted manually
geom_text(aes(5, 2.9, label = "-"), color = "blue", size = 1000) +
# Y position adjusted manually
geom_text(aes(5, 21.2, label = "-"), color = "blue", size = 1000) +
# Plot outside plot area
coord_cartesian(ylim = c(0, 10), clip = "off")
I am not completely happy with the solution as I don't fully grasp
how to change the size of the lines
why they are not perfectly aligned with top and bottom when using patchwork::wrap_plots()
why it does not show the top line using ggpubr::ggarrange() or cowplot::plot_grid()
but based on this code, I suggest the following solution:
library(ggplot2)
df <- data.frame(x = 1:5, y = 1:5)
p <- ggplot(data = df) + aes(x, y) + geom_point()
top_line <- grid::grobTree(grid::linesGrob(x = grid::unit(c(0, 1), "npc"), y = grid::unit(1, "npc")))
bot_line <- grid::grobTree(grid::linesGrob(x = grid::unit(c(0, 1), "npc"), y = grid::unit(0, "npc")))
patchwork::wrap_plots(top_line, p, bot_line,
ncol = 1, nrow = 3,
heights = c(0, 1, 0))
ggpubr::ggarrange(top_line, p, bot_line,
ncol = 1, nrow = 3,
heights = c(0, 1, 0))
cowplot::plot_grid(top_line, p, bot_line,
ncol = 1, nrow = 3,
rel_heights = c(0, 1, 0))
Created on 2022-08-25 with reprex v2.0.2

how to draw two half circles in ggplot in r

How can I make a plot like this with two different-sized half circles (or other shapes such as triangles etc.)?
I've looked into a few options: Another post suggested using some unicode symbol, that didn't work for me. And if I use a vector image, how can I properly adjust the size parameter so the 2 circles touch each other?
Sample data (I would like to make the size of the two half-circles equal to circle1size and circle2size):
df = data.frame(circle1size = c(1, 3, 2),
circle2size = c(3, 6, 5),
middlepointposition = c(1, 2, 3))
And ultimately is there a way to position the half-circles at different y-values too, to encode a 3rd dimension, like so?
Any advice is much appreciated.
What you're asking for is a bar plot in polar coordinates. This can be done easily in ggplot2. Note that we need to map y = sqrt(count) to get the area of the half circle proportional to the count.
df <- data.frame(x = c(1, 2),
type = c("Investors", "Assignees"),
count = c(19419, 1132))
ggplot(df, aes(x = x, y = sqrt(count), fill = type)) + geom_col(width = 1) +
scale_x_discrete(expand = c(0,0), limits = c(0.5, 2.5)) +
coord_polar(theta = "x", direction = -1)
Further styling would have to be applied to remove the gray background, remove the axes, change the color, etc., but that's all standard ggplot2.
Update 1: Improved version with multiple countries.
df <- data.frame(x = rep(c(1, 2), 3),
type = rep(c("Investors", "Assignees"), 3),
country = rep(c("Japan", "Germany", "Korea"), each = 2),
count = c(19419, 1132, 8138, 947, 8349, 436))
df$country <- factor(df$country, levels = c("Japan", "Germany", "Korea"))
ggplot(df, aes(x=x, y=sqrt(count), fill=type)) + geom_col(width =1) +
scale_x_continuous(expand = c(0, 0), limits = c(0.5, 2.5)) +
scale_y_continuous(expand = c(0, 0)) +
coord_polar(theta = "x", direction = -1) +
facet_wrap(~country) +
theme_void()
Update 2: Drawing the individual plots at different locations.
We can do some trickery to take the individual plots and plot them at different locations in an enclosing plot. This works, and is a generic method that can be done with any sort of plot, but it's probably overkill here. Anyways, here is the solution.
library(tidyverse) # for map
library(cowplot) # for draw_text, draw_plot, get_legend, insert_yaxis_grob
# data frame of country data
df <- data.frame(x = rep(c(1, 2), 3),
type = rep(c("Investors", "Assignees"), 3),
country = rep(c("Japan", "Germany", "Korea"), each = 2),
count = c(19419, 1132, 8138, 947, 8349, 436))
# list of coordinates
coord_list = list(Japan = c(1, 3), Germany = c(2, 1), Korea = c(3, 2))
# make list of individual plots
split(df, df$country) %>%
map( ~ ggplot(., aes(x=x, y=sqrt(count), fill=type)) + geom_col(width =1) +
scale_x_continuous(expand = c(0, 0), limits = c(0.5, 2.5)) +
scale_y_continuous(expand = c(0, 0), limits = c(0, 160)) +
draw_text(.$country[1], 1, 160, vjust = 0) +
coord_polar(theta = "x", start = 3*pi/2) +
guides(fill = guide_legend(title = "Type", reverse = T)) +
theme_void() + theme(legend.position = "none") ) -> plotlist
# extract the legend
legend <- get_legend(plotlist[[1]] + theme(legend.position = "right"))
# now plot the plots where we want them
width = 1.3
height = 1.3
p <- ggplot() + scale_x_continuous(limits = c(0.5, 3.5)) + scale_y_continuous(limits = c(0.5, 3.5))
for (country in names(coord_list)) {
p <- p + draw_plot(plotlist[[country]], x = coord_list[[country]][1]-width/2,
y = coord_list[[country]][2]-height/2,
width = width, height = height)
}
# plot without legend
p
# plot with legend
ggdraw(insert_yaxis_grob(p, legend))
Update 3: Completely different approach, using geom_arc_bar() from the ggforce package.
library(ggforce)
df <- data.frame(start = rep(c(-pi/2, pi/2), 3),
type = rep(c("Investors", "Assignees"), 3),
country = rep(c("Japan", "Germany", "Korea"), each = 2),
x = rep(c(1, 2, 3), each = 2),
y = rep(c(3, 1, 2), each = 2),
count = c(19419, 1132, 8138, 947, 8349, 436))
r <- 0.5
scale <- r/max(sqrt(df$count))
ggplot(df) +
geom_arc_bar(aes(x0 = x, y0 = y, r0 = 0, r = sqrt(count)*scale,
start = start, end = start + pi, fill = type),
color = "white") +
geom_text(data = df[c(1, 3, 5), ],
aes(label = country, x = x, y = y + scale*sqrt(count) + .05),
size =11/.pt, vjust = 0)+
guides(fill = guide_legend(title = "Type", reverse = T)) +
xlab("x axis") + ylab("y axis") +
coord_fixed() +
theme_bw()
If you don't need to have ggplot2 map aesthetics other than x and y you could try egg::geom_custom,
# devtools::install_github("baptiste/egg")
library(egg)
library(grid)
library(ggplot2)
d = data.frame(r1= c(1,3,2), r2=c(3,6,5), x=1:3, y=1:3)
gl <- Map(mushroomGrob, r1=d$r1, r2=d$r2, gp=list(gpar(fill=c("bisque","maroon"), col="white")))
d$grobs <- I(gl)
ggplot(d, aes(x,y)) +
geom_custom(aes(data=grobs), grob_fun=I) +
theme_minimal()
with the following grob,
mushroomGrob <- function(x=0.5, y=0.5, r1=0.2, r2=0.1, scale = 0.01, angle=0, gp=gpar()){
grob(x=x,y=y,r1=r1,r2=r2, scale=scale, angle=angle, gp=gp , cl="mushroom")
}
preDrawDetails.mushroom <- function(x){
pushViewport(viewport(x=x$x,y=x$y))
}
postDrawDetails.mushroom<- function(x){
upViewport()
}
drawDetails.mushroom <- function(x, recording=FALSE, ...){
th2 <- seq(0,pi, length=180)
th1 <- th2 + pi
d1 <- x$r1*x$scale*cbind(cos(th1+x$angle*pi/180),sin(th1+x$angle*pi/180))
d2 <- x$r2*x$scale*cbind(cos(th2+x$angle*pi/180),sin(th2+x$angle*pi/180))
grid.polygon(unit(c(d1[,1],d2[,1]), "snpc")+unit(0.5,"npc"),
unit(c(d1[,2],d2[,2]), "snpc")+unit(0.5,"npc"),
id=rep(1:2, each=length(th1)), gp=x$gp)
}
# grid.newpage()
# grid.draw(mushroomGrob(gp=gpar(fill=c("bisque","maroon"), col=NA)))

Make a rectangular legend, with rows and columns labeled, in grid

I've got a ggplot where I'm mapping factors to both fill and alpha, like this:
set.seed(47)
the_data <- data.frame(value = rpois(6, lambda=20),
cat1 = rep(c("A", "B"), each = 3),
cat2 = rep(c("X", "Y", "Z"), 2))
ggplot(the_data, aes(y = value, x = cat2, alpha = cat1, fill = cat2)) +
geom_bar(stat = "identity", position = "dodge") +
scale_alpha_discrete(range = c(0.5, 1)) +
theme_bw()
The people I'm producing it for don't find the legend for alpha very clear. I think a good alternative would be something like this (which I hacked together in base graphics):
I know I can't generate a legend like that with high-level ggplot commands, but can I do it in grid and put it on top of my plot?
Here is one possible starting point. I create two different plots which have the appropriate legends - a 'bright' and a 'pale'. Extract the legends from the plot objects. Then use grid viewports, one for the plot, and one for each legend, to put the pieces together.
library(grid)
library(gtable)
# create plot with legend with alpha = 1
g1 <- ggplot(the_data, aes(y = value, x = cat2, alpha = cat1, fill = cat2)) +
geom_bar(stat = "identity", position = "dodge") +
scale_alpha_discrete(range = c(0.5, 1)) +
theme_bw() +
guides(fill = guide_legend(title = "A",
title.hjust = 0.4),
alpha = FALSE) +
theme_bw() +
theme(legend.text = element_blank())
g1
# grab legend
legend_g1 <- gtable_filter(ggplot_gtable(ggplot_build(g1)), "guide-box")
# create plot with 'pale' legend
g2 <- ggplot(the_data, aes(y = value, x = cat2, alpha = cat1, fill = cat2)) +
geom_bar(stat = "identity", position = "dodge") +
scale_alpha_discrete(range = c(0.5, 1)) +
guides(fill = guide_legend(override.aes = list(alpha = 0.5),
title = "B",
title.hjust = 0.3),
alpha = FALSE) +
theme_bw()
g2
# grab legend
legend_g2 <- gtable_filter(ggplot_gtable(ggplot_build(g2)), "guide-box")
# arrange plot and legends
# legends to the right
# define plotting regions (viewports)
vp_plot <- viewport(x = 0.4, y = 0.5,
width = 0.8, height = 1)
vp_legend_g1 <- viewport(x = 0.85, y = 0.5,
width = 0.4, height = 0.4)
vp_legend_g2 <- viewport(x = 0.90, y = 0.5,
width = 0.4, height = 0.4)
# clear current device
grid.newpage()
# add objects to the viewports
# plot without legend
print(g1 + theme(legend.position = "none"), vp = vp_plot)
upViewport(0)
pushViewport(vp_legend_g1)
grid.draw(legend_g1)
upViewport(0)
pushViewport(vp_legend_g2)
grid.draw(legend_g2)
# legends on top
vp_plot <- viewport(x = 0.5, y = 0.4,
width = 1, height = 0.85)
vp_legend_g1 <- viewport(x = 0.5, y = 0.9,
width = 0.4, height = 0.4)
vp_legend_g2 <- viewport(x = 0.55, y = 0.9,
width = 0.4, height = 0.4)
grid.newpage()
print(g1 + theme(legend.position = "none"), vp = vp_plot)
upViewport(0)
pushViewport(vp_legend_g1)
grid.draw(legend_g1)
upViewport(0)
pushViewport(vp_legend_g2)
grid.draw(legend_g2)
#Henrik
This might be a little easier,
g1 <- ggplotGrob(p1)
g2 <- ggplotGrob(p2)
leg1 <- gtable_filter(g1, "guide-box")
leg2 <- gtable_filter(g2, "guide-box")
leg <- gtable:::cbind_gtable(leg1[["grobs"]][[1]], leg2[["grobs"]][[1]], "first")
g1$grobs[g1$layout$name == "guide-box"][[1]] <- leg
g1$widths[max(subset(g1$layout, name == "guide-box")[["r"]])] <- list(leg1$width + leg2$width)
grid.newpage()
grid.draw(g1)

Resources