Draw line between multiple plots in R [duplicate] - r

Take a very simple example, mfrow=c(1,3); each figure is a different histogram; how would I draw a horizontal line (akin to abline(h=10)) that went across all 3 figures? (That is, even the margins between them.) Obviously, I could add an abline to each figure, but that's not what I want. I can think of a very complicated way to do this by really only having 1 figure, and drawing each 'figure' within it using polygon etc. That would be ridiculous. Isn't there an easy way to do this?

As #joran noted, the grid graphical system offers more flexible control over arrangement of multiple plots on a single device.
Here, I first use grconvertY() to query the location of a height of 50 on the y-axis in units of "normalized device coordinates". (i.e. as a proportion of the total height of the plotting device, with 0=bottom, and 1=top). I then use grid functions to: (1) push a viewport that fills the device; and (2) plot a line at the height returned by grconvertY().
## Create three example plots
par(mfrow=c(1,3))
barplot(VADeaths, border = "dark blue")
barplot(VADeaths, border = "yellow")
barplot(VADeaths, border = "green")
## From third plot, get the "normalized device coordinates" of
## a point at a height of 50 on the y-axis.
(Y <- grconvertY(50, "user", "ndc"))
# [1] 0.314248
## Add the horizontal line using grid
library(grid)
pushViewport(viewport())
grid.lines(x = c(0,1), y = Y, gp = gpar(col = "red"))
popViewport()
EDIT: #joran asked how to plot a line that extends from the y-axis of the 1st plot to the edge of the last bar in the 3rd plot. Here are a couple of alternatives:
library(grid)
library(gridBase)
par(mfrow=c(1,3))
# barplot #1
barplot(VADeaths, border = "dark blue")
X1 <- grconvertX(0, "user", "ndc")
# barplot #2
barplot(VADeaths, border = "yellow")
# barplot #3
m <- barplot(VADeaths, border = "green")
X2 <- grconvertX(tail(m, 1) + 0.5, "user", "ndc") # default width of bars = 1
Y <- grconvertY(50, "user", "ndc")
## Horizontal line
pushViewport(viewport())
grid.lines(x = c(X1, X2), y = Y, gp = gpar(col = "red"))
popViewport()
Finally, here's an almost equivalent, and more generally useful approach. It employs the functions grid.move.to() and grid.line.to() demo'd by Paul Murrell in the article linked to in #mdsumner's answer:
library(grid)
library(gridBase)
par(mfrow=c(1,3))
barplot(VADeaths); vps1 <- do.call(vpStack, baseViewports())
barplot(VADeaths)
barplot(VADeaths); vps3 <- do.call(vpStack, baseViewports())
pushViewport(vps1)
Y <- convertY(unit(50,"native"), "npc")
popViewport(3)
grid.move.to(x = unit(0, "npc"), y = Y, vp = vps1)
grid.line.to(x = unit(1, "npc"), y = Y, vp = vps3,
gp = gpar(col = "red"))

This is the best I can do without thinking about it harder:
par(mfrow = c(1,3),xpd = NA)
for (i in 1:3){
x <- rnorm(200,i)
hist(x)
if (i == 1) segments(par("usr")[1],10,30,10)
}
I'm not sure how to make sure the line ends at the right spot without tinkering. Plotting a segment in each region would solve that, but introduce the issue of having the heights line up properly. But this might be a good starting point, at least.
I'd guess this is easier in grid graphics, but I'd have to do some research to verify.

This article by Paul Murrell shows the use of grid graphics to draw lines between two different coordinate systems, in this case lines that have end points specified in the native space of two separate sub-plots:
Paul Murrell. The grid graphics package. R News, 2(2):14-19, June 2002
It's on page 17 of the PDF article:
http://cran.r-project.org/doc/Rnews/Rnews_2002-2.pdf

Related

Is it possible to draw the axis line first, before the data?

This is a follow up to my previous question where I was looking for a solution to get the axis drawn first, then the data. The answer works for that specific question and example, but it opened a more general question how to change the plotting order of the underlying grobs. First the axis, then the data.
Very much in the way that the panel grid grob can be drawn on top or not.
Panel grid and axis grobs are apparently generated differently - axes more as guide objects rather than "simple" grobs. (Axes are drawn with ggplot2:::draw_axis(), whereas the panel grid is built as part of the ggplot2:::Layout object).
I guess this is why axes are drawn on top, and I wondered if the drawing order can be changed.
# An example to play with
library(ggplot2)
df <- data.frame(var = "", val = 0)
ggplot(df) +
geom_point(aes(val, var), color = "red", size = 10) +
scale_x_continuous(
expand = c(0, 0),
limits = c(0,1)
) +
coord_cartesian(clip = "off") +
theme_classic()
A ggplot can be represented by its gtable. The position of the grobs are given by the layout element, and "the z-column is used to define the drawing order of the grobs".
The z value for the panel, which contains the points grob, can then be increased so that it is drawn last.
So if p is your plot then
g <- ggplotGrob(p) ;
g$layout[g$layout$name == "panel", "z"] <- max(g$layout$z) + 1L
grid::grid.draw(g)
However, as noted in the comment this changes how the axis look, which perhaps, is due to the panel being drawn over some of the axis.
But in new exciting news from dww
if we add theme(panel.background = element_rect(fill = NA)) to the plot, the axes are no longer partially obscured. This both proves that this is the cause of the thinner axis lines, and also provides a reasonable workaround, provided you don't need a colored panel background.
Since you are looking for a more "on the draw level" solution, then the place to start is to ask "how is the ggplot drawn in the first place?". The answer can be found in the print method for ggplot objects:
ggplot2:::print.ggplot
#> function (x, newpage = is.null(vp), vp = NULL, ...)
#> {
#> set_last_plot(x)
#> if (newpage)
#> grid.newpage()
#> grDevices::recordGraphics(requireNamespace("ggplot2",
#> quietly = TRUE), list(), getNamespace("ggplot2"))
#> data <- ggplot_build(x)
#> gtable <- ggplot_gtable(data)
#> if (is.null(vp)) {
#> grid.draw(gtable)
#> }
#> else {
#> if (is.character(vp))
#> seekViewport(vp)
#> else pushViewport(vp)
#> grid.draw(gtable)
#> upViewport()
#> }
#> invisible(x)
#> }
where you can see that a ggplot is actually drawn by calling ggplot_build on the ggplot object, then ggplot_gtable on the output of ggplot_build.
The difficulty is that the panel, with its background, gridlines and data is created as a distinct grob tree. This is then nested as a single entity inside the final grob table produced by ggplot_build. The axis lines are drawn "on top" of that panel. If you draw these lines first, part of their thickness will be over-drawn with the panel. As mentioned in user20650's answer, this is not a problem if you don't need your plot to have a background color.
To my knowledge, there is no native way to include the axis lines as part of the panel unless you add them yourself as grobs.
The following little suite of functions allows you to take a plot object, remove the axis lines from it and add axis lines into the panel:
get_axis_grobs <- function(p_table)
{
axes <- grep("axis", p_table$layout$name)
axes[sapply(p_table$grobs[axes], function(x) class(x)[1] == "absoluteGrob")]
}
remove_lines_from_axis <- function(axis_grob)
{
axis_grob$children[[grep("polyline", names(axis_grob$children))]] <- zeroGrob()
axis_grob
}
remove_all_axis_lines <- function(p_table)
{
axes <- get_axis_grobs(p_table)
for(i in axes) p_table$grobs[[i]] <- remove_lines_from_axis(p_table$grobs[[i]])
p_table
}
get_panel_grob <- function(p_table)
{
p_table$grobs[[grep("panel", p_table$layout$name)]]
}
add_axis_lines_to_panel <- function(panel)
{
old_order <- panel$childrenOrder
panel <- grid::addGrob(panel, grid::linesGrob(x = unit(c(0, 0), "npc")))
panel <- grid::addGrob(panel, grid::linesGrob(y = unit(c(0, 0), "npc")))
panel$childrenOrder <- c(old_order[1],
setdiff(panel$childrenOrder, old_order),
old_order[2:length(old_order)])
panel
}
These can all be co-ordinated into a single function now to make the whole process much easier:
underplot_axes <- function(p)
{
p_built <- ggplot_build(p)
p_table <- ggplot_gtable(p_built)
p_table <- remove_all_axis_lines(p_table)
p_table$grobs[[grep("panel", p_table$layout$name)]] <-
add_axis_lines_to_panel(get_panel_grob(p_table))
grid::grid.newpage()
grid::grid.draw(p_table)
invisible(p_table)
}
And now you can just call underplot_axes on a ggplot object. I have modified your example a little to create a gray background panel, so that we can see more clearly what's going on:
library(ggplot2)
df <- data.frame(var = "", val = 0)
p <- ggplot(df) +
geom_point(aes(val, var), color = "red", size = 10) +
scale_x_continuous(
expand = c(0, 0),
limits = c(0,1)
) +
coord_cartesian(clip = "off") +
theme_classic() +
theme(panel.background = element_rect(fill = "gray90"))
p
underplot_axes(p)
Created on 2021-05-07 by the reprex package (v0.3.0)
Now, you may consider this "creating fake axes", but I would consider it more as "moving" the axis lines from one place in the grob tree to another. It's a shame that the option doesn't seem to be built into ggplot, but I can also see that it would take a pretty major overhaul of how a ggplot is constructed to allow that option.
Here's a hack that doesn't require going "under the hood", but rather uses patchwork to add another layer on top that is just the geom layer.
a <- [your plot above]
library(patchwork)
a + inset_element(a + them_void(), left = 0, bottom = 0, right = 1, top = 1)

Left-aligning ggplot when saved while using a fixed aspect ratio

I'm building a custom ggplot theme to standardize the look & feel of graphs I produce. The goal is more complex than this minimal example, so I'm looking for a general solution. I have a few key goals:
I want all graphs to export at the same size (3000 pixels wide, 1500 pixels high).
I want to control the aspect ratio of the plot panel itself.
I want to use textGrobs to include figure numbers.
I want the image to be left-aligned
The challenge I'm facing is that when combining these two constraints, the image that gets saved centers the ggplot graph within the window, which makes sense as a default, but looks bad in this case.
I'm hoping there's a general solution to left-align the ggplot panel when I export. Ideally, this will also work similarly for faceted graphs.
It seems that something should be possible using one of or some combination of the gridExtra, gtable, cowplot, and egg packages, but after experimenting for a few hours I'm at a bit of a loss. Does anybody know how I can accomplish this? My code is included below.
This is the image that gets produced. As you can see, the caption is left-aligned at the bottom, but the ggplot itself is horizontally centered. I want the ggplot graph left-aligned as well.
Graph output: https://i.stack.imgur.com/5EM2c.png
library(ggplot2)
# Generate dummy data
x <- paste0("var", seq(1,10))
y <- LETTERS[1:10]
data <- expand.grid(X=x, Y=y)
data$Z <- runif(100, -2, 2)
# Generate heatmap with fixed aspect ratio
p1 <- ggplot(data, aes(X, Y, fill= Z)) +
geom_tile() +
labs(title = 'A Heatmap Graph') +
theme(aspect.ratio = 1)
# A text grob for the footer
figure_number_grob <- grid::textGrob('Figure 10',
x = 0.004,
hjust = 0,
gp = grid::gpar(fontsize = 10,
col = '#01A184'))
plot_grid <- ggpubr::ggarrange(p1,
figure_number_grob,
ncol = 1,
nrow = 2,
heights = c(1,
0.05))
# save it
png(filename = '~/test.png', width = 3000, height = 1500, res = 300, type = 'cairo')
print(plot_grid)
dev.off()
I was able to find a solution to this that works for my needs, though it does feel a bit hacky.
Here's the core idea:
Generate the plot without a fixed aspect ratio.
Split the legend from the plot as its own component
Use GridExtra's arrangeGrob to combine the plot, a spacer, the legend, and another spacer horizontally
Set the width of the plot to some fraction of npc (normal parent coordinates), in this case 0.5. This means that the plot will take up 50% of the horizontal space of the output file.
Note that this is not exactly the same as setting a fixed aspect ratio for the plot. If you know the size of the output file, it's close to the same thing, but the size of axis text & axis titles will affect the output aspect ratio for the panel itself, so while it gets you close, it's not ideal if you need a truly fixed aspect ratio
Set the width of the spacers to the remaining portion of the npc (in this case, 0.5 again), minus the width of the legend to horizontally center the legend in the remaining space.
Here's my code:
library(ggplot2)
# Generate dummy data
x <- paste0("var", seq(1,10))
y <- LETTERS[1:10]
data <- expand.grid(X=x, Y=y)
data$Z <- runif(100, -2, 2)
# Generate heatmap WITHOUT fixed aspect ratio. I address this below
p1 <- ggplot(data, aes(X, Y, fill= Z)) +
geom_tile() +
labs(title = 'A Heatmap Graph')
# Extract the legend from our plot
legend = gtable::gtable_filter(ggplotGrob(p1), "guide-box")
plot_output <- gridExtra::arrangeGrob(
p1 + theme(legend.position="none"), # Remove legend from base plot
grid::rectGrob(gp=grid::gpar(col=NA)), # Add a spacer
legend, # Add the legend back
grid::rectGrob(gp=grid::gpar(col=NA)), # Add a spacer
nrow=1, # Format plots in 1 row
widths=grid::unit.c(unit(0.5, "npc"), # Plot takes up half of width
(unit(0.5, "npc") - legend$width) * 0.5, # Spacer width
legend$width, # Legend width
(unit(0.5, "npc") - legend$width) * 0.5)) # Spacer width
# A text grob for the footer
figure_number_grob <- grid::textGrob('Figure 10',
x = 0.004,
hjust = 0,
gp = grid::gpar(fontsize = 10,
col = '#01A184'))
plot_grid <- ggpubr::ggarrange(plot_output,
figure_number_grob,
ncol = 1,
nrow = 2,
heights = c(1,
0.05))
# save it
png(filename = '~/test.png', width = 3000, height = 1500, res = 300, type = 'cairo')
print(plot_grid)
dev.off()
And here's the output image: https://i.stack.imgur.com/rgzFy.png

R Two graphs with lines going from one to the other

I want to plot some point in a normal graph and link those points to a map displayed under it. What I would like to have basically is that (here I added manually the links):
Somehow I should use segments with pdt=T to write outside the margins, but I am not sure what mathematical transformation I need to do in order to set the right coordinates for the segment extremity that go into the map.
And I would prefere to use the traditional plot function and not ggplot2
Here the source used to draw the exemple (warning it may take time to load the open street map):
library(OpenStreetMap)
#Random point to plot in the graph
fdata=cbind.data.frame(runif(12),runif(12),c(rep("A",4),rep("B",4),rep("C",4)))
colnames(fdata)=c("x","y","city")
#random coordinate to plot in the map
cities=cbind.data.frame(runif(3,4.8,5),runif(3,50.95,51),c("A","B","C"))
colnames(cities)=c("long","lat","name")
#city to color correspondance
color=1:length(cities$name)
names(color)=cities$name
maxlat=max(cities$lat)
maxlong=max(cities$long)
minlat=min(cities$lat)
minlong=min(cities$long)
#get some open street map
map = openmap(c(lat=maxlat+0.02,long=minlong-0.04 ) ,
c(lat=minlat-0.02,long=maxlong+.04) ,
minNumTiles=9,type="osm")
longlat=openproj(map) #Change coordinate projection
par(mfrow=c(2,1),mar=c(0,5,4,6))
plot( fdata$y ~ fdata$x ,xaxt="n",ylab="Comp.2",xlab="",col=color[fdata$city],pch=20)
axis(3)
mtext(side=3,"-Comp.1",line=3)
par(mar=rep(1,4))
#plot the map
plot(longlat,removeMargin=F)
points(cities$lat ~ cities$long, col= color[cities$name],cex=1,pch=20)
text(cities$long,cities$lat-0.005,labels=cities$name)
The grid graphical system (which underlies both the lattice and ggplot2 graphics packages) is much better suited to this sort of operation than is R's base graphical system. Unfortunately, both of your plots use the base graphical system. Fortunately, though, the superb gridBase package supplies functions that allow one to translate between the two systems.
In the following (which starts with your call to par(mfrow=c(2,1),...)), I've marked the lines I added with comments indicating that they are My addition. For another, somewhat simpler example of this strategy in action, see here.
library(grid) ## <-- My addition
library(gridBase) ## <-- My addition
par(mfrow=c(2,1),mar=c(0,5,4,6))
plot(fdata$y ~ fdata$x, xaxt = "n", ylab = "Comp.2", xlab = "",
col = color[fdata$city],pch=20)
vps1 <- do.call(vpStack, baseViewports()) ## <-- My addition
axis(3)
mtext(side = 3,"-Comp.1",line=3)
par(mar = rep(1,4))
#plot the map
plot(longlat,removeMargin=F)
vps2 <- do.call(vpStack, baseViewports()) ## <-- My addition
points(cities$lat ~ cities$long, col= color[cities$name],cex=1,pch=20)
text(cities$long,cities$lat-0.005,labels=cities$name)
## My addition from here on out...
## A function that draws a line segment between two points (each a
## length two vector of x-y coordinates), the first point in the top
## plot and the second in the bottom plot.
drawBetween <- function(ptA, ptB, gp = gpar()) {
## Find coordinates of ptA in "Normalized Parent Coordinates"
pushViewport(vps1)
X1 <- convertX(unit(ptA[1],"native"), "npc")
Y1 <- convertY(unit(ptA[2],"native"), "npc")
popViewport(3)
## Find coordinates of ptB in "Normalized Parent Coordinates"
pushViewport(vps2)
X2 <- convertX(unit(ptB[1],"native"), "npc")
Y2 <- convertY(unit(ptB[2],"native"), "npc")
popViewport(3)
## Plot line between the two points
grid.move.to(x = X1, y = Y1, vp = vps1)
grid.line.to(x = X2, y = Y2, vp = vps2, gp = gp)
}
## Try the function out on one pair of points
ptA <- fdata[1, c("x", "y")]
ptB <- cities[1, c("long", "lat")]
drawBetween(ptA, ptB, gp = gpar(col = "gold"))
## Using a loop, draw lines from each point in `fdata` to its
## corresponding city in `cities`
for(i in seq_len(nrow(fdata))) {
ptA <- fdata[i, c("x", "y")]
ptB <- cities[match(fdata[i,"city"], cities$name), c("long", "lat")]
drawBetween(ptA, ptB, gp = gpar(col = color[fdata[i,"city"]]))
}
You can create a new plot area over your plots and then add the lines:
#New plot area
par(new=T, mfrow = c(1,1))
plot(0:1, type = "n", xaxt='n', ann=FALSE, axes=FALSE, frame.plot=TRUE, bty="n")
The problem of this is that you need do the mapping between yours plot and the new plot area, if you ever use the same area you can get some references (see locator) and then interpolate all the other point.
For example, in mi plot B is {1.751671, 0.1046729} and 8th point is {1.320507, 0.6892523}:
points(c(1.320507, 1.751671), c(0.6892523, 0.1046729), col = "red", type = "l")
UPDATE (Plots mapping):
X11(7, 7)
par(mfrow=c(2,1),mar=c(0,5,4,6))
plot( fdata$y ~ fdata$x ,xaxt="n",ylab="Comp.2",xlab="",col=color[fdata$city],pch=20)
axis(3)
mtext(side=3,"-Comp.1",line=3)
usr1 <- par("usr")
#plot the map
par(mar=rep(1,4))
plot(longlat,removeMargin=F)
points(cities$lat ~ cities$long, col= color[cities$name],cex=1,pch=20)
text(cities$long,cities$lat-0.005,labels=cities$name)
usr2 <- par("usr")
par(new=T, mfrow = c(1,1))
plot(0:1, type = "n", xaxt='n', ann=FALSE, axes=FALSE, frame.plot=TRUE, bty="n")
# Position of the corners (0, 0) and (1, 1) of the two graphs in the window X11(7, 7)
#ref <- locator()
ref <- list(x = c(1.09261365729382, 1.8750001444129, 1.06363637999312, 1.93636379046146),
y = c(0.501704460496285, 0.941477257177598,
-0.0335228967050026, 0.45909081740701))
fdata$x_map <- approxfun(usr1[1:2], ref$x[1:2])(fdata$x)
fdata$y_map <- approxfun(usr1[3:4], ref$y[1:2])(fdata$y)
points(fdata$y_map ~ fdata$x_map ,pch=6)
Keep in mind that the interpolation of the map must consider the projection, the linear projection can only be used with UTM coordinates.

How do you draw a line across a multiple-figure environment in R?

Take a very simple example, mfrow=c(1,3); each figure is a different histogram; how would I draw a horizontal line (akin to abline(h=10)) that went across all 3 figures? (That is, even the margins between them.) Obviously, I could add an abline to each figure, but that's not what I want. I can think of a very complicated way to do this by really only having 1 figure, and drawing each 'figure' within it using polygon etc. That would be ridiculous. Isn't there an easy way to do this?
As #joran noted, the grid graphical system offers more flexible control over arrangement of multiple plots on a single device.
Here, I first use grconvertY() to query the location of a height of 50 on the y-axis in units of "normalized device coordinates". (i.e. as a proportion of the total height of the plotting device, with 0=bottom, and 1=top). I then use grid functions to: (1) push a viewport that fills the device; and (2) plot a line at the height returned by grconvertY().
## Create three example plots
par(mfrow=c(1,3))
barplot(VADeaths, border = "dark blue")
barplot(VADeaths, border = "yellow")
barplot(VADeaths, border = "green")
## From third plot, get the "normalized device coordinates" of
## a point at a height of 50 on the y-axis.
(Y <- grconvertY(50, "user", "ndc"))
# [1] 0.314248
## Add the horizontal line using grid
library(grid)
pushViewport(viewport())
grid.lines(x = c(0,1), y = Y, gp = gpar(col = "red"))
popViewport()
EDIT: #joran asked how to plot a line that extends from the y-axis of the 1st plot to the edge of the last bar in the 3rd plot. Here are a couple of alternatives:
library(grid)
library(gridBase)
par(mfrow=c(1,3))
# barplot #1
barplot(VADeaths, border = "dark blue")
X1 <- grconvertX(0, "user", "ndc")
# barplot #2
barplot(VADeaths, border = "yellow")
# barplot #3
m <- barplot(VADeaths, border = "green")
X2 <- grconvertX(tail(m, 1) + 0.5, "user", "ndc") # default width of bars = 1
Y <- grconvertY(50, "user", "ndc")
## Horizontal line
pushViewport(viewport())
grid.lines(x = c(X1, X2), y = Y, gp = gpar(col = "red"))
popViewport()
Finally, here's an almost equivalent, and more generally useful approach. It employs the functions grid.move.to() and grid.line.to() demo'd by Paul Murrell in the article linked to in #mdsumner's answer:
library(grid)
library(gridBase)
par(mfrow=c(1,3))
barplot(VADeaths); vps1 <- do.call(vpStack, baseViewports())
barplot(VADeaths)
barplot(VADeaths); vps3 <- do.call(vpStack, baseViewports())
pushViewport(vps1)
Y <- convertY(unit(50,"native"), "npc")
popViewport(3)
grid.move.to(x = unit(0, "npc"), y = Y, vp = vps1)
grid.line.to(x = unit(1, "npc"), y = Y, vp = vps3,
gp = gpar(col = "red"))
This is the best I can do without thinking about it harder:
par(mfrow = c(1,3),xpd = NA)
for (i in 1:3){
x <- rnorm(200,i)
hist(x)
if (i == 1) segments(par("usr")[1],10,30,10)
}
I'm not sure how to make sure the line ends at the right spot without tinkering. Plotting a segment in each region would solve that, but introduce the issue of having the heights line up properly. But this might be a good starting point, at least.
I'd guess this is easier in grid graphics, but I'd have to do some research to verify.
This article by Paul Murrell shows the use of grid graphics to draw lines between two different coordinate systems, in this case lines that have end points specified in the native space of two separate sub-plots:
Paul Murrell. The grid graphics package. R News, 2(2):14-19, June 2002
It's on page 17 of the PDF article:
http://cran.r-project.org/doc/Rnews/Rnews_2002-2.pdf

Get plot() bounding box values

I'm generating numerous plots with xlim and ylim values that I'm calculating on a per-plot basis. I want to put my legend outside the plot area (just above the box around the actual plot), but I can't figure out how to get the maximum y-value of the box around my plot area.
Is there a method for even doing this? I can move the legend where I want it by manually changing the legend() x and y values, but this takes a LONG time for the amount of graphs I'm creating.
Thanks!
-JM
Here's a basic example illustrating what I think you're looking for using one of the code examples from ?legend.
#Construct some data and start the plot
x <- 0:64/64
y <- sin(3*pi*x)
plot(x, y, type="l", col="blue")
points(x, y, pch=21, bg="white")
#Grab the plotting region dimensions
rng <- par("usr")
#Call your legend with plot = FALSE to get its dimensions
lg <- legend(rng[1],rng[2], "sin(c x)", pch=21,
pt.bg="white", lty=1, col = "blue",plot = FALSE)
#Once you have the dimensions in lg, use them to adjust
# the legend position
#Note the use of xpd = NA to allow plotting outside plotting region
legend(rng[1],rng[4] + lg$rect$h, "sin(c x)", pch=21,
pt.bg="white", lty=1, col = "blue",plot = TRUE, xpd = NA)
The command par('usr') will return the coordinates of the bounding box, but you can also use the grconvertX and grconvertY functions. A simple example:
plot(1:10)
par(xpd=NA)
legend(par('usr')[1], par('usr')[4], yjust=0, legend='anything', pch=1)
legend( grconvertX(1, from='npc'), grconvertY(1, from='npc'), yjust=0,
xjust=1, legend='something', lty=1)
The oma, omd, and omi arguments of par() control boundaries and margins of plots - they can be queried using par()$omd (etc). and set (if needed) using par(oma=c()) (where the vector can have up to 4 values - see ?par)

Resources