222

As the title says: How can I plot a legend outside the plotting area when using base graphics?

I thought about fiddling around with layout and produce an empty plot to only contain the legend, but I would be interested in a way using just the base graph facilities and e.g., par(mar = ) to get some space on the right of the plot for the legend.


Here an example:

plot(1:3, rnorm(3), pch = 1, lty = 1, type = "o", ylim=c(-2,2))
lines(1:3, rnorm(3), pch = 2, lty = 2, type="o")
legend(1,-1,c("group A", "group B"), pch = c(1,2), lty = c(1,2))

produces:

alt text

But as said, I would like the legend to be outside the plotting area (e.g., to the right of the graph/plot.

2
  • ...you can also hack par with dummy container for the legend, easy and quite convenient time-to-time. Similar question here.
    – hhh
    Commented Jan 5, 2012 at 5:35
  • 6
    @hhh The link is not working anymore. Can you update it or post an answer using this approach?
    – Henrik
    Commented May 10, 2012 at 8:57

11 Answers 11

178

No one has mentioned using negative inset values for legend. Here is an example, where the legend is to the right of the plot, aligned to the top (using keyword "topright").

# Random data to plot:
A <- data.frame(x=rnorm(100, 20, 2), y=rnorm(100, 20, 2))
B <- data.frame(x=rnorm(100, 21, 1), y=rnorm(100, 21, 1))

# Add extra space to right of plot area; change clipping to figure
par(mar=c(5.1, 4.1, 4.1, 8.1), xpd=TRUE)

# Plot both groups
plot(y ~ x, A, ylim=range(c(A$y, B$y)), xlim=range(c(A$x, B$x)), pch=1,
               main="Scatter plot of two groups")
points(y ~ x, B, pch=3)

# Add legend to top right, outside plot region
legend("topright", inset=c(-0.2,0), legend=c("A","B"), pch=c(1,3), title="Group")

The first value of inset=c(-0.2,0) might need adjusting based on the width of the legend.

legend_right

2
  • 17
    @Henrik no it does'nt work without xpd=TRUE. Also note that it is better to set xpd=TRUE as an argument of the legend() function. Commented Jul 26, 2012 at 11:27
  • 3
    Sometimes xpd must be set to TRUE for the negative inset to work. But sometimes not. With the command args.legend=list(x="bottom", horiz=TRUE, inset=-0.2) within a barplot(... it doesn't seemd to need xpd=TRUE but with just legend(x="bottom", horiz=TRUE, inset=-0.2) it does seem to need xpd=TRUE. Any insights? I am just confused in passing my arguments? Commented Jan 24, 2018 at 16:59
135

Maybe what you need is par(xpd=TRUE) to enable things to be drawn outside the plot region. So if you do the main plot with bty='L' you'll have some space on the right for a legend. Normally this would get clipped to the plot region, but do par(xpd=TRUE) and with a bit of adjustment you can get a legend as far right as it can go:

 set.seed(1) # just to get the same random numbers
 par(xpd=FALSE) # this is usually the default

 plot(1:3, rnorm(3), pch = 1, lty = 1, type = "o", ylim=c(-2,2), bty='L')
 # this legend gets clipped:
 legend(2.8,0,c("group A", "group B"), pch = c(1,2), lty = c(1,2))

 # so turn off clipping:
 par(xpd=TRUE)
 legend(2.8,-1,c("group A", "group B"), pch = c(1,2), lty = c(1,2))
5
  • 34
    Note that you can pass xpd directly to legend so you don't need to worry about resetting par afterwards. Also see grconvertX & Y for a way to specify the location of the legend in a way not dependent on the limits of the data you're plotting.
    – Charles
    Commented Oct 14, 2010 at 11:29
  • 7
    as this question and answer are still very popular, par(xpd=NA) is even more powerful (i.e., plots to more regions).
    – Henrik
    Commented Aug 13, 2015 at 7:46
  • +1. We should mention that it makes sense to have a separate par call right before the legend. In my plot, I used par(new=T) on several other occasions and simply wanted to add the the xpd param in the same call, which causes trouble. Commented Jul 25, 2016 at 9:11
  • 1
    Even in the second example the legend gets clipped slightly off the screen. Even if you try to resize the graph the legend box remains partially outside the screen. Is there a way to make it remain within the screen? Commented Apr 23, 2021 at 5:43
  • @numbermaniac I'm having the exact same issue. Did you figure out a solution?
    – felixm
    Commented Feb 8 at 2:51
39

Another solution, besides the ones already mentioned (using layout or par(xpd=TRUE)) is to overlay your plot with a transparent plot over the entire device and then add the legend to that.

The trick is to overlay a (empty) graph over the complete plotting area and adding the legend to that. We can use the par(fig=...) option. First we instruct R to create a new plot over the entire plotting device:

par(fig=c(0, 1, 0, 1), oma=c(0, 0, 0, 0), mar=c(0, 0, 0, 0), new=TRUE)

Setting oma and mar is needed since we want to have the interior of the plot cover the entire device. new=TRUE is needed to prevent R from starting a new device. We can then add the empty plot:

plot(0, 0, type='n', bty='n', xaxt='n', yaxt='n')

And we are ready to add the legend:

legend("bottomright", ...)

will add a legend to the bottom right of the device. Likewise, we can add the legend to the top or right margin. The only thing we need to ensure is that the margin of the original plot is large enough to accomodate the legend.

Putting all this into a function;

add_legend <- function(...) {
  opar <- par(fig=c(0, 1, 0, 1), oma=c(0, 0, 0, 0), 
    mar=c(0, 0, 0, 0), new=TRUE)
  on.exit(par(opar))
  plot(0, 0, type='n', bty='n', xaxt='n', yaxt='n')
  legend(...)
}

And an example. First create the plot making sure we have enough space at the bottom to add the legend:

par(mar = c(5, 4, 1.4, 0.2))
plot(rnorm(50), rnorm(50), col=c("steelblue", "indianred"), pch=20)

Then add the legend

add_legend("topright", legend=c("Foo", "Bar"), pch=20, 
   col=c("steelblue", "indianred"),
   horiz=TRUE, bty='n', cex=0.8)

Resulting in:

Example figure shown legend in top margin

3
  • 2
    Great addition to the list here. There's an explanation about how to make this work with multiple plots in the graphic here.
    – shiri
    Commented Jan 20, 2015 at 16:26
  • Jan, is there a way to increase the font size in the legend, without some text being clipped? For example, I have a graphic 4 different types of labels, but with a lot of empty space between them. Commented Mar 9, 2017 at 22:41
  • I've written a question with more details stackoverflow.com/questions/42707308/… Commented Mar 9, 2017 at 23:14
19

I like to do it like this:

par(oma=c(0, 0, 0, 5))
plot(1:3, rnorm(3), pch=1, lty=1, type="o", ylim=c(-2,2))
lines(1:3, rnorm(3), pch=2, lty=2, type="o")
legend(par('usr')[2], par('usr')[4], bty='n', xpd=NA,
       c("group A", "group B"), pch=c(1, 2), lty=c(1,2))

enter image description here

The only tweaking required is in setting the right margin to be wide enough to accommodate the legend.

However, this can also be automated:

dev.off() # to reset the graphics pars to defaults
par(mar=c(par('mar')[1:3], 0)) # optional, removes extraneous right inner margin space
plot.new()
l <- legend(0, 0, bty='n', c("group A", "group B"), 
            plot=FALSE, pch=c(1, 2), lty=c(1, 2))
# calculate right margin width in ndc
w <- grconvertX(l$rect$w, to='ndc') - grconvertX(0, to='ndc')
par(omd=c(0, 1-w, 0, 1))
plot(1:3, rnorm(3), pch=1, lty=1, type="o", ylim=c(-2, 2))
lines(1:3, rnorm(3), pch=2, lty=2, type="o")
legend(par('usr')[2], par('usr')[4], bty='n', xpd=NA,
       c("group A", "group B"), pch=c(1, 2), lty=c(1, 2))

enter image description here

2
  • Using xpd=T or xpd=NA doesn't prevent my 'main' (title) from being clipped when it is stretched to try to use the area added with the wide right margin.
    – Phil Goetz
    Commented Nov 29, 2016 at 3:54
  • @PhilGoetz are you sure you're plotting main within the plot area? Is it possible that you don't have enough lines of margin there to plot in?
    – jbaums
    Commented Nov 29, 2016 at 3:57
18

Sorry for resurrecting an old thread, but I was with the same problem today. The simplest way that I have found is the following:

# Expand right side of clipping rect to make room for the legend
par(xpd=T, mar=par()$mar+c(0,0,0,6))

# Plot graph normally
plot(1:3, rnorm(3), pch = 1, lty = 1, type = "o", ylim=c(-2,2))
lines(1:3, rnorm(3), pch = 2, lty = 2, type="o")

# Plot legend where you want
legend(3.2,1,c("group A", "group B"), pch = c(1,2), lty = c(1,2))

# Restore default clipping rect
par(mar=c(5, 4, 4, 2) + 0.1)

Found here: http://www.harding.edu/fmccown/R/

2
  • 4
    Even better is oldpar <- par(xpd=T, mar=par()$mar+c(0,0,0,6)) ... par(oldpar) (See the help of par)
    – rakensi
    Commented Oct 21, 2014 at 15:48
  • This solution is better because the space for legend is fixed not matter the length of strings of legend
    – Sergio
    Commented Jun 7, 2020 at 13:36
13

Adding another simple alternative that is quite elegant in my opinion.

Your plot:

plot(1:3, rnorm(3), pch = 1, lty = 1, type = "o", ylim=c(-2,2))
lines(1:3, rnorm(3), pch = 2, lty = 2, type="o")

Legend:

legend("bottomright", c("group A", "group B"), pch=c(1,2), lty=c(1,2),
       inset=c(0,1), xpd=TRUE, horiz=TRUE, bty="n"
       )

Result:

picture with legend

Here only the second line of the legend was added to your example. In turn:

  • inset=c(0,1) - moves the legend by fraction of plot region in (x,y) directions. In this case the legend is at "bottomright" position. It is moved by 0 plotting regions in x direction (so stays at "right") and by 1 plotting region in y direction (from bottom to top). And it so happens that it appears right above the plot.
  • xpd=TRUE - let's the legend appear outside of plotting region.
  • horiz=TRUE - instructs to produce a horizontal legend.
  • bty="n" - a style detail to get rid of legend bounding box.

Same applies when adding legend to the side:

par(mar=c(5,4,2,6))
plot(1:3, rnorm(3), pch = 1, lty = 1, type = "o", ylim=c(-2,2))
lines(1:3, rnorm(3), pch = 2, lty = 2, type="o")

legend("topleft", c("group A", "group B"), pch=c(1,2), lty=c(1,2),
       inset=c(1,0), xpd=TRUE, bty="n"
       )

Here we simply adjusted legend positions and added additional margin space to the right side of the plot. Result:

picture with legend 2

1
  • Tried this and it works. Much simpler.
    – CK7
    Commented Dec 8, 2021 at 20:08
10

I can offer only an example of the layout solution already pointed out.

layout(matrix(c(1,2), nrow = 1), widths = c(0.7, 0.3))
par(mar = c(5, 4, 4, 2) + 0.1)
plot(1:3, rnorm(3), pch = 1, lty = 1, type = "o", ylim=c(-2,2))
lines(1:3, rnorm(3), pch = 2, lty = 2, type="o")
par(mar = c(5, 0, 4, 2) + 0.1)
plot(1:3, rnorm(3), pch = 1, lty = 1, ylim=c(-2,2), type = "n", axes = FALSE, ann = FALSE)
legend(1, 1, c("group A", "group B"), pch = c(1,2), lty = c(1,2))

an ugly picture :S

10

Recently I found very easy and interesting function to print legend outside of the plot area where you want.

Make the outer margin at the right side of the plot.

par(xpd=T, mar=par()$mar+c(0,0,0,5))

Create a plot

plot(1:3, rnorm(3), pch = 1, lty = 1, type = "o", ylim=c(-2,2))
lines(1:3, rnorm(3), pch = 2, lty = 2, type="o")

Add legend and just use locator(1) function as like below. Then you have to just click where you want after load following script.

legend(locator(1),c("group A", "group B"), pch = c(1,2), lty = c(1,2))

Try it

0
4

You could do this with the Plotly R API, with either code, or from the GUI by dragging the legend where you want it.

Here is an example. The graph and code are also here.

x = c(0,1,2,3,4,5,6,7,8) 
y = c(0,3,6,4,5,2,3,5,4) 
x2 = c(0,1,2,3,4,5,6,7,8) 
y2 = c(0,4,7,8,3,6,3,3,4)

You can position the legend outside of the graph by assigning one of the x and y values to either 100 or -100.

legendstyle = list("x"=100, "y"=1)
layoutstyle = list(legend=legendstyle)

Here are the other options:

  • list("x" = 100, "y" = 0) for Outside Right Bottom
  • list("x" = 100, "y"= 1) Outside Right Top
  • list("x" = 100, "y" = .5) Outside Right Middle
  • list("x" = 0, "y" = -100) Under Left
  • list("x" = 0.5, "y" = -100) Under Center
  • list("x" = 1, "y" = -100) Under Right

Then the response.

response = p$plotly(x,y,x2,y2, kwargs=list(layout=layoutstyle));

Plotly returns a URL with your graph when you make a call. You can access that more quickly by calling browseURL(response$url) so it will open your graph in your browser for you.

url = response$url
filename = response$filename

That gives us this graph. You can also move the legend from within the GUI and then the graph will scale accordingly. Full disclosure: I'm on the Plotly team.

Legend on side of graph

3

Try layout() which I have used for this in the past by simply creating an empty plot below, properly scaled at around 1/4 or so and placing the legend parts manually in it.

There are some older questions here about legend() which should get you started.

1
  • As already said in the question, this is what I thought about, too. But it would be ideal, if there would be another way. Somehow I suppose there isn't.
    – Henrik
    Commented Oct 14, 2010 at 10:44
0

An old question, but I'd like to flag a new solution. The tinyplot package (homepage) is a lightweight extension of the base R graphics system that automatically supports group legends outside of the plot area. It is available on CRAN.

dat = data.frame(
  x   = rep(1:3, times = 2),
  y   = rnorm(6),
  grp = rep(c("A","B"), each = 3)
)

library(tinyplot)

plt(y ~ x | grp, dat, type = "o")

By default, grouping is displayed through colors. But you can adjust/add grouping aesthetics by invoking the "by" convenience argument.

plt(y ~ x | grp, dat, type = "o", pch = "by")

The default legend position is outside and to the right. But is easily moved using standard keywords. The trailing ! here causes the legend to be plotted outside of the plotting area.

plt(y ~ x | grp, dat, type = "o", pch = "by", legend = "bottom!")

You can can tweak the appearance of the legend by passing standard arguments via through a list, e.g. here adding a border.

plt(y ~ x | grp, dat, type = "o", pch = "by", legend = list("bottom!", bty = "o"))

etc.

tinyplot also supports continuous grouping variables through gradient legends, plus a bunch of other convenience features. But it the end result is always a base R plot and so it should be compatible with other base (i.e, "graphics") workflows and settings.

Not the answer you're looking for? Browse other questions tagged or ask your own question.