How to draw arrows across panels of multi-panel plot? - r

Suppose we have a multi-panel plot in R, created by using layout(). I would like to draw an arrow from a specified point in one panel to a specified point in another panel. Thus, the arrow goes across panels of the layout. The starting point of the arrow is specified in the coordinates of its panel, and the end point of the arrow is specified in the coordinates of the destination panel.
As a minimal example, consider this:
layout( matrix( 1:2 , nrow=2 ) )
plot( x=c(1,2) , y=c(1,2) , main="Plot 1" )
plot( x=c(10,20) , y=c(10,20) , main="Plot 2" )
# I want to make an arrow
# from point c(x=1.2,y=1.2) in Plot 1
# to point c(x=18,y=18) in Plot 2
I've searched for methods to accomplish this, but haven't found anything. Thank you for solutions or pointers.

Update
(I'm keeping the previous answer below this, but this more-programmatic way is better given your comments.)
The trick is knowing how to convert from "user" coordinates to the coordinates of the overarching device. This can be done with grconvertX and *Y. I've made some sloppy helper functions here, though they are barely necessary.
user2ndc <- function(x, y) {
list(x = grconvertX(x, 'user', 'ndc'),
y = grconvertY(y, 'user', 'ndc'))
}
ndc2user <- function(x, y) {
list(x = grconvertX(x, 'ndc', 'user'),
y = grconvertY(y, 'ndc', 'user'))
}
For the sake of keeping magic-constants out of the code, I'll predefine your points-of-interest:
pointfrom <- list(x = 1.2, y = 1.2)
pointto <- list(x = 18, y = 18)
It's important that the conversion from 'user' to 'ndc' happen while the plot is still current; once you switch from plot 1 to 2, the coordinates change.
layout( matrix( 1:2 , nrow=2 ) )
Plot 1.
plot( x=c(1,2) , y=c(1,2) , main="Plot 1" )
points(y~x, data=pointfrom, pch=16, col='red')
ndcfrom <- with(pointfrom, user2ndc(x, y))
Plot 2.
plot( x=c(10,20) , y=c(10,20) , main="Plot 2" )
points(y~x, data=pointto, pch=16, col='red')
ndcto <- with(pointto, user2ndc(x, y))
As I did before (far below here), I remap the region on which the next plotting commands will take place. Under the hood, layout is doing things like this. (Some neat tricks can be done with par(fig=..., new=T), including overlaying one plot in, around, or barely-overlapping another.)
par(fig=c(0:1,0:1), new=TRUE)
plot.new()
newpoints <- ndc2user(c(ndcfrom$x, ndcto$x), c(ndcfrom$y, ndcto$y))
with(newpoints, arrows(x[1], y[1], x[2], y[2], col='green', lwd=2))
I might have been able to avoid the ndc2user conversino from ndc back to current user points, but that's playing with margins and axis-expansion and things like that, so I opted not to.
It is possible that the translated points may be outside of the user-points region of this last overlaid plot, in which case they may be masked. To fix this, add xpd=NA to arrows (or in a par(xpd=NA) before it).
Generalized
Okay, so imagine you want to be able to determine the coordinates of any drawing after layout completion. There's a more complex implementation that currently supports what you're asking for. the only requirement is that you call NDC$add() after every (meaningful) plot. For example:
NDC$reset()
layout(matrix(1:4, nrow=2))
plot(1)
NDC$add()
plot(11)
NDC$add()
plot(21)
NDC$add()
plot(31)
NDC$add()
with(NDC$convert(1:4, c(1,1,1,1), c(1,11,21,31)), {
arrows(x[1], y[1], x[2], y[2], xpd=NA, col='red')
arrows(x[2], y[2], x[3], y[3], xpd=NA, col='blue')
arrows(x[3], y[3], x[4], y[4], xpd=NA, col='green')
})
Source can be found here: https://gist.github.com/r2evans/8a8ba8fff060bade13bf21e89f0616c5
Previous Answer
One way is to use par(fig=...,new=TRUE), but it does not preserve the coordinates you e
layout(matrix(1:4,nr=2))
plot(1)
plot(1)
plot(1)
plot(1)
par(fig=c(0,1,0,1),new=TRUE)
plot.new()
lines(c(0.25,0.75),c(0.25,0.75),col='blue',lwd=2)
Since you may be more likely to use this if you have better (non-arbitrary) control over the ends of the points, here's a trick to allow you more control over the points. If I use this, connectiong the top-left point with the bottom-right point:
p <- locator(2)
str(p)
# List of 2
# $ x: num [1:2] 0.181 0.819
# $ y: num [1:2] 0.9738 0.0265
and then in place of lines above I use this:
with(p, arrows(x[1], y[1], x[2], y[2], col='green', lwd=2))
I get
(This picture and the values in p demonstrate how the coordinates are different. When using par(fig=...,new=T);plot.new();, the coordinates return to
par('usr')
# [1] -0.04 1.04 -0.04 1.04
There might be trickery to try to workaround this (such as if you need to automate this step), but it likely will be non-trivial (and not robust).

Related

abline will not put line in correct position

I am quite new to programming/R and I'm having a very unusual problem. I've made a scatterplot and I would like to simply put the x y axis at 0 on the plot. However, when I use abline they are slightly off. I managed to get them to 0 using trial and error, but trying to plot other lines becomes impossible.
library('car')
scatterplot(cost~qaly, reg.line=FALSE, smooth=FALSE, spread=FALSE,
boxplots='xy', span=0.5, xlab="QALY", ylab="COST", main="Bootstrap",
cex=0.5, data=scat2, xlim=c(-.05,.05), grid=FALSE)
abline(v = 0, h = 0)
This gives lines which are slightly to the left and below 0.
here is an image of what this returns:
(I can't post an image since I'm new apparently)
I found that these values put the lines on 0:
abline(v=0.003)
abline(h=3000)
Thanks in advance for the help!
Using #Laterow's example, reproduce the issue
require(car)
set.seed(10)
x <- rnorm(1000); y <- rnorm(1000)
scatterplot(y ~ x)
abline(v=0, h=0)
scatterplot seems to be resetting the par settings on exit. You can sort of check this with locator(1) around some point, eg, for {-3,-3} I get
# $x
# [1] -2.469414
#
# $y
# [1] -2.223922
Option 1
As #joran points out, reset.par = FALSE is the easiest way
scatterplot(y ~ x, reset.par = FALSE)
abline(v=0, h=0)
Option 2
In ?scatterplot, it says that ... is passed to plot meaning you can use plot's very useful panel.first and panel.last arguments (among others).
scatterplot(y ~ x, panel.first = {grid(); abline(v = 0)}, grid = FALSE)
Note that if you were to do the basic
scatterplot(y ~ x, panel.first = abline(v = 0))
you would be unable to see the line because the default scatterplot grid covers it up, so you can turn that off, plot a grid first then do the abline.
You could also do the abline in panel.last, but this would be on top of your points, so maybe not as desirable.

Distortions in multipanel plots

If I plot a data and use lines to superimpose the same data points on the graph, I get the same data points. Lets say
x<-rnorm(100)
plot(x, type="p")
lines(x, type="p",pch=2)
However, I have realized that there is a distortion in R plots when the same is done in a multipanel graph. It seems R is unable to recall the exact values on the y-axis when you plot the same data again. A simple code below shows the outputs from "plot" and "lines" are not the same.
set.seed(1000)
Range<-rbind(rep(0,4),c(100,100,1,100));thres<-70
Ylab<-c("MAD","Bias","CP","CIL")
X<-list(EVI=cbind(runif(10,0,100),runif(10,0,100),
runif(10,0,1),runif(10,0,100)),
Qp=cbind(runif(10,0,100),runif(10,0,100),runif(10,0,1),runif(10,0,100)))
Plot<-function(x,Pch=1,thres)
{
par(mfrow=c(1,4),las=2)
for(j in 1:4)
{
plot(x[,j],xaxt = "n",xlab="Estimator",
ylab=Ylab[j],type = "p", pch = Pch, ylim=Range[,j])
par(mfg=c(1,j))
axis(1, at=1:nrow(x), labels=LETTERS[1:nrow(x)])
if(j!=3){
par(mfg=c(1,j))
abline(h=thres,col=2)
}else{
par(mfg=c(1,j))
abline(h=c(0.90,0.95,0.99),lty=c(2,1,2),col=rep(2,3))
}
}
}
Line<-function(x,Pch)
{
for(j in 1:ncol(x)) {
par(mfg=c(1,j))
lines(x[,j], type = "p", pch = Pch,col=2)
}
}
lapply(X,function(dat)Plot(dat,thres=thres))
## First panel
Line(X$EVI,Pch=2)
## Move to second panel
Line(X$Qp,Pch=2)
What explains the distortions in the positioning of the points in the 3rd column? Note that, I have included the range of each data courtesy #WhiteViking in the "Plot" function. However, the distortion keeps showing. Thank you
The problem is in the ordering of 'plot' and 'lines'.
Code like this, with all 3 'plot' commands upfront:
set.seed(1)
X <- cbind(rnorm(20), 2 * rnorm(20), 3 * rnorm(20))
par(mfrow = c(1,3))
for (i in 1:3) {
plot(X[,i])
}
for (i in 1:3) {
par(mfg = c(1,i))
lines(X[,i], type = "p", col = 2, pch = 3)
}
yields misaligment:
In the example above the first 'lines' command that get executed bases its scaling on the last 'plot' that happened. Since that had a larger vertical range than the first, the scaling of the 'lines' is incorrect.
Whereas structured like so:
set.seed(1)
X <- cbind(rnorm(20), 2 * rnorm(20), 3 * rnorm(20))
par(mfrow = c(1,3))
for (i in 1:3) {
par(mfg = c(1,i))
plot(X[,i])
lines(X[,i], type = "p", col = 2, pch = 3)
}
it gives correct alignment of 'plot' and 'lines':
You'll probably have to rework your code to group 'plot' and 'lines' together for each sub-plot.
When the third column is converted to percentages, the ylim becomes uniform and hence there isn't such distortion. However, it will be good to get a way around it instead of such adhoc transformation
plot() sets up a coordinate system via plot.window based on the range of the data. This information is apparently stored in par(usr) for the latest plot, which means that if you want to revisit older plots, you should store those usr values and reset them accordingly,
set.seed(123)
d1 <- data.frame(x=1:10, y=rnorm(10))
d2 <- data.frame(x=1:10, y=10*rnorm(10))
par(mfrow=c(1,2),mar=c(2.5,2.5,0,0))
plot(d1, type="p")
usr1 <- par("usr")
plot(d2, type="p")
usr2 <- par("usr")
par(mfg=c(1,1), usr=usr1)
points(d1, col="red", pch=3)
par(mfg=c(1,2), usr=usr2)
points(d2, col="red", pch=3)

Using the identify function in R

In a scatterplot, I would like to use identify function to label the right top point.
I did this:
identify(x, y, labels=name, plot=TRUE)
*I have a named vector.
Then, while it is running, I point to the right point. Then after stopping it, it shows me the
label of the point.
Do I have to click the point that I want to label each time? Can I save it?
# Here is an example
x = 1:10
y = x^2
name = letters[1:10]
plot(x, y)
identify(x, y, labels = name, plot=TRUE)
# Now you have to click on the points and select finish at the end
# The output will be the labels you have corresponding to the dots.
Regarding saving it:
I couldn't do it using
pdf()
# plotting code
dev.off()
However in Rstudio it was posible to "copy-paste" it. If you need one plot only, i guess this would work.
You can use the return value of identify function to reproduce the labelling:
labels <- rep(letters, length.out=nrow(cars))
p <- identify(cars$speed, cars$dist, labels, plot=T)
#now we can reproduce labelling
plot(cars)
text(cars$speed[p], cars$dist[p], labels[p], pos=3)
To save the plot after using identify, you can use dev.copy:
labels <- rep(letters, length.out=nrow(cars))
identify(cars$speed, cars$dist, labels, plot=T)
#select your points here
dev.copy(png, 'myplot.png', width=600, height=600)
dev.off()

Colorfill boxplot in R-cran with lines, dots, or similar

I need to use black and white color for my boxplots in R. I would like to colorfill the boxplot with lines and dots. For an example:
I imagine ggplot2 could do that but I can't find any way to do it.
Thank you in advance for your help!
I thought this was a great question and pondered if it was possible to do this in base R and to obtain the checkered look. So I put together some code that relies on boxplot.stats and polygon (which can draw angled lines). Here's the solution, which is really not ready for primetime, but is a solution that could be tinkered with to make more general.
boxpattern <-
function(y, xcenter, boxwidth, angle=NULL, angle.density=10, ...) {
# draw an individual box
bstats <- boxplot.stats(y)
bxmin <- bstats$stats[1]
bxq2 <- bstats$stats[2]
bxmedian <- bstats$stats[3]
bxq4 <- bstats$stats[4]
bxmax <- bstats$stats[5]
bleft <- xcenter-(boxwidth/2)
bright <- xcenter+(boxwidth/2)
# boxplot
polygon(c(bleft,bright,bright,bleft,bleft),
c(bxq2,bxq2,bxq4,bxq4,bxq2), angle=angle[1], density=angle.density)
polygon(c(bleft,bright,bright,bleft,bleft),
c(bxq2,bxq2,bxq4,bxq4,bxq2), angle=angle[2], density=angle.density)
# lines
segments(bleft,bxmedian,bright,bxmedian,lwd=3) # median
segments(bleft,bxmin,bright,bxmin,lwd=1) # min
segments(xcenter,bxmin,xcenter,bxq2,lwd=1)
segments(bleft,bxmax,bright,bxmax,lwd=1) # max
segments(xcenter,bxq4,xcenter,bxmax,lwd=1)
# outliers
if(length(bstats$out)>0){
for(i in 1:length(bstats$out))
points(xcenter,bstats$out[i])
}
}
drawboxplots <- function(y, x, boxwidth=1, angle=NULL, ...){
# figure out all the boxes and start the plot
groups <- split(y,as.factor(x))
len <- length(groups)
bxylim <- c((min(y)-0.04*abs(min(y))),(max(y)+0.04*max(y)))
xcenters <- seq(1,max(2,(len*(1.4))),length.out=len)
if(is.null(angle)){
angle <- seq(-90,75,length.out=len)
angle <- lapply(angle,function(x) c(x,x))
}
else if(!length(angle)==len)
stop("angle must be a vector or list of two-element vectors")
else if(!is.list(angle))
angle <- lapply(angle,function(x) c(x,x))
# draw plot area
plot(0, xlim=c(.97*(min(xcenters)-1), 1.04*(max(xcenters)+1)),
ylim=bxylim,
xlab="", xaxt="n",
ylab=names(y),
col="white", las=1)
axis(1, at=xcenters, labels=names(groups))
# draw boxplots
plots <- mapply(boxpattern, y=groups, xcenter=xcenters,
boxwidth=boxwidth, angle=angle, ...)
}
Some examples in action:
mydat <- data.frame(y=c(rnorm(200,1,4),rnorm(200,2,2)),
x=sort(rep(1:2,200)))
drawboxplots(mydat$y, mydat$x)
mydat <- data.frame(y=c(rnorm(200,1,4),rnorm(200,2,2),
rnorm(200,3,3),rnorm(400,-2,8)),
x=sort(rep(1:5,200)))
drawboxplots(mydat$y, mydat$x)
drawboxplots(mydat$y, mydat$x, boxwidth=.5, angle.density=30)
drawboxplots(mydat$y, mydat$x, # specify list of two-element angle parameters
angle=list(c(0,0),c(90,90),c(45,45),c(45,-45),c(0,90)))
EDIT: I wanted to add that one could also obtain dots as a fill by basically drawing a pattern of dots, then covering them a "donut"-shaped polygon, like so:
x <- rep(1:10,10)
y <- sort(x)
plot(y~x, xlim=c(0,11), ylim=c(0,11), pch=20)
outerbox.x <- c(2.5,0.5,10.5,10.5,0.5,0.5,2.5,7.5,7.5,2.5)
outerbox.y <- c(2.5,0.5,0.5,10.5,10.5,0.5,2.5,2.5,7.5,7.5)
polygon(outerbox.x,outerbox.y, col="white", border="white") # donut
polygon(c(2.5,2.5,7.5,7.5,2.5),c(2.5,2.5,2.5,7.5,7.5)) # inner box
But mixing that with angled lines in a single plotting function would be a bit difficult, and is generally a bit more challenging, but it starts to get you there.
I think it is hard to do this with ggplot2 since it dont use shading polygon(gris limitatipn). But you can use shading line feature in base plot, paramtered by density and angle arguments in some plot functions ( ploygon, barplot,..).
The problem that boxplot don't use this feature. So I hack it , or rather I hack bxp internally used by boxplot. The hack consist in adding 2 arguments (angle and density) to bxp function and add them internally in the call of xypolygon function ( This occurs in 2 lines).
my.bxp <- function (all.bxp.argument,angle,density, ...) {
.....#### bxp code
xypolygon(xx, yy, lty = boxlty[i], lwd = boxlwd[i],
border = boxcol[i],angle[i],density[i])
.......## bxp code after
xypolygon(xx, yy, lty = "blank", col = boxfill[i],angle[i],density[i])
......
}
Here an example. It should be noted that it is entirely the responsibility of the user to ensure
that the legend corresponds to the plot. So I add some code to rearrange the legend an the boxplot code.
require(stats)
set.seed(753)
(bx.p <- boxplot(split(rt(100, 4), gl(5, 20))))
layout(matrix(c(1,2),nrow=1),
width=c(4,1))
angles=c(60,30,40,50,60)
densities=c(50,30,40,50,30)
par(mar=c(5,4,4,0)) #Get rid of the margin on the right side
my.bxp(bx.p,angle=angles,density=densities)
par(mar=c(5,0,4,2)) #No margin on the left side
plot(c(0,1),type="n", axes=F, xlab="", ylab="")
legend("top", paste("region", 1:5),
angle=angles,density=densities)

Shade area between 2 curves

I can't seem to wrap my mind arround how polygon() works. I've searched a lot but I cant seem to understand how polygon wants the x,y points and what do they represent.
Could someone please help me and explain how to shade for example the area between the red and blue line
curve(x/2, from=0 , to =1, col="darkblue")
curve(x/4, from=0 , to =1, add=T, col="darkred")
Thanks a lot
Because, in this case, there isn't really any curve to the line you could use something very simple (that highlights how polygon works).
x <- c(0,1,1,0)
y <- c(x[1:2]/2, x[3:4]/4)
polygon(x,y, col = 'green', border = NA)
Now, if you had a curve you'd need more vertices.
curve(x^2, from=0 , to =1, col="darkblue")
curve(x^4, from=0 , to =1, add=T, col="darkred")
x <- c(seq(0, 1, 0.01), seq(1, 0, -0.01))
y <- c(x[1:101]^2, x[102:202]^4)
polygon(x,y, col = 'green', border = NA)
(extend the range of that last curve and see how using similar code treats the crossing curves yourself)
To generalise the accepted answer.. if you have two curves (two vectors) f1(x1), f2(x2) which satisfy f1(x1) < f2(x2) for all x1,x2, then you can use
#' #brief Draws a polygon between two curves
#' #f1,f2 Vectors satisfying f1 < f2
#' #x1,x2 Respective domains of f1, f2
#' #... Arguments to ?polygon
ShadeBetween <- function(x1, x2, f1, f2, ...) {
polygon(c(x1, rev(x2)), c(f1, rev(f2)), ...)
}
For this specific example we have :
x <- seq(0,1,length=100)
matplot(x,cbind(x/2, x/4), type='l', col='white')
ShadeBetween(x,x, x/2, x/4, col='red')

Resources