# The following code can be run in R to create your own jitter plot.
# Parameters can be changed in settings.

library(ggplot2)
# =======================
# Settings (edit these)
# =======================
cats            <- c("A","B","C","D","E","F","G")
palette         <- c("#FFFFFF","#5f0f40", "#9a031e", "#fb8b24", "#e36414", "#0f4c5c","#FFFFFF")
bar_height      <- 10
points_per_bar  <- 500
# Motion & schedule
mode               <- "random_walk"  # "rerandomize" | "random_walk" | "ar1"
first_hold_frames  <- 10             # initial frames with zero jitter
total_frames       <- 50           # total frame count (including the hold)
sigma_margin       <- 3              # ~99.7% stay within half-gap
rw_step_sd         <- 0.02           # step SD for random_walk/ar1
ar_phi             <- 0.85           # stickiness for ar1 (0..1)
# Output & look
black_bg        <- FALSE
fps             <- 15
png_width_px    <- 750  # exported PNG size
png_height_px   <- 500
dpi             <- 300    # used only to convert pixels -> inches for ggsave
dot_size        <- .01
seed <- 123               # <- change per run if you want different results
set.seed(seed)

# =======================
# Derived / setup
# =======================
png_w_in <- png_width_px  / dpi
png_h_in <- png_height_px / dpi
bg       <- if (black_bg) "black" else "white"
run_tag  <- format(Sys.time(), "%Y%m%d_%H%M%S")
out_dir  <- paste0("run_", run_tag, "_jitter")
file_prefix <- "frame"
if (!dir.exists(out_dir)) dir.create(out_dir, recursive = TRUE)
unlink(list.files(out_dir, full.names = TRUE))
# Safe max jitter (avoid overlap)
bar_positions <- seq_along(cats)                 # 1..K
min_gap       <- min(diff(bar_positions))        # here = 1
j_max         <- (min_gap/2) / sigma_margin
# Build a triangular "breathe" schedule for the non-hold portion
sched_len <- max(0, total_frames - first_hold_frames)
make_tri_schedule <- function(n, jmax) {
  if (n <= 0) return(numeric(0))
  if (n == 1) return(jmax/2)  # trivial case
  half <- ceiling((n + 1)/2)
  up   <- seq(0, 1, length.out = half)
  down <- rev(up)[-1]
  tri  <- c(up, down)
  tri[seq_len(n)] * jmax
}
breath <- make_tri_schedule(sched_len, j_max)
jitters <- c(rep(0, first_hold_frames), breath)  # prepend the hold
# Data: base points per bar
make_eps <- function(n) {        # symmetric vector about 0
  m <- n %/% 2
  e <- rnorm(m)
  c(e, -e, if (n %% 2 == 1) 0 else NULL)
}
# Data: base points per bar  (replace your current 'base' block)
base <- do.call(rbind, lapply(seq_along(cats), function(k) {
  y_strat <- ((seq_len(points_per_bar) - 0.5) / points_per_bar) * bar_height
  data.frame(
    category = cats[k],
    base_x   = bar_positions[k],
    base_y   = y_strat
  )
}))

# State for walk/AR1
state_offsets <- rep(0, nrow(base))
# =======================
# Render frames
# =======================
for (i in seq_along(jitters)) {
  j <- jitters[i]
  
  if (mode == "rerandomize") {
    eps <- unlist(tapply(seq_len(nrow(base)), base$category,
                         function(idx) make_eps(length(idx))))
    x <- base$base_x + eps * j
    
  } else if (mode == "random_walk") {
    if (i > first_hold_frames) {
      steps <- rnorm(length(state_offsets), 0, rw_step_sd)
      state_offsets <- pmin(pmax(state_offsets + steps, -j_max), j_max)
    } else {
      state_offsets[] <- 0
    }
    x <- base$base_x + state_offsets
    
  } else if (mode == "ar1") {
    if (i > first_hold_frames) {
      noise <- rnorm(length(state_offsets), 0, rw_step_sd)
      state_offsets <- ar_phi * state_offsets + noise
      state_offsets <- pmin(pmax(state_offsets, -j_max), j_max)
    } else {
      state_offsets[] <- 0
    }
    x <- base$base_x + state_offsets
    
  } else stop("Unknown mode: use 'rerandomize', 'random_walk', or 'ar1'.")
  
  df <- transform(base, x = x, y = base_y)
  
  p <- ggplot(df, aes(x = x, y = y, color = category)) +
    geom_point(alpha = 0.7, size = dot_size) +
    scale_color_manual(values = palette) +
    coord_cartesian(ylim = c(0, bar_height)) +
    theme_void() +
    theme(
      legend.position = "none",
      panel.background = element_rect(fill = bg, color = NA),
      plot.background  = element_rect(fill = bg, color = NA)
    )
  
  ggsave(
    filename = file.path(out_dir, sprintf("%s_%03d.png", file_prefix, i)),
    plot = p, width = png_w_in, height = png_h_in, dpi = dpi, bg = bg
  )
}
message(sprintf(
  "No-overlap j_max (≈%dσ) = %.4f | first_hold_frames = %d | total_frames = %d | mode = %s",
  sigma_margin, j_max, first_hold_frames, total_frames, mode
))
# =======================
# GIF (gifski)
# =======================
if (!requireNamespace("gifski", quietly = TRUE)) install.packages("gifski")
pngs <- Sys.glob(file.path(out_dir, paste0(file_prefix, "_*.png")))
pngs <- pngs[order(pngs)]
# Mirror: forward + reverse (skip last to avoid duplicate pause)
pngs_loop <- c(pngs, rev(pngs[-length(pngs)]))
gifski::gifski(
  pngs_loop,
  gif_file = file.path(out_dir, "jitter.gif"),
  width = png_width_px, height = png_height_px,
  delay = 1/fps
)


Back to Top