rggplot2r-grid

How to keep linetype spacing constant despite line size


I've been attempting to plot lines in either ggplot2 or grid with equal spacing between line segments when the sizes differ. However I've not been succesfull so I ask you for help.

In the examples below, how can I keep the absolute spacing between line segments equal while the line sizes differ?

I'd like to avoid making custom makeContent.myclass methods to control this myself.

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)

I'd like something that resembles the following made in illustrator. Note that the red line pieces are of equal length.

enter image description here

Any ideas? Thanks for reading!


Solution

  • This is probably not what you're looking for Teunbrand, but I guess you could convert your lines to a series of thin polygonGrobs equally spaced along the lines.

    This function takes a series of x and y co-ordinates and returns a dashed line (as a single treeGrob). As per your example it returns it in normalised npc co-ordinates. You have full control over the line width, dash length and break length (though not the pattern), as well as the colour. I'm afraid the units are a bit arbitrary, and this is far from production standard, but it's fairly effective:

    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))
       }))
    
    }
    

    It's fairly straightforward to use:

    set.seed(2)
    
    x <- 1:10
    y <- rnorm(10)
    
    grid::grid.newpage()
    grid::grid.draw(segmentify(x, y))
    

    enter image description here

    And changing the line width without affecting the spacing is just like this:

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

    enter image description here

    And you can control spacing and color like this:

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

    enter image description here