How to specify the legend box size in ggplot/ggplot2 - r

In R/ggplot2, I have multiple plots, each of which has a legend box.
I want the legend box to be the same width for each plot, but ggplot2 tries to dynamically size the legend box based on the legend name, key values, etc. (which are unique to each plot).
The various plots must fit into a specified publication slot, with a specified width for the legend, and the plots must be made separately (so faceting to guarantee identical legend widths across the plots isn't possible).
Looking at theme I couldn't find an option to specify the legend box width ... any ideas?

To specify the legend box size you could use + theme(legend.key.size = unit(2, "cm")).
library(tidyverse)
tb <- tibble(a = 1:10, b = 10:1, c = rep(1:2, 5))
ggplot(tb, aes(a, b, colour = c)) +
geom_point() +
theme(legend.key.size = unit(0.2, "cm"))
More details and additional modifications are here and under the keywidth argument here.

#Z.lin had the right approach in the comments. Based on https://wilkelab.org/cowplot/articles/shared_legends.html this might look something like:
library(ggplot2)
library(cowplot)
Make a ggplot object
my_plot <- ggplot(iris, aes(x = Sepal.Length, y = Petal.Length, colour = Species))+
geom_point()
Extract the legend
my_legend <- get_legend(
# create some space to the left of the legend
my_plot + theme(legend.box.margin = margin(0, 0, 0, 12))
)
Re-plot your plot in a grid without the legend (can combine multiple plots here if desired)
my_plot_nl <- <- plot_grid(
my_plot + theme(legend.position="none"),
align = 'vh',
hjust = -1,
nrow = 1
)
Recombine your legend-free plot and legend and specify the relative width of each. The plot now takes up 3/4 of the plot width and the legend 1/4.
plot_grid(my_plot_nl, my_legend, rel_widths = c(3,1))
If you do this for each plot, making sure to use the same rel_widths and saving the figures using the same dimensions, the plot area and legend should be consistent across them.

You might attempt to change your theme call as follows:
theme(legend.margin =margin(r=10,l=5,t=5,b=5))?

Related

How to turn my legend horizontal as opposed to vertical with ggplot2?

I am struggling to understand why legend.horizontal is not rotating my legend axis so it isn't displaying vertically? Any help would be massively appreciated.
library(phyloseq)
library(ggplot2)
##phylum level
ps_tmp <- get_top_taxa(physeq_obj = ps.phyl, n = 10, relative = TRUE, discard_other = FALSE, other_label = "Other")
ps_tmp <- name_taxa(ps_tmp, label = "Unkown", species = T, other_label = "Other")
phyl <- fantaxtic_bar(ps_tmp, color_by = "phylum", label_by = "phylum",facet_by = "TREATMENT", other_label = "Other", order_alg = "as.is")
phyl + theme(legend.direction = "horizontal", legend.position = "bottom", )
Legends for discrete values don't have a formal direction per se and are positioned however ggplot2 decides it can best fit with your data. This is why things like legend.direction won't work here. I don't have the phyloseq package or access to your particular data, so I'll show you how this works and how you can mess with the legend using a reproducible example dataset.
library(ggplot2)
set.seed(8675309)
df <- data.frame(x=LETTERS[1:8], y=sample(1:100, 8))
p <- ggplot(df, aes(x, y, fill=x)) + geom_col()
p
By default, ggplot is putting our legend to the right and organizes it vertically as one column. Here's what happens when we move the legend to the bottom:
p + theme(legend.position="bottom")
Now ggplot thinks it's best to put that legend into 4 columns, 2 rows each. As u/Tech Commodities mentioned, you can use the guides() functions to specify how the legend looks. In this case, we will specify to have 2 columns instead of 4. We only need to supply the number of columns (or rows), and ggplot figures out the rest.
p + theme(legend.position="bottom") +
guides(fill=guide_legend(ncol=2))
So, to get a "horizontally-arranged" legend, you just need to specify that there should be only one row:
p + theme(legend.position="bottom") +
guides(fill=guide_legend(nrow=1))

ggplot2: how to horizontal center legend at top of plot with long Y-axis labels?

I am attempting to plot some data with really long Y-axis labels. I want to position the legend at the top of the plot, and center it horizontally w.r.t. the entire plot width (as dictated by the width of the panel + the Y-axis labels). I can't seem to figure out how to do this.
Here is the toy data I have been working with:
library(magrittr)
library(dplyr)
library(ggplot2)
iris_new <- iris
iris_new %<>%
mutate(Sepal.Width = paste0(Sepal.Width,
paste0(sample(letters, size = 100, replace = TRUE),
collapse = "")))
plt0 <- ggplot(iris_new, aes(Sepal.Length, Sepal.Width, color = Species)) +
geom_point() + theme(legend.position = "top", legend.direction = "horizontal")
plt0
The basic plot:
legend.justification = "left" simulates what I want, but not perfectly since it is simply aligning the legend to the left margin of the panel, and it just so happens that this approximately coincides with the middle of the plot width.
plt1 <- plt0 + theme(legend.justification = "left")
plt1
But this solution isn't really satisfactory as I cannot guarantee that the Y-axis labels might not get longer (or shorter). Of course, I could use stringr::str_wrap() to limit the label width to say 40 characters, but I really want to know if there is a simple ggplot2 solution to the alignment issue. This would also not account for the fact that the plotted data might force the X-axis to vary between plots.
guides(fill = guide_legend(title.position = "left", label.position = "left")) and guides(fill = guide_legend(title.hjust = 0, label.hjust = 0)) didn't work either.
The expected output would be something like this (simulated using LibreOffice Impress):
I apologize if this is a trivial question. I did try and search for relevant posts on stackoverflow, but I was unable to find a solution.
This is close to what you require
p1 <-ggplot(iris_new, aes(Sepal.Length, Sepal.Width, color = Species)) +
geom_point()
p2 <- p1 + theme(legend.position = "top")
le1 <- cowplot::get_legend(p1)
le1
cowplot::plot_grid(p2, le1, nrow = 2, rel_heights = c(1, 0.2))
Although I do see another legend at the bottom not sure how to remove it but horizontal center its close

Is it possible to specify the size / layout of a single plot to match a certain grid in R?

I use R for most of my data analysis. Until now I used to export the results as a CSV and visualized them using Macs Numbers.
The reason: The Graphs are embeded in documents and there is a rather large border on the right side reserved for annotations (tufte handout style). Between the acutal text and the annotations column there is white space. The plot of the graphs needs to fit the width of text while the legend should be placed in the annotation column.
I would prefer to also create the plots within R for a better workflow and higher efficiency. Is it possible to create such a layout using plotting with R?
Here is an example of what I would like to achieve:
And here is some R Code as a starter:
library(tidyverse)
data <- midwest %>%
head(5) %>%
select(2,23:25) %>%
pivot_longer(cols=2:4,names_to="Variable", values_to="Percent") %>%
mutate(Variable=factor(Variable, levels=c("percbelowpoverty","percchildbelowpovert","percadultpoverty"),ordered=TRUE))
ggplot(data=data, mapping=aes(x=county, y=Percent, fill=Variable)) +
geom_col(position=position_dodge(width=0.85),width=0.8) +
labs(x="County") +
theme(text=element_text(size=9),
panel.background = element_rect(fill="white"),
panel.grid = element_line(color = "black",linetype="solid",size= 0.3),
panel.grid.minor = element_blank(),
panel.grid.major.x=element_blank(),
axis.line.x=element_line(color="black"),
axis.ticks= element_blank(),
legend.position = "right",
legend.title = element_blank(),
legend.box.spacing = unit(1.5,"cm") ) +
scale_y_continuous(breaks= seq(from=0, to=50,by=5),
limits=c(0,51),
expand=c(0,0)) +
scale_fill_manual(values = c("#CF232B","#942192","#000000"))
I know how to set a custom font, just left it out for easier saving.
Using ggsave
ggsave("Graph_with_R.jpeg",plot=last_plot(),device="jpeg",dpi=300, width=18, height=9, units="cm")
I get this:
This might resample the result aimed for in the actual case, but the layout and sizes do not fit exact. Also recognize the different text sizes between axis titles, legend and tick marks on y-axes. In addition I assume the legend width depends on the actual labels and is not fixed.
Update
Following the suggestion of tjebo I posted a follow-up question.
Can it be done? Yes. Is it convenient? No.
If you're working in ggplot2 you can translate the plot to a gtable, a sort of intermediate between the plot specifications and the actual drawing. This gtable, you can then manipulate, but is messy to work with.
First, we need to figure out where the relevant bits of our plot are in the gtable.
library(ggplot2)
library(gtable)
library(grid)
plt <- ggplot(mtcars, aes(factor(cyl), fill = factor(vs))) +
geom_bar(position = position_dodge2(preserve = "single"))
# Making gtable
gt <- ggplotGrob(plt)
gtable_show_layout(gt)
Then, we can make a new gtable with prespecified dimensions and place the bits of our old gtable into it.
# Making a new gtable
new <- gtable(widths = unit(c(12.5, 1.5, 4), "cm"),
heights = unit(9, "cm"))
# Adding main panel and axes in first cell
new <- gtable_add_grob(
new,
gt[7:9, 3:5], # If you see the layout above as a matrix, the main bits are in these rows/cols
t = 1, l = 1
)
# Finding the legend
legend <- gt$grobs[gt$layout$name == "guide-box"][[1]]
legend <- legend$grobs[legend$layout$name == "guides"][[1]]
# Adding legend in third cell
new <- gtable_add_grob(
new, legend, t = 1, l = 3
)
# Saving as raster
ragg::agg_png("test.png", width = 18, height = 9, units = "cm", res = 300)
grid.newpage(); grid.draw(new)
dev.off()
#> png
#> 2
Created on 2021-04-02 by the reprex package (v1.0.0)
The created figure should match the dimensions you're looking for.
Another option is to draw the three components as separate plots and stitch them together in the desired ratio.
The below comes quite close to the desired ratio, but not exactly. I guess you'd need to fiddle around with the values given the exact saving dimensions. In the example I used figure dimensions of 7x3.5 inches (which is similar to 18x9cm), and have added the black borders just to demonstrate the component limits.
library(tidyverse)
library(patchwork)
data <- midwest %>%
head(5) %>%
select(2,23:25) %>%
pivot_longer(cols=2:4,names_to="Variable", values_to="Percent") %>%
mutate(Variable=factor(Variable, levels=c("percbelowpoverty","percchildbelowpovert","percadultpoverty"),ordered=TRUE))
p1 <-
ggplot(data=data, mapping=aes(x=county, y=Percent, fill=Variable)) +
geom_col() +
scale_fill_manual(values = c("#CF232B","#942192","#000000"))
p_legend <- cowplot::get_legend(p1)
p_main <- p1 <-
ggplot(data=data, mapping=aes(x=county, y=Percent, fill=Variable)) +
geom_col(show.legend = FALSE) +
scale_fill_manual(values = c("#CF232B","#942192","#000000"))
p_main + plot_spacer() + p_legend +
plot_layout(widths = c(12.5, 1.5, 4)) &
theme(plot.margin = margin(),
plot.background = element_rect(colour = "black"))
Created on 2021-04-02 by the reprex package (v1.0.0)
update
My solution is only semi-satisfactory as pointed out by the OP. The problem is that one cannot (to my knowledge) define the position of the grob in the third panel.
Other ideas for workarounds:
One could determine the space needed for text (but this seems not so easy) and then to size the device accordingly
Create a fake legend - however, this requires the tiles / text to be aligned to the left with no margin, and this can very quickly become very hacky.
In short, I think teunbrand's solution is probably the most straight forward one.
Update 2
The problem with the left alignment should be fixed with Stefan's suggestion in this thread

Adjust spacing between subplots of panel figure (ggplot) in case of different axis.title/text properties

I would like to adjust the spacing between plots that are aligned in a panel using the cowplot package when some plots contain axis titles/labels, and others don't.
Example
Let's create three plots:
library(tidyverse)
library(cowplot)
set.seed(123)
df <- data.frame(x = rnorm(n = 100),
y = rnorm(n = 100))
plot <- ggplot(data = df, aes(x, y)) + geom_point()
plot_grid(plot, plot, plot, nrow = 1, align = "vh")
These plots are aligned perfectly! But often, I have a scenario in which I would like to create a 'cleaner' panel figure. One way to do this is to remove the titles/text of the y-axis of the second and third plots.
Like this:
plot2 <- plot + theme(axis.title.y = element_blank(),
axis.text.y = element_blank())
plot_grid(plot, plot2, plot2, nrow = 1, align = "vh")
Again, perfectly aligned, but the spacing between the first and the second plot (and the second and third plot) is quite large. I would like to reduce the spacing to create a more compact plot, while the axis remain exactly the same size.
Expected output
Is this possible with cowplot? Or is there another way to do this?
Referencing this post on github, plot_grid() doesn't add any space by default and uses the margins of your plot. To remove the space outside your plot area, you can use them(plot.margin=...) to remove.
With that being said... that's not what's going on here! Printing either plot or plot2 will yield a plot with no margins. It appears the issue is with the use of the align= argument in plot_grid(). I'm not sure why, but setting it to anything other than the default values (align="none") results in the extra whitespace around the plots. Very strange... needless to say, removing that argument fixes your problem:
Original code using align="vh"
plot_grid(plot, plot2, plot2, nrow = 1, align="vh")
Using align="none"
plot_grid(plot, plot2, plot2, nrow = 1, align="none")
Any further space would be added according to your graphics device, since the actual plot you get depends on the size and resolution of that device.
Here is a solution using the patchwork package
library(tidyverse)
set.seed(123)
df <- data.frame(x = rnorm(n = 100),
y = rnorm(n = 100))
plot1 <- ggplot(data = df, aes(x, y)) + geom_point()
plot2 <- plot1 + theme(axis.title.y = element_blank(),
axis.text.y = element_blank())
# install.packages("patchwork", dependencies = TRUE)
library(patchwork)
plot1 + plot2 + plot2 +
plot_layout(ncol = 3)
Created on 2020-07-24 by the reprex package (v0.3.0)

Line up columns of bar graph with points of line plot with ggplot

Is there any way to line up the points of a line plot with the bars of a bar graph using ggplot when they have the same x-axis? Here is the sample data I'm trying to do it with.
library(ggplot2)
library(gridExtra)
data=data.frame(x=rep(1:27, each=5), y = rep(1:5, times = 27))
yes <- ggplot(data, aes(x = x, y = y))
yes <- yes + geom_point() + geom_line()
other_data = data.frame(x = 1:27, y = 50:76 )
no <- ggplot(other_data, aes(x=x, y=y))
no <- no + geom_bar(stat = "identity")
grid.arrange(no, yes)
Here is the output:
The first point of the line plot is to the left of the first bar, and the last point of the line plot is to the right of the last bar.
Thank you for your time.
Extending #Stibu's post a little: To align the plots, use gtable (Or see answers to your earlier question)
library(ggplot2)
library(gtable)
data=data.frame(x=rep(1:27, each=5), y = rep(1:5, times = 27))
yes <- ggplot(data, aes(x = x, y = y))
yes <- yes + geom_point() + geom_line() +
scale_x_continuous(limits = c(0,28), expand = c(0,0))
other_data = data.frame(x = 1:27, y = 50:76 )
no <- ggplot(other_data, aes(x=x, y=y))
no <- no + geom_bar(stat = "identity") +
scale_x_continuous(limits = c(0,28), expand = c(0,0))
gYes = ggplotGrob(yes) # get the ggplot grobs
gNo = ggplotGrob(no)
plot(rbind(gNo, gYes, size = "first")) # Arrange and plot the grobs
Edit To change heights of plots:
g = rbind(gNo, gYes, size = "first") # Combine the plots
panels <- g$layout$t[grepl("panel", g$layout$name)] # Get the positions for plot panels
g$heights[panels] <- unit(c(0.7, 0.3), "null") # Replace heights with your relative heights
plot(g)
I can think of (at least) two ways to align the x-axes in the two plots:
The two axis do not align because in the bar plot, the geoms cover the x-axis from 0.5 to 27.5, while in the other plot, the data only ranges from 1 to 27. The reason is that the bars have a width and the points don't. You can force the axex to align by explicitly specifying an x-axis range. Using the definitions from your plot, this can be achieved by
yes <- yes + scale_x_continuous(limits=c(0,28))
no <- no + scale_x_continuous(limits=c(0,28))
grid.arrange(no, yes)
limits sets the range of the x-axis. Note, though, that the alginment is still not quite perfect. The y-axis labels take up a little more space in the upper plot, because the numbers have two digits. The plot looks as follows:
The other solution is a bit more complicated but it has the advantage that the x-axis is drawn only once and that ggplot makes sure that the alignment is perfect. It makes use of faceting and the trick described in this answer. First, the data must be combined into a single data frame by
all <- rbind(data.frame(other_data,type="other"),data.frame(data,type="data"))
and then the plot can be created as follows:
ggplot(all,aes(x=x,y=y)) + facet_grid(type~.,scales = "free_y") +
geom_bar(data=subset(all,type=="other"),stat="identity") +
geom_point(data=subset(all,type=="data")) +
geom_line(data=subset(all,type=="data"))
The trick is to let the facets be constructed by the variable type which was used before to label the two data sets. But then each geom only gets the subset of the data that should be drawn with that specific geom. In facet_grid, I also used scales = "free_y" because the two y-axes should be independent. This plot looks as follows:
You can change the labels of the facets by giving other names when you define the data frame all. If you want to remove them alltogether, then add the following to your plot:
+ theme(strip.background = element_blank(), strip.text = element_blank())

Resources