{"spec_id":"sunburst-basic","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' sunburst-basic: Basic Sunburst Chart\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 91/100 | Created: 2026-07-26\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(ragg)\n\nset.seed(42)\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\"\nIMPRINT_PALETTE <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n                     \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\n\n# WCAG relative luminance, used to pick readable ink for wedge labels\nrelative_luminance <- function(hex) {\n  channel <- grDevices::col2rgb(hex) / 255\n  linear <- ifelse(channel <= 0.03928, channel / 12.92, ((channel + 0.055) / 1.055)^2.4)\n  sum(c(0.2126, 0.7152, 0.0722) * linear)\n}\n\n# Alpha-composite `hex` over `bg_hex` so luminance (and therefore ink choice)\n# reflects what actually renders on screen, not the undiluted wedge hue —\n# the outer ring is drawn at alpha = 0.55, which shifts it toward PAGE_BG.\nblend_hex <- function(hex, bg_hex, alpha) {\n  fg <- as.vector(grDevices::col2rgb(hex))\n  bg <- as.vector(grDevices::col2rgb(bg_hex))\n  blended <- alpha * fg + (1 - alpha) * bg\n  grDevices::rgb(blended[1], blended[2], blended[3], maxColorValue = 255)\n}\n\n# Angle (degrees) for a label sitting at mid-fraction `mid` of the circle,\n# flipped so text never renders upside down\nwedge_label_angle <- function(mid_frac) {\n  raw <- ((90 - 360 * mid_frac + 180) %% 360) - 180\n  case_when(\n    raw < -90 ~ raw + 180,\n    raw > 90  ~ raw - 180,\n    TRUE      ~ raw\n  )\n}\n\n# --- Data: annual budget by department and team -------------------------\nleaves <- tibble::tribble(\n  ~department,   ~team,             ~budget,\n  \"Engineering\", \"Platform\",           1450,\n  \"Engineering\", \"Infrastructure\",      980,\n  \"Engineering\", \"Data Science\",        760,\n  \"Sales\",       \"Enterprise\",         1120,\n  \"Sales\",       \"SMB\",                 640,\n  \"Marketing\",   \"Brand\",               540,\n  \"Marketing\",   \"Growth\",              480,\n  \"Marketing\",   \"Content\",             310,\n  \"Operations\",  \"Facilities\",          420,\n  \"Operations\",  \"HR\",                  380,\n  \"Operations\",  \"Finance\",             350\n)\n\ndept_order <- leaves %>%\n  group_by(department) %>%\n  summarise(total = sum(budget), .groups = \"drop\") %>%\n  arrange(desc(total)) %>%\n  mutate(\n    frac_end   = cumsum(total) / sum(total),\n    frac_start = lag(frac_end, default = 0),\n    fill_hex   = IMPRINT_PALETTE[seq_len(n())],\n    label_ink  = ifelse(sapply(fill_hex, relative_luminance) > 0.4, \"#1A1A17\", \"#FFFDF6\"),\n    department = factor(department, levels = department)\n  )\n\nleaves <- leaves %>%\n  mutate(department = factor(department, levels = levels(dept_order$department))) %>%\n  arrange(department, desc(budget)) %>%\n  left_join(dept_order, by = \"department\") %>%\n  group_by(department) %>%\n  mutate(\n    team_end   = frac_start + (cumsum(budget) / sum(budget)) * (frac_end - frac_start),\n    team_start = lag(team_end, default = first(frac_start))\n  ) %>%\n  ungroup()\n\n# Ring 1 (inner) — department totals. Label size scales down for narrow\n# wedges so long department names never overflow their own slice.\ndept_df <- dept_order %>%\n  mutate(\n    xmin  = 0.15, xmax = 0.95,\n    mid   = (frac_start + frac_end) / 2,\n    angle = wedge_label_angle(mid),\n    span  = frac_end - frac_start,\n    label_size = pmin(3.0, pmax(1.8, (span * 45) / sqrt(nchar(as.character(department)))))\n  )\n\n# Ring 2 (outer) — teams within each department. Wedges render at\n# alpha = 0.55 over PAGE_BG, so label_ink must be recomputed from that\n# composited color rather than reused from the solid inner-ring hue —\n# otherwise the ink stays white even where the light-theme blend washes\n# the wedge toward pastel.\nteam_df <- leaves %>%\n  mutate(\n    xmin       = 1.05, xmax = 1.95,\n    mid        = (team_start + team_end) / 2,\n    angle      = wedge_label_angle(mid),\n    span       = team_end - team_start,\n    show_label = span > 0.035,\n    blended    = mapply(blend_hex, fill_hex, MoreArgs = list(bg_hex = PAGE_BG, alpha = 0.55)),\n    label_ink  = ifelse(sapply(blended, relative_luminance) > 0.4, \"#1A1A17\", \"#FFFDF6\")\n  )\n\nplot_title <- \"sunburst-basic · r · ggplot2 · anyplot.ai\"\ntitle_size <- round(12 * min(1, 67 / nchar(plot_title)))\n\n# --- Plot -----------------------------------------------------------------\np <- ggplot() +\n  geom_rect(\n    data = dept_df,\n    aes(xmin = xmin, xmax = xmax, ymin = frac_start, ymax = frac_end, fill = department),\n    color = PAGE_BG, linewidth = 1.0\n  ) +\n  geom_rect(\n    data = team_df,\n    aes(xmin = xmin, xmax = xmax, ymin = team_start, ymax = team_end, fill = department),\n    color = PAGE_BG, linewidth = 1.0, alpha = 0.55\n  ) +\n  geom_text(\n    data = dept_df,\n    aes(x = (xmin + xmax) / 2, y = mid, label = department, angle = angle,\n        color = label_ink, size = label_size),\n    fontface = \"bold\"\n  ) +\n  scale_size_identity() +\n  geom_text(\n    data = filter(team_df, show_label),\n    aes(x = (xmin + xmax) / 2, y = mid, label = team, angle = angle, color = label_ink),\n    size = 2.6\n  ) +\n  scale_fill_manual(\n    values = setNames(dept_order$fill_hex, as.character(dept_order$department)),\n    name   = \"Department\"\n  ) +\n  scale_color_identity() +\n  coord_polar(theta = \"y\") +\n  scale_x_continuous(limits = c(0, 2), expand = c(0, 0)) +\n  scale_y_continuous(limits = c(0, 1), expand = c(0, 0)) +\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_size, face = \"bold\",\n                                      hjust = 0.5, margin = margin(b = 10)),\n    legend.title      = element_text(color = INK, size = 10),\n    legend.text       = element_text(color = INK_SOFT, size = 8),\n    legend.background = element_rect(fill = PAGE_BG, color = NA),\n    legend.key        = element_rect(fill = PAGE_BG, color = NA),\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"}