【问题标题】:How to keep linetype spacing constant despite line size尽管线条大小如何保持线型间距不变
【发布时间】:2020-11-13 14:42:53
【问题描述】:

我一直试图在 ggplot2 或网格中绘制线条,当尺寸不同时,线段之间的间距相等。但是我没有成功,所以我请你帮忙。

在下面的示例中,如何在线条大小不同的情况下保持线段之间的绝对间距相等?

我想避免使用自定义 makeContent.myclass 方法来自己控制它。

library(ggplot2)
library(grid)

df <- data.frame(
  x = c(1:2, 1:2),
  y = c(1:2, 2:1),
  size = c(1,1,10,10)
)

# In ggplot2
ggplot(df, aes(x, y, size = size, group = size)) +
  geom_line(linetype = 2)

# In grid
lines <- polylineGrob(
  x = scales::rescale(df$x), 
  y = scales::rescale(df$y), 
  id = c(1,1,2,2),
  gp = gpar(lty = 2, lwd = c(1, 10))
)

grid.newpage(); grid.draw(lines)

我想要类似于以下插图的东西。请注意,红线段的长度相等。

有什么想法吗?感谢阅读!

【问题讨论】:

  • 看起来不正确,但这可能会有所帮助:stackoverflow.com/questions/52885265/…
  • 是的,这确实有助于阅读,谢谢。 tl;dr 的一些要点:(1) 图形设备增加了一层不可预测性。 (2) 对于 2 线宽的幂(2、4、8、16 等),它似乎工作得很好,但也只有 2 的幂。在我试图解决的首要问题中,这将过于严格。

标签: r ggplot2 r-grid


【解决方案1】:

这可能不是您要寻找的 Teunbrand,但我想您可以将您的线条转换为一系列沿线条等距分布的细多边形 Grobs。

此函数采用一系列 x 和 y 坐标并返回一条虚线(作为单个 treeGrob)。根据您的示例,它以标准化的 npc 坐标返回它。您可以完全控制线宽、虚线长度和断线长度(尽管不是图案)以及颜色。恐怕单位有点随意,这与生产标准相差甚远,但它相当有效:

segmentify <- function(x, y, linewidth = 1, dash_len = 1, 
                       break_len = 1, col = "black")
{
  
  linewidth <- 0.002 * linewidth
  dash_len  <- 0.01  * dash_len
  break_len <- 0.04  * break_len

  if(length(y) != length(x)) 
    stop("x and y must be the same length")
  if(!is.numeric(x) | !is.numeric(y))
    stop("x and y must be numeric vectors")
  if(length(x) < 2)
    stop("Insufficient x, y pairs to make line.")
  
  x <- scales::rescale(x)
  y <- scales::rescale(y)
  
  n_dashes <- 0
  skip_len <- break_len + dash_len
  
   df <- list()
  for(i in seq_along(x)[-1])
  {
    x_diff          <- x[i] - x[i - 1]
    y_diff          <- y[i] - y[i - 1]
    seg_len         <- sqrt(x_diff^2 + y_diff^2)
    seg_prop        <- skip_len / seg_len
    dist_from_start <- n_dashes * skip_len
    prop_start      <- dist_from_start/seg_len
    x_start         <- x[i-1] + prop_start * x_diff
    y_len           <- y_diff * seg_prop
    x_len           <- x_diff * seg_prop
    y_start         <- y[i-1] + prop_start * y_diff
    n_breaks        <- (seg_len - dist_from_start)/skip_len
    n_dashes        <- (n_dashes + n_breaks) %% 1
    n_breaks        <- floor(n_breaks)
    
    if(n_breaks)
    {
       df[[length( df) + 1]] <- data.frame(
        x = seq(x_start, x[i], by = x_len),
        y = seq(y_start, y[i], by = y_len)
        )
       df[[length( df)]]$theta <-
        atan(rep(y_diff/x_diff, length( df[[length( df)]]$x)))
    }
  }
  
   df <- do.call(rbind,  df)
   df$x1 <-  df$x + sin( df$theta) * linewidth + cos(df$theta) * dash_len
   df$x2 <-  df$x + sin( df$theta) * linewidth - cos(df$theta) * dash_len
   df$x3 <-  df$x - sin( df$theta) * linewidth - cos(df$theta) * dash_len
   df$x4 <-  df$x - sin( df$theta) * linewidth + cos(df$theta) * dash_len
   
   df$y1 <-  df$y - cos( df$theta) * linewidth + sin(df$theta) * dash_len
   df$y2 <-  df$y - cos( df$theta) * linewidth - sin(df$theta) * dash_len
   df$y3 <-  df$y + cos( df$theta) * linewidth - sin(df$theta) * dash_len
   df$y4 <-  df$y + cos( df$theta) * linewidth + sin(df$theta) * dash_len
  
   do.call(grid::grobTree, lapply(seq(nrow(df)), function(i) {
    grid::polygonGrob(c(df$x1[i], df$x2[i], df$x3[i], df$x4[i]), 
                      c(df$y1[i], df$y2[i], df$y3[i], df$y4[i]),
              gp = gpar(col = "#00000000", lwd = 0, fill = col))
   }))

}

使用起来相当简单:

set.seed(2)

x <- 1:10
y <- rnorm(10)

grid::grid.newpage()
grid::grid.draw(segmentify(x, y))

而在不影响间距的情况下改变线宽就是这样:

grid::grid.newpage()
grid::grid.draw(segmentify(x, y, linewidth = 3))

你可以像这样控制间距和颜色:

grid::grid.newpage()
grid::grid.draw(segmentify(x, y, linewidth = 2, break_len = 0.5, col = "forestgreen"))

【讨论】:

  • 感谢艾伦,这本身就是一个非常好的答案,我非常感谢您付出所有这些努力来提出这一切。我会尝试一下,看看它是如何工作的!
【解决方案2】:

好的,在艾伦的鼓励下,我自己画这些东西并没有那么糟糕,我决定也尝试解决这个问题。它正在做我试图避免这个问题的事情,但它可能对你们其他人有所帮助。

我采用了稍微不同的方法,主要区别在于 (1) 我们保留折线而不是转换为多边形,以及 (2) 我对三角函数不太满意,因此我使用 approxfun() 来插入线和(3) 我们将使用绝对单位而不是相对单位,因此在调整设备大小时不会很尴尬。

首先,当我打算在自定义 geom 函数中使用它时,我的目标是制作一个 grob 结构,以便在 geom 的绘制方法的末尾轻松粘贴。你可以给它一个 grob,或者一个 grob 的参数。它更改了 grob 的类(稍后将变得相关),删除线型参数并添加破折号和中断的信息。

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 类的巧妙之处在于,您可以在它们被绘制之前拦截它们,以便您可以在最后一刻进行更改。为此,我们为网格中的makeContext 函数编写了一个 S3 方法,以进行相关更改。我知道这是一个很长的函数,但我试图通过插入告诉我要做什么的 cmets 使其更容易理解。

makeContext.linetypeGrob <- function(x) {
  # Sort out line IDs
  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)
    }
  }

  # Delete previous line IDs
  x$id <- NULL
  x$id.lengths <- NULL

  # Take dashes and breaks parameters out of the old grob
  dashes <- x$dashes
  x$dashes <- NULL
  breaks <- x$breaks
  x$breaks <- NULL

  # Convert to absolute units
  newx <- convertX(x$x, "mm", TRUE)
  newy <- convertY(x$y, "mm", TRUE)

  # Express lines as points along a cumulative distances
  dist <- sqrt(diff(newx)^2 + diff(newy)^2)
  cumdist <- cumsum(c(0, dist))

  # Take new lines as a sequence along the cumulative distance
  starts <- seq(0, max(cumdist), by = (dashes + breaks))
  ends <- seq(dashes, max(cumdist), by = (dashes + breaks))
  if (length(ends) == length(starts) - 1) {
    # Case when the end actually should have gone beyond `max(cumdist)`
    ends <- c(ends, max(cumdist))
  }

  # Set index for graphical parameters
  gp_i <- findInterval(starts, cumdist[cumsum(rle(id)$lengths)]) + 1

  # Basically dealing with elbow pieces a bit
  # Find mismatches between the original segments that starts and ends fall on
  start_id <- findInterval(starts, cumdist)
  end_id <- findInterval(ends, cumdist)
  mismatch <- which(start_id != end_id)

  # Insert elbow pieces
  starts <- c(starts, cumdist[end_id[mismatch]])
  starts <- starts[{o <- order(starts)}] # Need the order for later
  ends <- sort(c(ends, cumdist[end_id[mismatch]]))

  # Join elbow pieces
  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)
  }

  # Seperate lines with different IDs
  keepfun <- approxfun(cumdist, id)
  keep <- (keepfun(starts) %% 1) == 0 & (keepfun(ends) %% 1) == 0

  # Interpolate x
  xfun <- approxfun(cumdist, newx)
  x0 <- xfun(starts[keep])
  x1 <- xfun(ends[keep])

  # Interpolate y
  yfun <- approxfun(cumdist, newy)
  y0 <- yfun(starts[keep])
  y1 <- yfun(ends[keep])

  # Expand graphic parameters by new ID
  x$gp[] <- lapply(x$gp, function(x){
    if (length(x) == 1) {
      return(x)
    } else {
      x[as.integer(gp_i)]
    }
  })

  # Put everything back into the grob
  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)

如果我调整设备大小,您可以看到破折号和中断的长度保持不变:

【讨论】:

  • 非常好@teunbrand。获得中断的连续性和良好的线路连接是我在发布之前没有花时间解决的棘手部分。您是否正在计划一个新的geom_line 变体?看起来它可以用来让线条进行额外的连续美学映射。
  • 嗯,这也是一个有用的想法,但这并不是我的意图。我试图通过让它们以不同的大小重叠来设计一些主题元素和几何图形。但是,对于不同的线型,这并不是很顺利。例如,我尝试以这种方式让它们“发光”:teunbrand.github.io/elementalist/index.html#glow
  • 啊,是的,堆叠宽度稍微增加的线条会随着线条宽度的增加而变得更加透明。这就是为什么您需要在同一个地方使用片段,但可以很好地控制宽度和片段长度。不错。
  • ...为您的wiggle 主题额外+2 - 它让我想起了去年的视网膜脱离。干杯
  • @AllanCameron 是的,这正是我需要控制线段间距的原因。我的意思是,人们可能可以不用线型,过着不受束缚的生活,但如果它们可用,那就太好了。
猜你喜欢
  • 2018-05-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-25
  • 1970-01-01
相关资源
最近更新 更多