{"spec_id":"dendrogram-radial","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' dendrogram-radial: Radial Dendrogram\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 89/100 | Created: 2026-09-05\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(tidyr)\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\"\nINK       <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT  <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\nINK_MUTED <- if (THEME == \"light\") \"#6B6A63\" else \"#A8A79F\"\n\n# Imprint palette (see prompts/default-style-guide.md \"Categorical Palette\")\nIMPRINT_PALETTE <- c(\n  \"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n  \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"\n)\n\n# --- Data: synthetic gene expression profiles across 4 latent groups --------\nn_genes   <- 32\nn_samples <- 8\nn_groups  <- 4\n\ngroup_id     <- rep(1:n_groups, length.out = n_genes)\ngroup_means  <- matrix(rnorm(n_groups * n_samples, mean = 0, sd = 4), nrow = n_groups)\nexpr <- group_means[group_id, ] + matrix(rnorm(n_genes * n_samples, mean = 0, sd = 1.3), nrow = n_genes)\nrownames(expr) <- sprintf(\"Gene_%02d\", seq_len(n_genes))\n\nhc           <- hclust(dist(expr), method = \"average\")\nleaf_cluster <- cutree(hc, k = n_groups)\nn            <- n_genes\nmax_height   <- max(hc$height)\n\n# --- Walk the merge tree: assign circumferential position + branch purity ---\nleaf_pos <- match(seq_len(n), hc$order)\n\nnode_x       <- numeric(n - 1)\nnode_cluster <- rep(NA_integer_, n - 1)\n\nget_x       <- function(idx) if (idx < 0) leaf_pos[-idx] else node_x[idx]\nget_height  <- function(idx) if (idx < 0) 0 else hc$height[idx]\nget_cluster <- function(idx) if (idx < 0) leaf_cluster[[-idx]] else node_cluster[idx]\n\nradial_rows <- vector(\"list\", 2 * (n - 1))\narc_rows    <- vector(\"list\", n - 1)\n\nfor (k in seq_len(n - 1)) {\n  left  <- hc$merge[k, 1]\n  right <- hc$merge[k, 2]\n\n  x_left  <- get_x(left)\n  x_right <- get_x(right)\n  node_x[k] <- (x_left + x_right) / 2\n\n  h_left  <- get_height(left)\n  h_right <- get_height(right)\n  h_here  <- hc$height[k]\n\n  c_left  <- get_cluster(left)\n  c_right <- get_cluster(right)\n  node_cluster[k] <- if (!is.na(c_left) && !is.na(c_right) && c_left == c_right) c_left else NA_integer_\n\n  radial_rows[[2 * k - 1]] <- tibble(\n    x = x_left, r_start = max_height - h_left, r_end = max_height - h_here, cluster = c_left\n  )\n  radial_rows[[2 * k]] <- tibble(\n    x = x_right, r_start = max_height - h_right, r_end = max_height - h_here, cluster = c_right\n  )\n\n  n_pts  <- max(10, round(abs(x_right - x_left) * 3))\n  arc_rows[[k]] <- tibble(\n    x = seq(x_left, x_right, length.out = n_pts), r = max_height - h_here,\n    cluster = node_cluster[k], seg = k\n  )\n}\n\nlabel_with_cluster <- function(df) mutate(df, cluster_label = ifelse(is.na(cluster), \"mixed\", as.character(cluster)))\n\nradial_df <- label_with_cluster(bind_rows(radial_rows))\narc_df    <- label_with_cluster(bind_rows(arc_rows))\n\n# --- Leaf labels, rotated tangentially around the circumference -------------\nleaf_labels <- tibble(x = leaf_pos, label = rownames(expr)) %>%\n  mutate(\n    r     = max_height * 1.06,\n    angle = 90 - 360 * (x - 0.5) / n,\n    hjust = ifelse(angle < -90, 1, 0),\n    angle = ifelse(angle < -90, angle + 180, angle)\n  )\n\n# --- Concentric distance-scale rings (quantitative reading of merge height) --\nring_heights <- pretty(c(0, max_height), n = 4)\nring_heights <- ring_heights[ring_heights > 0 & ring_heights < max_height]\n\nring_df <- crossing(h = ring_heights, x = seq(0.5, n + 0.5, length.out = 200)) %>%\n  mutate(r = max_height - h)\n\nring_labels <- tibble(h = ring_heights) %>%\n  mutate(x = 0.85, r = max_height - h, label = sprintf(\"%.0f\", h))\n\n# --- Colors -------------------------------------------------------------------\ncluster_levels <- c(as.character(seq_len(n_groups)), \"mixed\")\nCLUSTER_COLORS <- setNames(c(IMPRINT_PALETTE[seq_len(n_groups)], INK_MUTED), cluster_levels)\nCLUSTER_LABELS <- setNames(c(sprintf(\"Cluster %d\", seq_len(n_groups)), \"Mixed branch\"), cluster_levels)\n\n# --- Plot ---------------------------------------------------------------------\np <- ggplot() +\n  geom_path(\n    data = ring_df, aes(x = x, y = r, group = h),\n    color = INK, alpha = 0.18, linewidth = 0.3\n  ) +\n  geom_text(\n    data = ring_labels, aes(x = x, y = r, label = label),\n    color = INK_MUTED, size = 2.6, hjust = 1, vjust = -0.4\n  ) +\n  geom_segment(\n    data = radial_df,\n    aes(x = x, xend = x, y = r_start, yend = r_end, color = cluster_label),\n    linewidth = 0.7\n  ) +\n  geom_path(\n    data = arc_df,\n    aes(x = x, y = r, group = seg, color = cluster_label),\n    linewidth = 0.7\n  ) +\n  geom_text(\n    data = leaf_labels,\n    aes(x = x, y = r, label = label, angle = angle, hjust = hjust),\n    color = INK_SOFT, size = 2.7\n  ) +\n  scale_color_manual(values = CLUSTER_COLORS, labels = CLUSTER_LABELS, name = NULL) +\n  scale_x_continuous(limits = c(0.5, n + 0.5), expand = c(0, 0)) +\n  scale_y_continuous(limits = c(0, max_height * 1.22), expand = c(0, 0)) +\n  coord_polar(theta = \"x\", start = 0) +\n  labs(\n    title    = \"dendrogram-radial · r · ggplot2 · anyplot.ai\",\n    subtitle = \"Concentric rings mark merge distance (height); root at center\"\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 = 12, hjust = 0.5, margin = margin(b = 4)),\n    plot.subtitle    = element_text(color = INK_MUTED, size = 8, hjust = 0.5, margin = margin(b = 10)),\n    legend.position  = \"bottom\",\n    legend.text      = element_text(color = INK_SOFT, size = 8),\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"}