{"spec_id":"bubble-packed","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' bubble-packed: Basic Packed Bubble Chart\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 86/100 | Created: 2026-05-29\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(tibble)\nlibrary(ragg)\n\nset.seed(42)\n\n# Theme tokens — Imprint palette (see prompts/default-style-guide.md)\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\"\nLABEL_COLOR <- if (THEME == \"light\") INK else \"#FFFFFF\"\n\n# Imprint categorical palette — first series always #009E73\nIMPRINT_PALETTE <- c(\n  \"#009E73\",  # 1 brand green  — Consumer\n  \"#C475FD\",  # 2 lavender     — Infrastructure\n  \"#4467A3\",  # 3 blue         — Enterprise\n  \"#BD8233\",  # 4 ochre        — Fintech\n  \"#AE3030\"   # 5 matte red    — Emerging\n)\n\n# Data: global technology market segments (estimated revenue $B)\nsector_levels <- c(\"Consumer\", \"Infrastructure\", \"Enterprise\", \"Fintech\", \"Emerging\")\n\nsegments <- tibble(\n  label = c(\n    \"E-Commerce\", \"Digital Ads\", \"Social\",     \"Mobile\",   \"Streaming\",\n    \"Cloud\",      \"Semicon.\",    \"IoT\",        \"5G\",\n    \"Enterprise\", \"AI / ML\",    \"Analytics\",  \"Cyber\",    \"Dev Tools\", \"SaaS\",\n    \"Payments\",   \"Fintech\",    \"InsurTech\",\n    \"AR / VR\",    \"EdTech\",     \"HealthTech\"\n  ),\n  value = c(\n    680, 720, 550, 490, 380,\n    580, 510, 200, 260,\n    460, 620, 420, 240, 160, 380,\n    310, 290, 170,\n    110, 130, 180\n  ),\n  sector = c(\n    \"Consumer\", \"Consumer\", \"Consumer\", \"Consumer\", \"Consumer\",\n    \"Infrastructure\", \"Infrastructure\", \"Infrastructure\", \"Infrastructure\",\n    \"Enterprise\", \"Enterprise\", \"Enterprise\", \"Enterprise\", \"Enterprise\", \"Enterprise\",\n    \"Fintech\", \"Fintech\", \"Fintech\",\n    \"Emerging\", \"Emerging\", \"Emerging\"\n  )\n) |>\n  mutate(\n    sector = factor(sector, levels = sector_levels),\n    radius = sqrt(value) * 0.017  # area-proportional radii\n  )\n\nn_segs <- nrow(segments)\n\n# Force-directed circle packing with sector clustering\npack_circles <- function(r, sectors = NULL, n_iter = 1500) {\n  n  <- length(r)\n  ga <- pi * (3 - sqrt(5))  # golden angle\n\n  # Golden-angle spiral initial placement\n  px <- numeric(n)\n  py <- numeric(n)\n  for (i in seq_len(n)) {\n    ang   <- i * ga\n    rs    <- sqrt(i) * mean(r) * 2.0\n    px[i] <- rs * cos(ang)\n    py[i] <- rs * sin(ang)\n  }\n\n  # Iterative overlap resolution with centroid + sector gravity\n  for (it in seq_len(n_iter)) {\n    any_mv <- FALSE\n    for (i in seq_len(n - 1)) {\n      for (j in (i + 1):n) {\n        dx   <- px[i] - px[j]\n        dy   <- py[i] - py[j]\n        d    <- sqrt(dx * dx + dy * dy)\n        dmin <- r[i] + r[j] + 2e-3\n\n        if (d < dmin) {\n          any_mv <- TRUE\n          if (d < 1e-9) {\n            px[j] <- px[j] + 1e-3 * runif(1, -1, 1)\n            next\n          }\n          push   <- (dmin - d) * 0.51\n          ux     <- dx / d\n          uy     <- dy / d\n          px[i]  <- px[i] + push * ux\n          py[i]  <- py[i] + push * uy\n          px[j]  <- px[j] - push * ux\n          py[j]  <- py[j] - push * uy\n        }\n      }\n    }\n\n    # Global centroid gravity (tightens pack)\n    g  <- 0.04\n    cx <- mean(px)\n    cy <- mean(py)\n    px <- px + g * (cx - px)\n    py <- py + g * (cy - py)\n\n    # Sector centroid gravity (clusters same-sector circles)\n    if (!is.null(sectors)) {\n      gs <- 0.012\n      for (s in unique(sectors)) {\n        idx  <- which(sectors == s)\n        scx  <- mean(px[idx])\n        scy  <- mean(py[idx])\n        px[idx] <- px[idx] + gs * (scx - px[idx])\n        py[idx] <- py[idx] + gs * (scy - py[idx])\n      }\n    }\n\n    if (!any_mv) break\n  }\n\n  # Final tightening: nudge each circle toward nearest non-overlapping neighbor\n  for (pass in seq_len(8)) {\n    for (i in seq_len(n)) {\n      best_gap <- Inf\n      best_j   <- NA_integer_\n      for (j in seq_len(n)) {\n        if (j == i) next\n        d   <- sqrt((px[i] - px[j])^2 + (py[i] - py[j])^2)\n        gap <- d - r[i] - r[j]\n        if (gap > 0 && gap < best_gap) { best_gap <- gap; best_j <- j }\n      }\n      if (!is.na(best_j) && best_gap > 1e-3) {\n        dx   <- px[best_j] - px[i]\n        dy   <- py[best_j] - py[i]\n        d    <- sqrt(dx^2 + dy^2)\n        step <- min(best_gap * 0.45, 0.04)\n        px[i] <- px[i] + step * dx / d\n        py[i] <- py[i] + step * dy / d\n      }\n    }\n    # Re-resolve any overlaps introduced by tightening\n    for (i in seq_len(n - 1)) {\n      for (j in (i + 1):n) {\n        dx   <- px[i] - px[j]; dy <- py[i] - py[j]\n        d    <- sqrt(dx^2 + dy^2); dmin <- r[i] + r[j] + 2e-3\n        if (d < dmin && d > 1e-9) {\n          push  <- (dmin - d) * 0.51; ux <- dx / d; uy <- dy / d\n          px[i] <- px[i] + push * ux; py[i] <- py[i] + push * uy\n          px[j] <- px[j] - push * ux; py[j] <- py[j] - push * uy\n        }\n      }\n    }\n  }\n\n  list(x = px - mean(px), y = py - mean(py))\n}\n\npos      <- pack_circles(segments$radius, sectors = as.character(segments$sector))\nsegments <- segments |> mutate(x = pos$x, y = pos$y)\n\n# Circle polygons for geom_polygon\nn_pts        <- 72\ncircle_polys <- bind_rows(lapply(seq_len(n_segs), function(i) {\n  s     <- segments[i, ]\n  theta <- seq(0, 2 * pi, length.out = n_pts + 1)\n  tibble(\n    x      = s$x + s$radius * cos(theta),\n    y      = s$y + s$radius * sin(theta),\n    id     = i,\n    sector = as.character(s$sector)\n  )\n})) |>\n  mutate(sector = factor(sector, levels = sector_levels))\n\n# Labels only for circles large enough to hold text\nlabel_df <- segments |> filter(radius >= 0.24)\n\n# Title — square canvas, no scaling needed (42 chars < 67)\nplot_title <- \"bubble-packed · r · ggplot2 · anyplot.ai\"\nn_chr      <- nchar(plot_title)\ntitle_sz   <- if (n_chr > 67) round(12 * 67 / n_chr) else 12\n\np <- ggplot() +\n  geom_polygon(\n    data      = circle_polys,\n    aes(x = x, y = y, group = id, fill = sector),\n    alpha     = 0.85,\n    color     = PAGE_BG,\n    linewidth = 0.35\n  ) +\n  geom_text(\n    data     = label_df,\n    aes(x = x, y = y, label = label),\n    color    = LABEL_COLOR,\n    size     = 2.2,\n    fontface = \"bold\"\n  ) +\n  scale_fill_manual(\n    values = setNames(IMPRINT_PALETTE, sector_levels),\n    name   = \"Sector\"\n  ) +\n  coord_equal() +\n  labs(title = plot_title) +\n  theme_void(base_size = 8) +\n  theme(\n    plot.background   = element_rect(fill = PAGE_BG,     color = NA),\n    panel.background  = element_rect(fill = PAGE_BG,     color = NA),\n    plot.title          = element_text(\n      color  = INK,\n      size   = title_sz,\n      hjust  = 0.5,\n      face   = \"plain\",\n      margin = margin(t = 14, b = 10)\n    ),\n    plot.title.position = \"plot\",\n    legend.position   = \"right\",\n    legend.text       = element_text(color = INK_SOFT, size = 8),\n    legend.title      = element_text(color = INK,      size = 10),\n    legend.background = element_rect(fill = ELEVATED_BG, color = INK_SOFT, linewidth = 0.3),\n    legend.margin     = margin(6, 8, 6, 8),\n    plot.margin       = margin(16, 16, 16, 16)\n  )\n\n# Save — square canvas: 6 x 6 in @ 400 dpi = 2400 x 2400 px\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"}