Alternating color of individual dashes in a geom_line

I'm wondering if in a geom_line you can make it so the colors of, say, the dashes within a single line alternate (rather than the colors differing between lines). For example, if I wanted this singular line to alternate red, green, and blue rather than being just red.

library(tidyverse)

ggplot(tibble(x = 1:10, y = 1:10), aes(x, y)) +
    geom_line(linetype = "dashed", color = "red") # i'd like to say something like, color = c("red", "green", "blue") instead

While a little inefficient, a little-known thing about R's par(lty=) (that geom_line(linetype=) shares) is that it can be specified as on/off stretches. From ?par under Line Type Specification:

 Line types can either be specified by giving an index into a small
 built-in table of line types (1 = solid, 2 = dashed, etc, see
 'lty' above) ...

(which is what most tutorials/howtos/plots tend to use)

          ... or directly as the lengths of on/off stretches of
 line.  This is done with a string of an even number (up to eight)
 of characters, namely _non-zero_ (hexadecimal) digits which give
 the lengths in consecutive positions in the string.  For example,
 the string '"33"' specifies three units on followed by three off
 and '"3313"' specifies three units on followed by three off
 followed by one on and finally three off.  The 'units' here are
 (on most devices) proportional to 'lwd', and with 'lwd = 1' are in
 pixels or points or 1/96 inch.

So with your dat, one could do

dat <- tibble(x = 1:10, y = 1:10)
ggplot(dat, aes(x,y)) +
  geom_line(linetype="1741", color="red", size=3) +
  geom_line(linetype="1345", color="blue", size=3) +
  geom_line(linetype="49", color="green", size=3)

to get

enter image description here

I could not get it to work without one blank space: the on/off stretches must always start with an "on", and end with an "off"; as such I could not find a pattern that didn't (at least once) end on an "on" without an imposed gap.

For further explanation, since we always must start with an "on", I start all three with at least a single pixel of "on"; the trick is to make the "long" stretch for the beginning to be the last line plotted, so it over-plots the others.

red:  R.......RRRR.
      1       -4--
       ---7---    1

grn:  G...GGGG.....
      1   -4--
       -3-    --5--

blu:  BBBB.........
      -4--
          ----9----

This has some advantages: regardless of size=, it scales the same. For instance, omitting size=,

enter image description here


Using approx:

# number of points at which interpolation takes place
# increase if line takes sharp turns
n = 100

# number of segments along line, according to taste
n_seg = 20

# segment colors
cols = c("red", "green", "blue")

# interpolate
d = approx(dat$x, dat$y, n = n)

# create start and end points for segments
d2 = data.frame(x = head(d$x, -1), xend = d$x[-1],
                y = head(d$y, -1), yend = d$y[-1])

# create vector of segment colors
d2$col = rep(cols, each = ceiling((n - 1) / n_seg), length.out = n - 1)

ggplot(d2, aes(x = x, xend = xend, y = y, yend = yend, color = col)) +
  geom_segment() + scale_color_identity(guide = "none")

enter image description here


This is an implementation of a new Stat based on GeomSegment which creates alternating segments of different colors. This works by passing the alternating colors to the data frame created in Stat$compute_group. GeomSegment uses StatIdentity, so no need to specifically map xend, yend and color.

BIG THANKS to Henrik for showing a very neat way of creating the segments. (my own way was very convoluted, and I'll leave it in this thread for posterity). The only remaining "problem" is that the segments might have different lengths in changing slopes - on the other hand, it might be visually desirable to have different segment lengths in this case.

library(ggplot2)
## attaching just for demonstration purpose
library(patchwork)

# geom_colorpath
# @description lines with alternating color "just for the effect".
# @name colorpath
# @examples
# @export
StatColorPath <- ggproto("StatColorPath", Stat,
                         compute_group = function(data, scales, params,
                                                  n_seg = 20, n = 100, cols = c("black", "white")) {
                           # interpolate
                           d <- approx(data$x, data$y, n = n)
                           # create start and end points for segments
                           d2 <- data.frame(
                             x = head(d$x, -1), xend = d$x[-1],
                             y = head(d$y, -1), yend = d$y[-1]
                           )
                           # create vector of segment colors
                           d2$color <- rep(cols, each = ceiling((n - 1) / n_seg), length.out = n - 1)
                           d2
                         },
                         required_aes = c("x", "y")
)

# @rdname colorpath
# @import ggplot2
# @inheritParams ggplot2::layer
# @inheritParams ggplot2::geom_segment
# @param n_seg number of segments along line, according to taste
# @param n number of points at which interpolation takes place
#   increase if line takes sharp turns
# @param cols vector of alternating colors
# @export
geom_colorpath <- function(mapping = NULL, data = NULL, geom = "segment",
                           position = "identity", na.rm = FALSE, show.legend = NA,
                           inherit.aes = TRUE, cols = c("black", "white"),
                           n_seg = 20, n = 100, ...) {
  layer(
    stat = StatColorPath, data = data, mapping = mapping, geom = geom,
    position = position, show.legend = show.legend, inherit.aes = inherit.aes,
    params = list(na.rm = na.rm, cols = cols, n = n, n_seg = n_seg,...)
  )
}

# @rdname colorpath
# @export
geom_colorpath <- function (mapping = NULL, data = NULL, stat = "ColorPath", position = "identity",
                            ..., arrow = NULL, arrow.fill = NULL, lineend = "butt", linejoin = "round",
                            na.rm = FALSE, show.legend = NA, inherit.aes = TRUE)
{
  layer(data = data, mapping = mapping, stat = stat, geom = GeomSegment,
        position = position, show.legend = show.legend, inherit.aes = inherit.aes,
        params = list(arrow = arrow, arrow.fill = arrow.fill,
                      lineend = lineend, linejoin = linejoin, na.rm = na.rm,
                      ...))
}


dat <- data.frame(x = seq(2,10, 2), y = seq(4,20, 4))

p1 <- ggplot(dat, aes(x = x, y = y)) +
  geom_colorpath()+
  ggtitle("Default colors")
p2 <- ggplot(dat, aes(x, y)) +
  geom_colorpath(cols = c("red", "blue"))+
  ggtitle("Two colors")

p3 <- ggplot(dat, aes(x, y)) +
  geom_colorpath(cols = c("red", "blue", "green"))+
  ggtitle("Three colors")

p4 <- ggplot(dat, aes(x, y)) +
  geom_colorpath(cols = c("red", "blue", "green", "white"))+
  ggtitle("four colors")

wrap_plots(mget(ls(pattern = "p[1-9]")))

air_df <- data.frame(x = 1: length(AirPassengers), y = c(AirPassengers))

a1 <- ggplot(air_df, aes(x, y)) +
  geom_colorpath(cols = c("red", "blue", "green"))+
  ggtitle("Works also with more complex curves")

a2 <- ggplot(air_df, aes(x, y)) +
  geom_colorpath(cols = c("red", "blue", "green"), n_seg = 150)+
  ggtitle("... more color segments")

a1 / a2

Created on 2022-01-18 by the reprex package (v2.0.1)