{"spec_id":"chord-basic","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' chord-basic: Basic Chord Diagram\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 92/100 | Created: 2026-06-17\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(tidyr)\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\"\n\n# Imprint palette — one distinct hue per region, first sector brand green\nIMPRINT_PALETTE <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n                     \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\n\n# --- Data: annual migration flows between world regions (millions) ----------\nentities <- c(\"Africa\", \"Europe\", \"Asia\", \"Oceania\", \"N. America\", \"S. America\")\n\nflows <- tribble(\n  ~source,      ~target,       ~value,\n  \"Asia\",       \"Europe\",      3.2,\n  \"Asia\",       \"N. America\",  2.8,\n  \"Asia\",       \"Oceania\",     1.1,\n  \"Africa\",     \"Europe\",      2.4,\n  \"Africa\",     \"Asia\",        1.3,\n  \"Africa\",     \"N. America\",  0.9,\n  \"Europe\",     \"N. America\",  1.6,\n  \"Europe\",     \"Asia\",        1.0,\n  \"Europe\",     \"Oceania\",     0.7,\n  \"S. America\", \"N. America\",  2.1,\n  \"S. America\", \"Europe\",      1.2,\n  \"N. America\", \"Europe\",      0.8,\n  \"N. America\", \"Asia\",        0.6,\n  \"Oceania\",    \"Asia\",        0.5,\n  \"Oceania\",    \"Europe\",      0.4,\n  \"S. America\", \"Oceania\",     0.3\n)\nflows$id <- seq_len(nrow(flows))\n\n# --- Geometry helpers (data prep only — plot stays top-level) ---------------\ndeg2rad <- function(d) d * pi / 180\n\npt_on_circle <- function(angle_deg, r) {\n  data.frame(x = r * cos(deg2rad(angle_deg)), y = r * sin(deg2rad(angle_deg)))\n}\n\narc_seq <- function(a_from, a_to, r, n = 50) {\n  a <- seq(a_from, a_to, length.out = n)\n  data.frame(x = r * cos(deg2rad(a)), y = r * sin(deg2rad(a)))\n}\n\n# Quadratic bezier with control point pinned at the centre (0, 0) — this is\n# what bends each chord toward the middle of the circle.\nbezier_to_centre <- function(p0, p1, n = 40) {\n  t <- seq(0, 1, length.out = n)\n  data.frame(\n    x = (1 - t)^2 * p0$x + t^2 * p1$x,\n    y = (1 - t)^2 * p0$y + t^2 * p1$y\n  )\n}\n\n# --- Sector layout: each region gets an arc sized by its total flow ---------\nR_IN    <- 1.00   # inner radius — chords attach here\nR_OUT   <- 1.085  # outer radius — sector band thickness\nR_LAB   <- 1.20   # region labels\nGAP_DEG <- 3      # blank gap between adjacent sectors\nN       <- length(entities)\n\nsector_total <- sapply(entities, function(e) {\n  sum(flows$value[flows$source == e]) + sum(flows$value[flows$target == e])\n})\navail <- 360 - N * GAP_DEG\n\nsector_df <- data.frame(entity = entities, sec_total = sector_total)\nsector_df$span    <- sector_df$sec_total / sum(sector_total) * avail\nsector_df$a_start <- NA_real_\nsector_df$a_end   <- NA_real_\ncursor <- 90  # start at the top, lay sectors counter-clockwise\nfor (i in seq_len(N)) {\n  sector_df$a_start[i] <- cursor\n  sector_df$a_end[i]   <- cursor + sector_df$span[i]\n  cursor <- sector_df$a_end[i] + GAP_DEG\n}\n\n# --- Flow ends: each flow occupies a slice on its source AND target sector --\nends <- bind_rows(\n  transmute(flows, entity = source, flow_id = id, role = \"out\", value, partner = target),\n  transmute(flows, entity = target, flow_id = id, role = \"in\",  value, partner = source)\n)\nends$entity  <- factor(ends$entity, levels = entities)\nends$partner <- factor(ends$partner, levels = entities)\nends <- ends %>%\n  arrange(entity, role, partner) %>%\n  group_by(entity) %>%\n  mutate(cum_end = cumsum(value), cum_start = cum_end - value) %>%\n  ungroup() %>%\n  left_join(sector_df, by = \"entity\") %>%\n  mutate(\n    ang1 = a_start + cum_start / sec_total * span,\n    ang2 = a_start + cum_end   / sec_total * span\n  )\n\n# --- Build sector band polygons (annulus segments) --------------------------\nsector_poly <- do.call(rbind, lapply(seq_len(N), function(i) {\n  outer <- arc_seq(sector_df$a_start[i], sector_df$a_end[i], R_OUT)\n  inner <- arc_seq(sector_df$a_end[i], sector_df$a_start[i], R_IN)  # reversed\n  poly  <- rbind(outer, inner)\n  poly$entity <- entities[i]\n  poly$group  <- paste0(\"sector_\", i)\n  poly\n}))\n\n# --- Build chord ribbons: source arc → bezier → target arc → bezier back ----\nribbon_df <- do.call(rbind, lapply(flows$id, function(fid) {\n  src <- ends[ends$flow_id == fid & ends$role == \"out\", ]\n  tgt <- ends[ends$flow_id == fid & ends$role == \"in\", ]\n  poly <- rbind(\n    arc_seq(src$ang1, src$ang2, R_IN, n = 20),\n    bezier_to_centre(pt_on_circle(src$ang2, R_IN), pt_on_circle(tgt$ang1, R_IN)),\n    arc_seq(tgt$ang1, tgt$ang2, R_IN, n = 20),\n    bezier_to_centre(pt_on_circle(tgt$ang2, R_IN), pt_on_circle(src$ang1, R_IN))\n  )\n  poly$group      <- paste0(\"ribbon_\", fid)\n  poly$src_entity <- as.character(src$entity)\n  poly\n}))\n# Draw widest chords first so thin ones stay visible on top\nribbon_order <- flows$id[order(flows$value, decreasing = TRUE)]\nribbon_df$group <- factor(ribbon_df$group,\n                          levels = paste0(\"ribbon_\", ribbon_order))\n\n# --- Region labels at the sector mid-angle ----------------------------------\nlabel_df <- sector_df %>%\n  mutate(\n    mid = (a_start + a_end) / 2,\n    x   = R_LAB * cos(deg2rad(mid)),\n    y   = R_LAB * sin(deg2rad(mid))\n  )\n\nfill_values <- setNames(IMPRINT_PALETTE[seq_len(N)], entities)\n\n# --- Plot -------------------------------------------------------------------\np <- ggplot() +\n  geom_polygon(\n    data = ribbon_df,\n    aes(x, y, group = group, fill = src_entity),\n    alpha = 0.55, color = NA\n  ) +\n  geom_polygon(\n    data = sector_poly,\n    aes(x, y, group = group, fill = entity),\n    color = PAGE_BG, linewidth = 0.5\n  ) +\n  geom_text(\n    data = label_df,\n    aes(x, y, label = entity),\n    color = INK, size = 5.2, fontface = \"bold\"\n  ) +\n  scale_fill_manual(values = fill_values, guide = \"none\") +\n  coord_fixed(xlim = c(-1.62, 1.62), ylim = c(-1.62, 1.62), expand = FALSE) +\n  labs(\n    title    = \"chord-basic · r · ggplot2 · anyplot.ai\",\n    subtitle = \"Annual migration flows between world regions · chord width ∝ migrants, coloured by origin\"\n  ) +\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 = 12, hjust = 0.5,\n                                    face = \"bold\", margin = margin(t = 6, b = 3)),\n    plot.subtitle    = element_text(color = INK_SOFT, size = 8,  hjust = 0.5,\n                                    margin = margin(b = 4)),\n    plot.margin      = margin(10, 10, 10, 10)\n  )\n\n# --- Save -------------------------------------------------------------------\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"}