{"spec_id":"circlepacking-basic","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' circlepacking-basic: Circle Packing Chart\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 90/100 | Created: 2026-09-02\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: a repository's directory tree, sized by file weight (KB) -----\ncategories <- c(\"src\", \"tests\", \"docs\", \"assets\", \"config\", \"scripts\")\ncategory_labels <- c(\n  src     = \"Source Code\",\n  tests   = \"Tests\",\n  docs    = \"Documentation\",\n  assets  = \"Assets\",\n  config  = \"Config\",\n  scripts = \"Scripts\"\n)\nmeanlog_by_cat <- c(src = 5.2, tests = 4.3, docs = 4.0, assets = 6.1, config = 3.0, scripts = 4.1)\nsdlog_by_cat   <- c(src = 0.55, tests = 0.6, docs = 0.7, assets = 0.9, config = 0.5, scripts = 0.6)\nfile_pool <- list(\n  src     = c(\"router\", \"auth\", \"database\", \"utils\", \"server\", \"api\", \"cache\", \"logger\", \"parser\", \"scheduler\"),\n  tests   = c(\"test_auth\", \"test_api\", \"test_database\", \"test_utils\", \"test_router\", \"test_cache\"),\n  docs    = c(\"readme\", \"architecture\", \"api-guide\", \"changelog\", \"contributing\", \"faq\"),\n  assets  = c(\"logo\", \"banner\", \"icon-set\", \"hero-image\", \"background\", \"favicon\"),\n  config  = c(\"app\", \"database\", \"logging\", \"ci\", \"docker\", \"eslint\"),\n  scripts = c(\"deploy\", \"build\", \"migrate\", \"seed\", \"backup\", \"release\")\n)\nfile_ext <- c(src = \".R\", tests = \".R\", docs = \".md\", assets = \".png\", config = \".yaml\", scripts = \".sh\")\n\nleaves <- bind_rows(lapply(categories, function(category_name) {\n  n_files <- sample(6:11, 1)\n  names   <- sample(file_pool[[category_name]], n_files, replace = TRUE)\n  tibble(\n    category = category_name,\n    id       = paste0(category_name, \"_\", sprintf(\"%02d\", seq_len(n_files))),\n    parent   = category_name,\n    label    = paste0(names, file_ext[[category_name]]),\n    value    = round(rlnorm(n_files, meanlog = meanlog_by_cat[[category_name]], sdlog = sdlog_by_cat[[category_name]]), 1)\n  )\n}))\n\n# --- Circle packing: force-relaxation algorithm -------------------------\n# Places circles of given radii tangent to their neighbours without\n# overlap (spec: \"Pack circles efficiently using force simulation\"),\n# then recenters the cluster on its own centroid.\npack_children <- function(radii, iterations = 500) {\n  n <- length(radii)\n  if (n == 1) {\n    return(tibble(x = 0, y = 0, r = radii))\n  }\n\n  ord      <- order(radii, decreasing = TRUE)\n  r_sorted <- radii[ord]\n  padding  <- 0.03 * mean(r_sorted)\n\n  golden_angle <- pi * (3 - sqrt(5))\n  idx    <- seq_len(n)\n  spread <- sum(r_sorted) * 0.5\n  x <- spread * sqrt(idx / n) * cos(idx * golden_angle)\n  y <- spread * sqrt(idx / n) * sin(idx * golden_angle)\n\n  for (iter in seq_len(iterations)) {\n    for (i in seq_len(n - 1)) {\n      for (j in seq(i + 1, n)) {\n        dx   <- x[j] - x[i]\n        dy   <- y[j] - y[i]\n        dist <- sqrt(dx^2 + dy^2)\n        min_dist <- r_sorted[i] + r_sorted[j] + padding\n        if (dist < min_dist) {\n          if (dist < 1e-9) {\n            dx <- runif(1, -1, 1); dy <- runif(1, -1, 1)\n            dist <- sqrt(dx^2 + dy^2)\n          }\n          overlap <- (min_dist - dist) / 2\n          ux <- dx / dist; uy <- dy / dist\n          x[i] <- x[i] - ux * overlap; y[i] <- y[i] - uy * overlap\n          x[j] <- x[j] + ux * overlap; y[j] <- y[j] + uy * overlap\n        }\n      }\n    }\n    x <- x - mean(x) * 0.02\n    y <- y - mean(y) * 0.02\n  }\n\n  x <- x - mean(x)\n  y <- y - mean(y)\n  tibble(x = x[order(ord)], y = y[order(ord)], r = radii)\n}\n\ncircle_points <- function(id, cx, cy, r, n = 72) {\n  theta <- seq(0, 2 * pi, length.out = n)\n  tibble(id = id, x = cx + r * cos(theta), y = cy + r * sin(theta))\n}\n\n# Level 1: pack leaf circles inside each category (area-accurate radius)\nleaves_packed <- leaves %>%\n  group_by(category) %>%\n  group_modify(~ bind_cols(.x, pack_children(sqrt(.x$value / pi)))) %>%\n  ungroup()\n\n# Level 2: derive each category's outer radius from its packed children,\n# then pack the categories inside the root with the same algorithm\ncategory_stats <- leaves_packed %>%\n  group_by(category) %>%\n  summarise(enclose_r = max(sqrt(x^2 + y^2) + r), .groups = \"drop\") %>%\n  mutate(\n    draw_r = enclose_r * 1.15,\n    label  = category_labels[category]\n  ) %>%\n  arrange(match(category, categories))\n\ncat_positions <- pack_children(category_stats$draw_r)\ncategory_stats$x_cat <- cat_positions$x\ncategory_stats$y_cat <- cat_positions$y\n\nroot_r <- max(sqrt(category_stats$x_cat^2 + category_stats$y_cat^2) + category_stats$draw_r) * 1.14\n\nleaves_final <- leaves_packed %>%\n  left_join(category_stats %>% select(category, x_cat, y_cat), by = \"category\") %>%\n  mutate(abs_x = x + x_cat, abs_y = y + y_cat)\n\n# --- Polygons for rendering ----------------------------------------------\nroot_poly <- circle_points(\"root\", 0, 0, root_r)\n\ncat_polys <- bind_rows(Map(\n  circle_points,\n  id = category_stats$category, cx = category_stats$x_cat,\n  cy = category_stats$y_cat, r = category_stats$draw_r\n))\n\nleaf_polys <- bind_rows(Map(\n  circle_points,\n  id = leaves_final$id, cx = leaves_final$abs_x,\n  cy = leaves_final$abs_y, r = leaves_final$r\n)) %>%\n  left_join(leaves_final %>% select(id, category), by = \"id\")\n\ncat_labels <- category_stats %>%\n  mutate(\n    dist_from_root = pmax(sqrt(x_cat^2 + y_cat^2), 1e-6),\n    dir_x          = x_cat / dist_from_root,\n    dir_y          = y_cat / dist_from_root,\n    label_x        = x_cat + dir_x * (draw_r + root_r * 0.035),\n    label_y        = y_cat + dir_y * (draw_r + root_r * 0.035),\n    label_size     = 2.6 + 1.1 * (draw_r / max(draw_r)),\n    # anchor the text edge (not its center) to the outward point, so the\n    # whole label clears the circle boundary regardless of approach angle\n    label_hjust = case_when(\n      abs(dir_x) < abs(dir_y) ~ 0.5,\n      dir_x >= 0               ~ 0,\n      TRUE                     ~ 1\n    ),\n    label_vjust = case_when(\n      abs(dir_y) <= abs(dir_x) ~ 0.5,\n      dir_y >= 0                ~ 0,\n      TRUE                      ~ 1\n    )\n  )\n\n# --- Title (fontsize scales with title length) ---------------------------\nplot_title <- \"circlepacking-basic · r · ggplot2 · anyplot.ai\"\ntitle_n <- nchar(plot_title)\ntitle_ratio <- if (title_n > 67) 67 / title_n else 1.0\ntitle_fontsize <- max(8, round(12 * title_ratio))\n\nfill_values <- setNames(IMPRINT_PALETTE[seq_along(categories)], categories)\n\n# Text color per category chosen for contrast against that category's fill\n# (data-tied, so — like the fill colors themselves — it does not flip with\n# THEME).\npal_rgb        <- col2rgb(fill_values)\npal_luma       <- (0.299 * pal_rgb[\"red\", ] + 0.587 * pal_rgb[\"green\", ] + 0.114 * pal_rgb[\"blue\", ]) / 255\nleaf_label_ink <- ifelse(pal_luma < 0.5, \"#F5F3EC\", \"#1A1A17\")\n\n# --- The largest leaf in each category, kept only for the 2 biggest ------\n# categories, so every labeled circle is actually large enough to hold its\n# text: a small category's own biggest leaf can still be too tiny to read.\nleaf_top <- leaves_final %>%\n  group_by(category) %>%\n  slice_max(order_by = r, n = 1, with_ties = FALSE) %>%\n  ungroup() %>%\n  slice_max(order_by = r, n = 2, with_ties = FALSE) %>%\n  mutate(\n    label_size = pmin(3.2, pmax(1.8, 1.8 + 1.8 * (r / max(r)))),\n    text_color = leaf_label_ink[category]\n  )\n\nfocal_id   <- leaf_top$id[which.max(leaf_top$r)]\nfocal_ring <- leaf_polys %>% filter(id == focal_id)\n\n# --- Plot ------------------------------------------------------------------\np <- ggplot() +\n  geom_polygon(data = root_poly, aes(x, y), fill = ELEVATED_BG, color = NA) +\n  geom_polygon(\n    data = cat_polys, aes(x, y, group = id),\n    fill = NA, color = INK_SOFT, linewidth = 0.45, alpha = 0.7\n  ) +\n  geom_polygon(\n    data = leaf_polys, aes(x, y, group = id, fill = category),\n    color = PAGE_BG, linewidth = 0.3, alpha = 0.9\n  ) +\n  geom_polygon(\n    data = focal_ring, aes(x, y, group = id),\n    fill = NA, color = INK, linewidth = 0.9\n  ) +\n  geom_text(\n    data = cat_labels,\n    aes(label_x, label_y, label = label, size = label_size, hjust = label_hjust, vjust = label_vjust),\n    color = INK, fontface = \"bold\"\n  ) +\n  geom_text(\n    data = leaf_top,\n    aes(abs_x, abs_y, label = label, size = label_size, color = text_color),\n    fontface = \"bold\"\n  ) +\n  scale_fill_manual(values = fill_values, guide = \"none\") +\n  scale_color_identity() +\n  scale_size_identity(guide = \"none\") +\n  coord_fixed(\n    xlim = c(-root_r * 1.12, root_r * 1.12),\n    ylim = c(-root_r * 1.12, root_r * 1.12),\n    expand = FALSE\n  ) +\n  labs(title = plot_title) +\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_fontsize, face = \"bold\", hjust = 0.5, margin = margin(b = 12)),\n    plot.margin      = margin(14, 14, 14, 14)\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"}