{"spec_id":"donut-nested","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' donut-nested: Nested Donut Chart\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 88/100 | Created: 2026-08-18\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(tibble)\nlibrary(scales)\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\nIMPRINT_PALETTE <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n                     \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\n\n# --- Data: quarterly revenue ($M) by business unit and customer region --------\nrevenue <- tribble(\n  ~level_1,        ~level_2,        ~value,\n  \"Enterprise\",    \"North America\", 420,\n  \"Enterprise\",    \"Europe\",        310,\n  \"Enterprise\",    \"Asia-Pacific\",  190,\n  \"Consumer\",      \"North America\", 380,\n  \"Consumer\",      \"Europe\",        240,\n  \"Consumer\",      \"Asia-Pacific\",  175,\n  \"Consumer\",      \"Latin America\",  95,\n  \"Public Sector\", \"Federal\",       300,\n  \"Public Sector\", \"State/Local\",   180,\n  \"SMB\",           \"North America\", 260,\n  \"SMB\",           \"Europe\",        150\n)\n\nparent_totals <- revenue %>%\n  group_by(level_1) %>%\n  summarise(value = sum(value), .groups = \"drop\") %>%\n  arrange(desc(value))\n\nrevenue <- revenue %>%\n  mutate(level_1 = factor(level_1, levels = parent_totals$level_1)) %>%\n  arrange(level_1)\n\nn_parents <- nrow(parent_totals)\nparent_colors <- setNames(IMPRINT_PALETTE[seq_len(n_parents)], parent_totals$level_1)\n\n# Consistent color family per parent: same hue, lighter tint for each child\ntint_family <- function(base_hex, n) {\n  ramp <- colour_ramp(c(base_hex, \"#FFFFFF\"))\n  ramp(seq(0, 0.55, length.out = n))\n}\n\ntotal_value <- sum(revenue$value)\nlabel_threshold <- 0.06\n\n# --- Ring geometry (radial extents as x, cumulative value as y) ---------------\nHOLE        <- 1.3\nINNER_XMAX  <- HOLE + 0.8\nOUTER_XMIN  <- INNER_XMAX + 0.1\nOUTER_XMAX  <- OUTER_XMIN + 0.9\nCHILD_LABEL_X  <- OUTER_XMAX + 0.55\nPARENT_LABEL_X <- CHILD_LABEL_X + 0.85\n\nparent_df <- parent_totals %>%\n  mutate(\n    color = parent_colors[level_1],\n    ymax  = cumsum(value),\n    ymin  = lag(ymax, default = 0),\n    xmin  = HOLE,\n    xmax  = INNER_XMAX,\n    xmid  = PARENT_LABEL_X,\n    ymid  = (ymin + ymax) / 2,\n    label = sprintf(\"%s\\n%.0f%%\", level_1, 100 * value / total_value)\n  )\n\nchild_df <- revenue %>%\n  group_by(level_1) %>%\n  mutate(color = tint_family(parent_colors[[as.character(level_1[1])]], n())) %>%\n  ungroup() %>%\n  mutate(\n    ymax  = cumsum(value),\n    ymin  = lag(ymax, default = 0),\n    xmin  = OUTER_XMIN,\n    xmax  = OUTER_XMAX,\n    xmid  = CHILD_LABEL_X,\n    ymid  = (ymin + ymax) / 2,\n    share = value / total_value,\n    label = ifelse(share >= label_threshold, level_2, NA_character_),\n    legend_label = paste(level_1, level_2, sep = \" · \")\n  )\n\nlegend_df <- child_df %>% filter(is.na(label))\nlegend_colors <- setNames(legend_df$color, legend_df$legend_label)\n\n# --- Plot -----------------------------------------------------------------\np <- ggplot() +\n  geom_rect(\n    data = parent_df,\n    aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax, fill = color),\n    color = PAGE_BG, linewidth = 0.35\n  ) +\n  geom_rect(\n    data = child_df,\n    aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax, fill = color),\n    color = PAGE_BG, linewidth = 0.35\n  ) +\n  scale_fill_identity() +\n  geom_point(\n    data = legend_df, aes(x = xmid, y = ymid, color = legend_label),\n    size = 0, stroke = 0, alpha = 0, na.rm = TRUE\n  ) +\n  scale_color_manual(name = \"Smaller segments\", values = legend_colors) +\n  guides(color = guide_legend(override.aes = list(size = 6, shape = 15, alpha = 1))) +\n  geom_text(\n    data = parent_df, aes(x = xmid, y = ymid, label = label),\n    color = INK, size = 3.4, lineheight = 0.95, fontface = \"bold\", na.rm = TRUE\n  ) +\n  geom_text(\n    data = child_df, aes(x = xmid, y = ymid, label = label),\n    color = INK_SOFT, size = 2.7, na.rm = TRUE\n  ) +\n  coord_polar(theta = \"y\") +\n  scale_x_continuous(limits = c(0, PARENT_LABEL_X + 0.5)) +\n  labs(title = \"Revenue by Business Unit · donut-nested · r · ggplot2 · anyplot.ai\") +\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, margin = margin(b = 14)),\n    plot.margin        = margin(14, 14, 14, 14),\n    legend.position    = \"bottom\",\n    legend.background  = element_rect(fill = ELEVATED_BG, color = INK_SOFT),\n    legend.title       = element_text(color = INK, size = 10),\n    legend.text        = element_text(color = INK_SOFT, size = 8)\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"}