Ggplot Separate Legend and Plot

How to plot just the legends in ggplot2?

Here are 2 approaches:

Set Up Plot


library(ggplot2) 
library(grid)
library(gridExtra)

my_hist <- ggplot(diamonds, aes(clarity, fill = cut)) +
geom_bar()

Cowplot approach

# Using the cowplot package
legend <- cowplot::get_legend(my_hist)

grid.newpage()
grid.draw(legend)

Sample Image

Home grown approach

Shamelessly stolen from: Inserting a table under the legend in a ggplot2 histogram

## Function to extract legend
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]]
legend
}

legend <- g_legend(my_hist)

grid.newpage()
grid.draw(legend)

Created on 2018-05-31 by the reprex package (v0.2.0).

ggplot2 - separate legend for multiple geom_lines

Is this what you're trying to do?

library(tidyverse)

df1 <- data.frame(date=as.Date(seq(ISOdate(2019,1,1), by="1 day", length.out=365)),
value=runif(365))

df2 <- data.frame(date=as.Date(seq(ISOdate(2019,1,1), by="1 day", length.out=365)),
value=runif(365)+3)

df1$Lines <- factor("Line 1")
df2$Lines <- factor("Line 2")
df3 <- rbind(df1, df2)

ggplot(df3) +
geom_line(df3, mapping = aes(x = date, y = value, alpha = Lines)) +
geom_vline(aes(xintercept = as.Date("2019-06-15"), colour = "Milestone 1"), linetype = "dashed") +
geom_vline(aes(xintercept = as.Date("2019-07-20"), colour = "Milestone 2"), linetype = "dashed") +
geom_vline(aes(xintercept = as.Date("2019-09-15"), colour = "Milestone 3"), linetype = "dashed") +
scale_color_manual(name="Milestones",
breaks=c("Milestone 1","Milestone 2","Milestone 3"),
values = c("Milestone 1" = "red",
"Milestone 2" = "blue",
"Milestone 3" = "green"))

example.png

ggplot separate legend and plot

You can get the legend from the grob object of the ggplot. Then you could use the grid.arrange function to position everything.

library(gridExtra)
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]]
legend
}

legend <- g_legend(plot1)

grid.arrange(legend, plot1+ theme(legend.position = 'none'),
ncol=2, nrow=1, widths=c(1/6,5/6))

There are lots of examples on the web using the g_legend function.

HTH

How to place legends at different sides of plot (bottom and right side) with ggplot2?

Making use of cowplot you could do:

  1. Extract the color guide from a plot without a linetype guide and legend.position = "right" using cowplot::get_legend
  2. Making use of cowplot::plot_grid make a grid with two columns where the first column contains the plot without the color guide and the linetype guide placed at the bottom, while the color guide is put in the second column.
library(tidyverse)

df <- tibble(names = mtcars %>%
rownames(),
mtcars)

p1 <- df %>%
filter(names == "Duster 360" | names == "Valiant") %>%
ggplot(aes(x = as.factor(cyl), y = mpg, color = names)) +
geom_point() +
geom_hline(aes(yintercept = 20, linetype = "a"))

library(cowplot)

guide_color <- get_legend(p1 + guides(linetype = "none"))

plot_grid(p1 +
guides(color = "none") +
theme(legend.position = "bottom"),
guide_color,
ncol = 2, rel_widths = c(.85, .15))

Sample Image

Different legend positions on plot with multiple legends

I do not think this is possible with ggplot2-only functions. However, a common trick is:

  1. to make a plot without the legend,
  2. make other plots with target legends,
  3. extract the legends from these plots,
  4. arrange everything in a grid using packages like cowplot or gridExtra

You can find some examples of this process on SO:

  • ggplot - Multiple legends arrangement
  • How to place legends at different sides of plot (bottom and right side) with ggplot2?
  • How do I position two legends independently in ggplot

Here is an example with the provided data, I have not put much effort in arranging the grid because it can change a lot depending on the package you choose in the end. It is just to showcase the process.

library(cowplot) 
library(ggplot2)

# plot without legend
main_plot <- ggplot(data = df) +
geom_point(aes(x = factor(season_num), y = rating, size = count, color = doctor)) +
labs(x = "Season", y = "Rating (1-10)", title = "IMDb ratings distributions by Season") +
theme(legend.position = 'none',
legend.title = element_blank(),
plot.title = element_text(size = 10),
axis.title.x = element_text(size = 10),
axis.title.y = element_text(size = 10)) +
scale_size_continuous(range = c(1,8)) +
scale_y_continuous(limits=c(1, 10), breaks=c(seq(1, 10, by = 1))) +
scale_x_discrete(breaks=c(seq(27, 38, by = 1))) +
scale_color_brewer(palette = "Dark2")

# color legend, top, horizontally
color_plot <- ggplot(data = df) +
geom_point(aes(x = factor(season_num), y = rating, color = doctor)) +
theme(legend.position = 'top',
legend.title = element_blank()) +
scale_color_brewer(palette = "Dark2")

color_legend <- cowplot::get_legend(color_plot)

# size legend, right-hand side, vertically
size_plot <- ggplot(data = df) +
geom_point(aes(x = factor(season_num), y = rating, size = count)) +
theme(legend.position = 'right',
legend.title = element_blank()) +
scale_size_continuous(range = c(1,8))

size_legend <- cowplot::get_legend(size_plot)

# combine all these elements
cowplot::plot_grid(plotlist = list(color_legend,NULL, main_plot, size_legend),
rel_heights = c(1, 5),
rel_widths = c(4, 1))

Output:

output

R ggplot2 how to separate legend elements

I was working from the original post, where no points had associated + signs with them. Assuming these are drawn by a geom_point layer with shape = 3, taken from a subset of the data frame dta we can do:

ggplot(dta, aes(TEST1, TEST, fill = Interaction)) +
annotation_custom(grob = g3, xmin = -Inf, xmax = Inf, ymin = -Inf, ymax = Inf) +
geom_abline(intercept = 0, slope = 1, color="black", size = 1.0) +
geom_abline(intercept = c(-0.05, 0.05), slope = 1,
color = "black", linetype = "dotted", size = 0.25, alpha = 0.5) +
geom_abline(intercept = c(-0.1, 0.1), slope = 1, color = "black",
linetype = "dashed", size = 0.25, alpha = 0.5) +
geom_point(shape = 21, size = 4.5, stroke = 0.25, alpha = 0.9) +
geom_point(aes(TEST1, TEST, shape = "Positive"), inherit.aes = FALSE,
size = 1.5, data = dta[sample(nrow(dta), 12),]) +
scale_shape_manual(values = 3, name = "Special") +
xlim(0.4, 1.0) +
ylim(0.4, 1.0) +
theme(legend.position="top",
legend.key.height= unit(.4, 'cm'),
plot.background = element_blank(),
panel.background = element_rect(fill = "transparent", colour = "gray"),
panel.border = element_rect(fill = "transparent", colour = "black"),
axis.text = element_text(color = "black"),
legend.background = element_blank(),
legend.box.background = element_blank(),
legend.key = element_blank(),
legend.text = element_text(size = 9),
axis.text.x = element_text(size = 12),
axis.text.y = element_text(size = 12),
axis.title.x = element_text(color = "black",size = 13, face = "bold"),
axis.title.y = element_text(color = "black", size=15, face = "bold")
) +
xlab("TEST1") +
ylab("TEST") +
coord_fixed() +
guides(fill = guide_legend(ncol = 2,
title = "Interactions",
title.position = "top",
title.theme =
element_text(
size = 15,
face = "bold",
colour = "black",
margin = margin(t = 0, r = 0, b = 0, l = 5,
unit = "pt")
)
),
shape = guide_legend(title = "Special",
title.position = "top",
title.theme = element_text(
size = 15,
face = "bold",
colour = "black",
margin = margin(t = 0, r = 0, b = 0, l = 5,
unit = "pt")
)
))

Sample Image

Note I also had to try to reconstruct the g3 object from an educated guess.

R plotly separate functional legends

Plotly does not seem to easily support this, since different guides are linked to multiple traces. So deselecting e.g. "old" on an "Age" trace will not remove anything from the separate set of points from the "Gender" trace.

This is a workaround using crosstalk and a SharedData data object. Instead of (de)selecting plotly traces, this uses filters on the dataset that is used by plotly. It technically achieves the selection behaviour that is requested, but whether or not it is a working solution depends on the final application. There are likely ways to adjust the styling and layout to make it more plotly-ish, if the mechanism works for you.

library(crosstalk)

#SharedData object used for filters and plot
shared <- SharedData$new(X)

crosstalk::bscols(
widths = c(2, 10),
list(
crosstalk::filter_checkbox("Age",
label = "Age",
sharedData = shared,
group = ~age),
crosstalk::filter_checkbox("Gender",
label = "Gender",
sharedData = shared,
group = ~gender)
),
plot_ly(data = shared, x = ~xcoord, y = ~ycoord,
type = "scatter", mode = "markers",
marker = list(color = ~score,
colorbar = list(len = .5, y = .3),
cmin = 0, cmax = 6)) %>%
layout(
xaxis = list(range=c(.5,6.5)),
yaxis = list(range=c(.5,6.5))
)
)

Sample Image

Edit: initialize all checkboxes as "checked"

I only managed to do this by modifying the output HTML tags. This produces the same plot, but has all boxes checked at the beginning.

out <- crosstalk::bscols(...) #previous output object

library(htmltools)
out_tags <- htmltools::renderTags(out)

#check all Age and Gender checkboxes
out_tags$html <- stringr::str_replace_all(
out_tags$html,
'(<input type="checkbox" name="(Age|Gender)" value=".*")/>',
'\\1 checked="checked"/>'
)
out_tags$html <- HTML(out_tags$html)
# view in RStudio Viewer
browsable(as.tags(out_tags))
#or from Rmd chunk
as.tags(out_tags)


Related Topics



Leave a reply



Submit