好的,受到Allan的鼓励,我决定尝试着自己画这个东西,这与我在这个问题中试图避免的事情相同,但对于你们其他人可能会有所帮助。
我的方法略有不同,主要区别在于(1)我们保留折线而不是转换为多边形;(2)我不太熟悉三角函数,所以我使用了approxfun()
来插值线条;(3)我们将使用绝对单位而不是相对单位,这样当设备调整大小时就不会很尴尬。
首先,由于我打算将其用于自定义geom函数中,因此我旨在创建一个grob结构,以便轻松粘贴到geom的绘制方法的末尾。您可以给它一个grob,或者给出一个grob的参数。它会更改grob的类,这将在后面变得相关,删除linetype参数并添加虚线和断点的信息。
library(grid)
library(scales)
linetypeGrob <- function(x, ..., dashes = 1, breaks = 1) {
if (!inherits(x, "polyline")) {
x <- polylineGrob(x, ...)
}
class(x)[[1]] <- "linetypeGrob"
x$gp$lty <- NULL
x$dashes <- dashes
x$breaks <- breaks
x
}
现在,正如我之前提到的,我们会回到类。关于自定义grob类的好处是,您可以在它们被绘制之前拦截它们,以便进行最后一分钟的更改。为此,我们在grid中编写一个S3方法来
makeContext
函数,以进行相关更改。我知道这是一个很长的函数,但我尝试通过插入注释来使其更容易理解我正在尝试做什么。
makeContext.linetypeGrob <- function(x) {
id <- x$id
if (is.null(id)) {
if (is.null(x$id.lengths)) {
id <- rep(1L, length(x$x))
} else {
id <- rep(seq_along(x$id.lengths), x$id.lengths)
}
}
x$id <- NULL
x$id.lengths <- NULL
dashes <- x$dashes
x$dashes <- NULL
breaks <- x$breaks
x$breaks <- NULL
newx <- convertX(x$x, "mm", TRUE)
newy <- convertY(x$y, "mm", TRUE)
dist <- sqrt(diff(newx)^2 + diff(newy)^2)
cumdist <- cumsum(c(0, dist))
starts <- seq(0, max(cumdist), by = (dashes + breaks))
ends <- seq(dashes, max(cumdist), by = (dashes + breaks))
if (length(ends) == length(starts) - 1) {
ends <- c(ends, max(cumdist))
}
gp_i <- findInterval(starts, cumdist[cumsum(rle(id)$lengths)]) + 1
start_id <- findInterval(starts, cumdist)
end_id <- findInterval(ends, cumdist)
mismatch <- which(start_id != end_id)
starts <- c(starts, cumdist[end_id[mismatch]])
starts <- starts[{o <- order(starts)}]
ends <- sort(c(ends, cumdist[end_id[mismatch]]))
new_id <- seq_along(start_id)
if (length(mismatch)) {
i <- rep_len(1, length(new_id))
i[mismatch] <- 2
new_id <- rep(new_id, i)
}
keepfun <- approxfun(cumdist, id)
keep <- (keepfun(starts) %% 1) == 0 & (keepfun(ends) %% 1) == 0
xfun <- approxfun(cumdist, newx)
x0 <- xfun(starts[keep])
x1 <- xfun(ends[keep])
yfun <- approxfun(cumdist, newy)
y0 <- yfun(starts[keep])
y1 <- yfun(ends[keep])
x$gp[] <- lapply(x$gp, function(x){
if (length(x) == 1) {
return(x)
} else {
x[as.integer(gp_i)]
}
})
x$x <- unit(as.vector(rbind(x0, x1)), "mm")
x$y <- unit(as.vector(rbind(y0, y1)), "mm")
x$id <- as.vector(rbind(new_id[keep], new_id[keep]))
class(x)[[1]] <- "polyline"
x
}
最后,为了证明它的工作原理,我会用这个新的grob绘制一些虚拟数据。你可以像绘制普通的折线grob一样使用它。
set.seed(100)
x <- c(cumsum(rnorm(10)), cumsum(rnorm(10)))
y <- c(cumsum(rnorm(10)), cumsum(rnorm(10)))
id <- rep(c(1, 2), each = 10)
gp <- gpar(lwd = c(2, 10), lineend = "butt",
col = c("magenta", "blue"))
grob <- linetypeGrob(scales::rescale(x),
scales::rescale(y),
id = id, gp = gp, dashes = 5, breaks = 2)
grid.newpage(); grid.draw(grob)
如果我调整设备大小,您可以看到破折号和间隔的长度仍然保持相等: