{"spec_id":"circos-basic","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' circos-basic: Circos Plot\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 93/100 | Created: 2026-09-04\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(tidyr)\nlibrary(ragg)\n\n# --- Theme tokens ------------------------------------------------------------\nTHEME    <- Sys.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG  <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\nINK      <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\n\nIMPRINT_PALETTE <- c(\n    \"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n    \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"\n)\n\n# --- Data: inter-regional trade flows (USD billions) -------------------------\nsegment_order <- c(\n    \"North America\", \"Latin America\", \"Europe\",\n    \"Africa\", \"South Asia\", \"East Asia\", \"Oceania\"\n)\n\nflows <- tibble::tribble(\n    ~source,          ~target,          ~value,\n    \"North America\",  \"Europe\",          62,\n    \"North America\",  \"East Asia\",       78,\n    \"North America\",  \"Latin America\",   45,\n    \"Europe\",         \"North America\",   58,\n    \"Europe\",         \"East Asia\",       71,\n    \"Europe\",         \"Africa\",          33,\n    \"Europe\",         \"South Asia\",      29,\n    \"East Asia\",      \"North America\",   84,\n    \"East Asia\",      \"Europe\",          67,\n    \"East Asia\",      \"South Asia\",      52,\n    \"East Asia\",      \"Oceania\",         38,\n    \"South Asia\",     \"East Asia\",       41,\n    \"South Asia\",     \"Europe\",          22,\n    \"Latin America\",  \"North America\",   36,\n    \"Latin America\",  \"Europe\",          19,\n    \"Africa\",         \"Europe\",          27,\n    \"Africa\",         \"East Asia\",       31,\n    \"Oceania\",        \"East Asia\",       44\n) %>%\n    mutate(conn_id = row_number())\n\n# --- Geometry helpers (circular layout has no native ggplot2 support) -------\ndeg2rad <- function(deg) deg * pi / 180\n\narc_path <- function(a0, a1, r, n = 30) {\n    angles <- seq(a0, a1, length.out = n)\n    data.frame(x = r * cos(deg2rad(angles)), y = r * sin(deg2rad(angles)))\n}\n\nbezier_path <- function(p0, p1, control = c(0, 0), n = 30) {\n    t <- seq(0, 1, length.out = n)\n    data.frame(\n        x = (1 - t)^2 * p0[1] + 2 * (1 - t) * t * control[1] + t^2 * p1[1],\n        y = (1 - t)^2 * p0[2] + 2 * (1 - t) * t * control[2] + t^2 * p1[2]\n    )\n}\n\npolar_xy <- function(angle_deg, r) c(r * cos(deg2rad(angle_deg)), r * sin(deg2rad(angle_deg)))\n\n# --- Segment sizing: arc length proportional to total flow touching it ------\ngap_deg <- 4\nn_seg   <- length(segment_order)\n\ntouches <- bind_rows(\n    flows %>% transmute(segment = source, conn_id, value, role = \"source\"),\n    flows %>% transmute(segment = target, conn_id, value, role = \"target\")\n)\n\nsegment_totals <- touches %>%\n    group_by(segment) %>%\n    summarise(total = sum(value), .groups = \"drop\") %>%\n    arrange(match(segment, segment_order)) %>%\n    mutate(\n        span = (360 - gap_deg * n_seg) * total / sum(total),\n        color = IMPRINT_PALETTE[seq_len(n())]\n    )\n\nstart_angle <- 90\nangle_starts <- numeric(n_seg)\nangle_ends   <- numeric(n_seg)\ncur <- start_angle\nfor (i in seq_len(n_seg)) {\n    angle_starts[i] <- cur\n    angle_ends[i]   <- cur - segment_totals$span[i]\n    cur <- angle_ends[i] - gap_deg\n}\nsegment_totals$angle_start <- angle_starts\nsegment_totals$angle_end   <- angle_ends\n\n# --- Trade balance per region (net exports − imports) → diverging track ----\nbalance <- flows %>%\n    group_by(segment = source) %>%\n    summarise(exports = sum(value), .groups = \"drop\") %>%\n    full_join(\n        flows %>% group_by(segment = target) %>% summarise(imports = sum(value), .groups = \"drop\"),\n        by = \"segment\"\n    ) %>%\n    mutate(across(c(exports, imports), ~ replace_na(.x, 0)), net = exports - imports)\n\ndiv_ramp <- grDevices::colorRamp(c(\"#AE3030\", PAGE_BG, \"#4467A3\"), space = \"Lab\")\nnet_to_color <- function(net, max_abs) {\n    t <- pmin(pmax((net / max_abs + 1) / 2, 0), 1)\n    rgb_mat <- div_ramp(t)\n    grDevices::rgb(rgb_mat[, 1], rgb_mat[, 2], rgb_mat[, 3], maxColorValue = 255)\n}\n\nsegment_totals <- segment_totals %>%\n    left_join(balance %>% select(segment, net), by = \"segment\") %>%\n    mutate(track_color = net_to_color(net, max(abs(net))))\n\n# --- Sub-arcs: divide each segment's span among its individual connections -\ntouches <- touches %>%\n    left_join(segment_totals %>% select(segment, total, angle_start, angle_end), by = \"segment\") %>%\n    arrange(segment, conn_id) %>%\n    group_by(segment) %>%\n    mutate(cum_before = cumsum(value) - value, cum_after = cumsum(value)) %>%\n    ungroup() %>%\n    mutate(\n        seg_span = angle_start - angle_end,\n        a0 = angle_start - seg_span * cum_before / total,\n        a1 = angle_start - seg_span * cum_after / total\n    )\n\nribbon_edges <- flows %>%\n    left_join(\n        touches %>% filter(role == \"source\") %>% transmute(conn_id, a0_s = a0, a1_s = a1),\n        by = \"conn_id\"\n    ) %>%\n    left_join(\n        touches %>% filter(role == \"target\") %>% transmute(conn_id, a0_t = a0, a1_t = a1),\n        by = \"conn_id\"\n    )\n\n# --- Ribbon polygons: two arcs (at the sub-arc radius) joined by two Bezier\n#     curves that sweep through the circle's center, the classic chord shape -\nr_ribbon <- 1.00\n\nribbon_polygon <- function(row, r) {\n    p_s1 <- polar_xy(row$a1_s, r)\n    p_t0 <- polar_xy(row$a0_t, r)\n    p_t1 <- polar_xy(row$a1_t, r)\n    p_s0 <- polar_xy(row$a0_s, r)\n\n    bind_rows(\n        arc_path(row$a0_s, row$a1_s, r),\n        bezier_path(p_s1, p_t0),\n        arc_path(row$a0_t, row$a1_t, r),\n        bezier_path(p_t1, p_s0)\n    ) %>% mutate(conn_id = row$draw_order, source = row$source)\n}\n\n# Draw larger flows first and smaller ones last (on top) so thin connections\n# stay traceable through the dense crossing zone near the center.\nribbon_edges <- ribbon_edges %>%\n    arrange(desc(value)) %>%\n    mutate(draw_order = row_number())\n\nribbons <- bind_rows(lapply(seq_len(nrow(ribbon_edges)), function(i) {\n    ribbon_polygon(ribbon_edges[i, ], r_ribbon)\n})) %>%\n    left_join(segment_totals %>% select(segment, color), by = c(\"source\" = \"segment\"))\n\n# --- Track ring: constant-width band colored by net trade balance ----------\nr_track_inner <- 1.06\nr_track_outer <- 1.16\n\ntrack_polys <- bind_rows(lapply(seq_len(nrow(segment_totals)), function(i) {\n    seg <- segment_totals[i, ]\n    bind_rows(\n        arc_path(seg$angle_start, seg$angle_end, r_track_outer),\n        arc_path(seg$angle_end, seg$angle_start, r_track_inner)\n    ) %>% mutate(segment = seg$segment, color = seg$track_color)\n}))\n\n# --- Outer ring: one colored sector per segment -----------------------------\nr_seg_inner <- 1.25\nr_seg_outer <- 1.35\n\nsegment_polys <- bind_rows(lapply(seq_len(nrow(segment_totals)), function(i) {\n    seg <- segment_totals[i, ]\n    bind_rows(\n        arc_path(seg$angle_start, seg$angle_end, r_seg_outer),\n        arc_path(seg$angle_end, seg$angle_start, r_seg_inner)\n    ) %>% mutate(segment = seg$segment, color = seg$color)\n}))\n\n# --- Segment labels, flipped on the circle's left half for legibility ------\nsegment_totals <- segment_totals %>%\n    mutate(\n        mid_angle   = (angle_start + angle_end) / 2,\n        norm_angle  = ((mid_angle %% 360) + 360) %% 360,\n        label_r     = r_seg_outer + 0.10,\n        label_x     = label_r * cos(deg2rad(mid_angle)),\n        label_y     = label_r * sin(deg2rad(mid_angle)),\n        flipped     = norm_angle > 90 & norm_angle < 270,\n        label_angle = ifelse(flipped, mid_angle + 180, mid_angle),\n        label_hjust = ifelse(flipped, 1, 0)\n    )\n\n# --- Plot --------------------------------------------------------------------\ntitle_text <- \"Inter-regional Trade Flows · circos-basic · r · ggplot2 · anyplot.ai\"\ntitle_size <- max(8, round(12 * min(1, 67 / nchar(title_text))))\n\np <- ggplot() +\n    geom_polygon(\n        data = ribbons, aes(x, y, group = conn_id, fill = color),\n        color = NA, alpha = 0.42\n    ) +\n    geom_polygon(\n        data = track_polys, aes(x, y, group = segment, fill = color),\n        color = PAGE_BG, linewidth = 0.4\n    ) +\n    geom_polygon(\n        data = segment_polys, aes(x, y, group = segment, fill = color),\n        color = PAGE_BG, linewidth = 0.6\n    ) +\n    geom_text(\n        data = segment_totals,\n        aes(x = label_x, y = label_y, label = segment, angle = label_angle, hjust = label_hjust),\n        size = 3.2, color = INK, vjust = 0.5\n    ) +\n    scale_fill_identity() +\n    coord_fixed(xlim = c(-1.8, 1.8), ylim = c(-1.8, 1.8), clip = \"off\") +\n    labs(\n        title   = title_text,\n        caption = \"Inner ring: net trade balance (red = deficit, cream = balanced, blue = surplus)\"\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 = title_size, hjust = 0.5, margin = margin(b = 10)),\n        plot.caption     = element_text(color = INK_SOFT, size = 7, hjust = 0.5, margin = margin(t = 10)),\n        plot.margin      = margin(t = 15, r = 15, b = 15, l = 15)\n    )\n\n# --- Save (PNG, both themes) -------------------------------------------------\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"}