I was wondering if anyone could help me plot lines in R with multiple arrows in them, like this:
--->--->--->--->
Thanks in advance!
I thought I would add a base R solution here that calculates the arrow heads as segments. There are versions to create a new plot or add arrows to an existing plot. Longer segments are split into shorter ones:
add_arrows <- function(x, y, ...)
{
# Ensure equal lengthed vectors
min_len <- min(length(x), length(y))
x <- x[seq(min_len)]
y <- y[seq(min_len)]
# Calculate y:x ratio
yx_ratio <- 3*(max(y) - min(y)) / (max(x) - min(x))
# Create start and end points
x1 <- x[-length(x)]
y1 <- y[-length(y)]
x2 <- x[-1]
y2 <- y[-1]
# Length and angle of line segments
theta <- atan((y2 - y1)/(x2 - x1))
seg_lengths <- sqrt((x2 - x1)^2 + (y2 - y1)^2)
# Break long segments into shorter segments
long <- which(seg_lengths > (2 * min(seg_lengths)))
if(length(long) > 0)
{
n_segs <- floor(seg_lengths[long] / (1.5 * min(seg_lengths)))
for(i in seq_along(long))
{
xs <- x1[long[i]] + (0:n_segs[i]) * (x2[long[i]] - x1[long[i]]) / n_segs[i]
ys <- y1[long[i]] + (0:n_segs[i]) * (y2[long[i]] - y1[long[i]]) / n_segs[i]
x1 <- c(x1, xs[-length(xs)])
x2 <- c(x2, xs[-1])
y1 <- c(y1, ys[-length(ys)])
y2 <- c(y2, ys[-1])
theta <- c(theta, rep(theta[long[i]], length(xs) - 1))
}
x1 <- x1[-long]; x2 <- x2[-long]; y1 <- y1[-long]; y2 <- y2[-long];
theta <- theta[-long]
}
# Arrow head length and width
len <- sqrt((max(y) - min(y))^2 + (max(x) - min(x))^2)/ 20
wid <- len * 0.1
# Calculate arrow heads
zx <- x2 - len * cos(theta) * sign(x2 - x1)
zy <- y2 - len * sin(theta) * sign(x2 - x1)
ax1 <- zx + wid * sin(theta) * sign(x2 - x1)
ax2 <- zx - wid * sin(theta) * sign(x2 - x1)
ay1 <- zy - wid * cos(theta) * sign(x2 - x1) * yx_ratio
ay2 <- zy + wid * cos(theta) * sign(x2 - x1) * yx_ratio
df <- data.frame(x1 = x1 + 0.1 * (x2 - x1),
y1 = y1 + 0.1 * (y2 - y1),
x2 = x2 - 0.5 * len * cos(theta) * sign(x2 - x1),
y2 = y2 - 0.5 * len * sin(theta) * sign(x2 - x1),
ax1 = ax1, ay1 = ay1, ax2 = ax2, ay2 = ay2)
# Draw segments
segments(df$x1, df$y1, df$x2, df$y2, col = "red", lwd = 2)
segments(df$x2, df$y2, df$ax1, df$ay1, col = "red", lwd = 2)
segments(df$x2, df$y2, df$ax2, df$ay2, col = "red", lwd = 2)
}
This allows you to plot arrows as the basis of your plot:
plot_arrows <- function(x, y, ...)
{
plot(x, y, col = "white", ...)
add_arrows(x, y, ...)
}
So now you can do:
set.seed(69)
x <- 1:10
y <- sample(10)
plot(x, y)
add_arrows(x, y)
Created on 2020-02-29 by the reprex package (v0.3.0)
Simulair to DWin this is what I came up with:
arrowLine <- function(x0,y0,x1,y1,nArrow=1,...)
{
lines(c(x0,x1),c(y0,y1),...)
Ax=seq(x0,x1,length=nArrow+1)
Ay=seq(y0,y1,length=nArrow+1)
for (i in 1:nArrow)
{
arrows(Ax[i],Ay[i],Ax[i+1],Ay[i+1],...)
}
}
Basically it overlaps several arrows in a line, but does get the desired result (I assume straigth lines):
plot(0:1,0:1)
arrowLine(0,0,1,1,4)
The real tricky part comes to getting the arrow to got o the edge of points instead of the center. Is that needed?
As joran says, arrows
x=cos( seq(0, pi, by=pi/8) )
y=sin( seq(0, pi, by=pi/8))
plot(1,1, ylim=range(y), xlim=range(x))
arrows(x[-length(x)],y[-length(y)], x[-1],y[-1])
If you want to draw a straight multiple arrow curve with specification of equal lengths, then use this function:
multarrows <- function(x0,y0, x1,y1,n_arr, ...) {x<- seq(x0,x1, length=n_arr+1)
y<-seq(y0,y1, length=n_arr+1)
arrows(x[-length(x)],y[-length(y)], x[-1],y[-1], ...) }
plot(0,0, xlim=c(0,2), ylim=c(0,11)); multarrows(0,0, 1,10, 10)
There's a sneaky way to do this without having to plot a bunch of line segments or arrows.
Start with x
and y
data as above:
plot(x, y, t='l')
Then calculate the local slopes from x[1]
, y[1]
to x[2]
, y[2]
and so on, using some canned routine. Convert the slopes to angles in degrees, and then (calling the vector of slopes angslopes
)
text(x, y, labels=">", srt=angslopes) # throws error
Sorry, that won't work because, so far as I can tell, srt
must be a single value. So:
diflen <- length(x)-1
sapply(1:diflen, function(xind) text(x[xind+1], y[xind+1], labels='>', cex=2.5, srt=angslopes[xind]))
There may well be a much easier way to do this in the lattice package.
This approach has the cosmetic advantage of allowing you to specify the arrowhead size via par(cex)
as well as the color - which could be different from the line color, or even varying along the curve.
Based on the clarifications to the original question, I think a general answer should consider
calculating the total length of the broken curve specified by (x,y) points
splitting apart intermediate segments so as to ensure equal curvilinear length between arrow heads
take care of minor details such as initial "phase", end-point, whether the arrows heads should be followed by a thin space, etc.
Below is a rough stab at it.
arrowLine <- function(x, y, N=10, ...){
lengths <- c(0, sqrt(diff(x)^2 + diff(y)^2))
l <- cumsum(lengths)
tl <- l[length(l)]
el <- seq(0, to=tl, length=N+1)[-1]
plot(x, y, t="l", ...)
for(ii in el){
int <- findInterval(ii, l)
xx <- x[int:(int+1)]
yy <- y[int:(int+1)]
## points(xx,yy, col="grey", cex=0.5)
dx <- diff(xx)
dy <- diff(yy)
new.length <- ii - l[int]
segment.length <- lengths[int+1]
ratio <- new.length / segment.length
xend <- x[int] + ratio * dx
yend <- y[int] + ratio * dy
points(xend,yend, col="white", pch=19)
arrows(x[int], y[int], xend, yend, length=0.1)
}
}
set.seed(123)
x = sort(c(0, runif(200, 0,2* pi), 2*pi))
y=sin(x)
arrowLine(x, y, N=20)