Search code examples
rggplot2

How to draw circles (circular dots) from linetype in ggplot2


I have a plot with a linetype that are pill shaped. But I would like to draw them as circles. Is there a way to change the linetype in ggplot2 to get 1. circles, and 2. change the spacing between these circles (with a numeric parameter that would allow for more or less space between the circles)?

library(tidyverse)

x = factor(1:100)
y = 1
# set.seed(42)
df = data.frame(x, y)
add_lines = purrr::map(.x = 0:10, ~ df |>
                         mutate(y = y+.x+ sqrt(runif(1, min = 0, max = 20)), 
                                gr = paste0('line', .x))) |> 
  bind_rows() |> 
  mutate(x_nb = as.numeric(x))

ggplot(data = add_lines, 
       mapping = aes(x = x, 
                     y = sqrt(x_nb)*sin(y+pi*(x_nb/max(x_nb)))+cos(y)+sqrt(y)*-sin(sqrt(x_nb)), 
                     group = gr,
                     color = gr)) + 
  geom_path(size = 2, linetype = '11',lineend = "round") +
  scale_color_viridis_d() + 
  theme(panel.grid.major = element_line(linetype = "blank"),
    panel.grid.minor = element_line(linetype = "blank"),
    axis.title = element_text(size = 0),
    axis.text = element_text(size = 0), axis.text.x = element_text(size = 0),
    axis.text.y = element_text(size = 0),
    plot.title = element_text(size = 0),
    panel.background = element_rect(fill = "grey90"),
    plot.background = element_rect(colour = NA),
    axis.line = element_line(colour = NA),
    axis.ticks = element_line(colour = NA),
    legend.position = "none") +labs(x = NULL, y = NULL)

Solution

  • One way to tackle this is simply to draw dots along your paths using geom_point. For this, we can write a helper function that takes your data frame and modifies it so that it gives equally-spaced points along each path using interpolation.

    make_points <- function(data, x, y, groups = 1, spacing = 1) {
      
      xvals <- data[[deparse(substitute(x))]]
      yvals <- data[[deparse(substitute(y))]]
      ratio <- diff(range(yvals)) / diff(range(xvals))
      
      data %>%
        group_by({{groups}}) %>%
        mutate(dist = c(0, cumsum(sqrt(diff({{x}})^2 + 
                        (diff({{y}}) / ratio)^2)))) |>
        reframe(x = approx(dist, {{x}}, xout = seq(0, max(dist), spacing))$y,
                y = approx(dist, {{y}}, xout = seq(0, max(dist), spacing))$y)
    }
    

    Personally I would move the trig calculations of the y values outside of ggplot. This allows you to focus on the data and plotting of the data individually. Note your theme code can be shortened a lot using theme_void

    library(tidyverse)
    
    set.seed(42)
    
    add_lines <- purrr::map(.x = 0:10, function(.x) {
      data.frame(x = 1:100, y = 1) |>
        mutate(y = y+.x+ sqrt(runif(1, min = 0, max = 20)), 
               gr = paste0('line', .x),
               y = sqrt(x)*sin(y+pi*(x/max(x)))+cos(y)+sqrt(y)*-sin(sqrt(x)))
      }) |>
      bind_rows()
    
    ggplot(data = make_points(add_lines, x, y, gr, spacing = 2),
           mapping = aes(x = x, y = y, group = gr, color = gr)) + 
      geom_point(size = 2) +
      scale_color_viridis_d(guide = "none") + 
      theme_void() +
      theme(panel.background = element_rect(fill = "grey90"))
    

    enter image description here

    Just adjust spacing in the call to make_points to change the spacing and size inside geom_point to control the circle size. For example, changing spacing to 1.5 gives us

    enter image description here