{"spec_id":"spiral-timeseries","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' spiral-timeseries: Spiral Time Series Chart\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 89/100 | Created: 2026-09-09\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(tibble)\nlibrary(ragg)\n\nset.seed(42)\n\n# --- Theme tokens -------------------------------------------------------\nTHEME       <- Sys.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG     <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\nELEVATED_BG <- if (THEME == \"light\") \"#FFFDF6\" else \"#242420\"\nINK         <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT    <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\nIMPRINT_PALETTE <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n                     \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\n\n# --- Data: daily average temperature, one revolution per year -----------\ndates <- seq(as.Date(\"2021-01-01\"), as.Date(\"2024-12-31\"), by = \"day\")\nyears <- as.integer(format(dates, \"%Y\"))\ndoy   <- as.integer(format(dates, \"%j\"))\n\ndf <- tibble(\n  date       = dates,\n  year       = years,\n  year_index = years - min(years),\n  doy_frac   = (doy - 1) / 365.25\n) %>%\n  mutate(\n    temperature = 10 - 15 * cos(2 * pi * doy_frac) +\n      0.05 * year_index + rnorm(n(), mean = 0, sd = 1.8)\n  )\n\n# --- Archimedean spiral geometry (native polar coordinates) -------------\n# `doy_frac` (0-1) is the theta aesthetic (one lap per cycle); `radius`\n# grows by a constant GAP each lap, which is what coord_radial() renders\n# as an Archimedean spiral instead of a plain circle.\nR0       <- 0.6                       # inner radius (spiral start)\nGAP      <- 1.0                       # radial distance added per full cycle\nN_CYCLES <- max(df$year_index) + 1\nr_max    <- R0 + GAP * N_CYCLES\n\ndf <- df %>%\n  mutate(radius = R0 + GAP * (year_index + doy_frac))\n\nlatest_year_index <- max(df$year_index)\ndf_earlier <- df %>% filter(year_index < latest_year_index)\ndf_latest  <- df %>% filter(year_index == latest_year_index)\n\n# Concentric rings mark each full-cycle boundary\nring_theta <- seq(0, 1, length.out = 200)\ncircles <- do.call(rbind, lapply(0:N_CYCLES, function(i) {\n  tibble(x = ring_theta, y = R0 + GAP * i, ring = i)\n}))\n\n# Radial spokes mark month subdivisions within each cycle\nmonth_frac <- (0:11) / 12\nspokes <- tibble(x = month_frac, xend = month_frac, y = R0, yend = r_max)\n\n# One label per revolution, at the top of the spiral, for orientation\ncycle_labels <- df %>%\n  distinct(year, year_index) %>%\n  mutate(x = 0, y = R0 + GAP * year_index + 0.2)\n\n# --- Title (length-scaled fontsize; see prompts/plot-generator.md) ------\nplot_title <- \"spiral-timeseries · r · ggplot2 · anyplot.ai\"\ntitle_size <- max(8, round(12 * min(1, 67 / nchar(plot_title))))\n\n# --- Plot -----------------------------------------------------------------\np <- ggplot() +\n  geom_path(data = circles, aes(x, y, group = ring),\n            color = INK, linewidth = 0.3, alpha = 0.18) +\n  geom_segment(data = spokes, aes(x = x, y = y, xend = xend, yend = yend),\n               color = INK, linewidth = 0.3, alpha = 0.18) +\n  geom_path(data = df_earlier, aes(x = doy_frac, y = radius, color = temperature, group = 1),\n            linewidth = 1.1, lineend = \"round\") +\n  geom_path(data = df_latest, aes(x = doy_frac, y = radius, color = temperature, group = 1),\n            linewidth = 2.2, lineend = \"round\") +\n  geom_text(data = cycle_labels, aes(x, y, label = year),\n            color = INK_SOFT, size = 3.2) +\n  scale_x_continuous(limits = c(0, 1)) +\n  scale_y_continuous(limits = c(0, r_max)) +\n  scale_color_gradient(low = IMPRINT_PALETTE[1], high = IMPRINT_PALETTE[3],\n                        name = \"Avg temp (°C)\") +\n  coord_radial(theta = \"x\", start = 0, end = 2 * pi,\n               inner.radius = R0 / r_max, expand = FALSE) +\n  labs(title = plot_title,\n       subtitle = \"Bold outer ring = most recent cycle; outer rings trend warmer\") +\n  theme_void(base_size = 8) +\n  theme(\n    plot.background   = element_rect(fill = PAGE_BG, color = PAGE_BG),\n    panel.background  = element_rect(fill = PAGE_BG, color = NA),\n    plot.title        = element_text(color = INK, size = title_size,\n                                      hjust = 0.5, margin = margin(b = 4)),\n    plot.subtitle     = element_text(color = INK_SOFT, size = 8,\n                                      hjust = 0.5, margin = margin(b = 10)),\n    legend.position    = \"right\",\n    legend.background  = element_rect(fill = ELEVATED_BG, color = NA),\n    legend.title       = element_text(color = INK, size = 10),\n    legend.text        = element_text(color = INK_SOFT, size = 8),\n    legend.box.spacing = unit(4, \"pt\"),\n    plot.margin        = margin(8, 4, 2, 4)\n  ) +\n  guides(color = guide_colorbar(barwidth = unit(0.5, \"cm\"),\n                                 barheight = unit(6, \"cm\")))\n\n# --- Save (square canvas: symmetric radial layout) -------------------------\nggsave(\n  filename = sprintf(\"plot-%s.png\", THEME),\n  plot     = p,\n  device   = ragg::agg_png,\n  width    = 6,\n  height   = 6,\n  units    = \"in\",\n  dpi      = 400\n)\n"}