{"spec_id":"dendrogram-radial","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\ndendrogram-radial: Radial Dendrogram\nLibrary: seaborn 0.13.2 | Python 3.13.13\nQuality: 81/100 | Created: 2026-05-14\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport seaborn as sns\nfrom scipy.cluster.hierarchy import dendrogram, linkage\nfrom scipy.spatial.distance import pdist\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\"]\n\nsns.set_theme(style=\"white\", rc={\"figure.facecolor\": PAGE_BG, \"axes.facecolor\": PAGE_BG, \"text.color\": INK})\n\n# Data — gene expression profiles for 30 genes across 5 biological pathways\nnp.random.seed(42)\nN_CLUSTERS = 5\nN_PER_CLUSTER = 6\nN_SAMPLES = N_CLUSTERS * N_PER_CLUSTER\nN_FEATURES = 20\n\npathway_labels = [\n    [\"BRCA1\", \"BRCA2\", \"RAD51\", \"PALB2\", \"CHEK2\", \"ATM\"],\n    [\"EGFR\", \"KRAS\", \"BRAF\", \"MEK1\", \"ERK2\", \"AKT1\"],\n    [\"CDK4\", \"CDK6\", \"CCND1\", \"RB1\", \"E2F1\", \"CDKN2A\"],\n    [\"BCL2\", \"BAX\", \"CASP3\", \"CASP9\", \"TP53\", \"PUMA\"],\n    [\"VEGFA\", \"HIF1A\", \"PDGFR\", \"FGF2\", \"ANG1\", \"MMP9\"],\n]\npathway_names = [\"DNA Repair\", \"MAPK Pathway\", \"Cell Cycle\", \"Apoptosis\", \"Angiogenesis\"]\ngene_labels = [gene for pathway in pathway_labels for gene in pathway]\ntrue_clusters = np.repeat(np.arange(N_CLUSTERS), N_PER_CLUSTER)\n\ncluster_centers = np.random.randn(N_CLUSTERS, N_FEATURES) * 4\nX = np.vstack([cluster_centers[c] + np.random.randn(N_PER_CLUSTER, N_FEATURES) * 0.7 for c in range(N_CLUSTERS)])\n\n# Hierarchical clustering\nZ = linkage(pdist(X, metric=\"euclidean\"), method=\"ward\")\n# Use true_clusters for coloring so pathway labels stay consistent with colors\nflat = true_clusters\n\n# Dendrogram layout (Cartesian, no render)\ndend = dendrogram(Z, no_plot=True, labels=gene_labels)\nicoord = np.array(dend[\"icoord\"])\ndcoord = np.array(dend[\"dcoord\"])\nleaf_order = dend[\"leaves\"]\n\nx_max = 10.0 * N_SAMPLES\ny_max = float(dcoord.max())\n\nR_LEAF = 0.78  # outer radius (leaves)\nR_ROOT = 0.12  # inner radius (root)\n\n\ndef x_to_angle(x):\n    return x / x_max * 2 * np.pi\n\n\ndef y_to_radius(y):\n    # y=0 (leaves) -> R_LEAF outer; y=y_max (root) -> R_ROOT inner\n    return R_LEAF - (y / y_max) * (R_LEAF - R_ROOT)\n\n\n# Subtree coloring: pure cluster -> Okabe-Ito color; mixed -> neutral\ndef get_subtree_leaves(node_id):\n    if node_id < N_SAMPLES:\n        return [int(node_id)]\n    idx = int(node_id) - N_SAMPLES\n    return get_subtree_leaves(int(Z[idx, 0])) + get_subtree_leaves(int(Z[idx, 1]))\n\n\ndef get_node_color(node_id):\n    leaves_in = get_subtree_leaves(node_id)\n    clusters_in = {flat[leaf] for leaf in leaves_in}\n    if len(clusters_in) == 1:\n        return IMPRINT[clusters_in.pop()]\n    return INK_SOFT\n\n\n# Map merge height -> Z row index (Ward distances are unique)\ndist_to_z_idx = {round(float(Z[i, 2]), 6): i for i in range(len(Z))}\n\n# Plot — square canvas for symmetric layout\nfig = plt.figure(figsize=(12, 12), facecolor=PAGE_BG)\nax = fig.add_subplot(111, projection=\"polar\")\nax.set_facecolor(PAGE_BG)\nax.set_theta_zero_location(\"N\")\nax.set_theta_direction(-1)  # clockwise so layout reads naturally top -> right -> bottom\n\n# Draw each U-shape as radial lines + arc\nLW = 2.2\nfor ic, dc in zip(icoord, dcoord, strict=False):\n    merge_height = round(float(dc[1]), 6)\n    z_idx = dist_to_z_idx.get(merge_height)\n\n    if z_idx is not None:\n        left_id = int(Z[z_idx, 0])\n        right_id = int(Z[z_idx, 1])\n        color_left = get_node_color(left_id)\n        color_right = get_node_color(right_id)\n        color_arc = get_node_color(N_SAMPLES + z_idx)\n    else:\n        color_left = color_right = color_arc = INK_SOFT\n\n    theta_left = x_to_angle(ic[0])\n    theta_right = x_to_angle(ic[3])\n    r_left_bot = y_to_radius(dc[0])\n    r_right_bot = y_to_radius(dc[3])\n    r_top = y_to_radius(dc[1])\n\n    # Left radial bar (child to merge height)\n    ax.plot([theta_left, theta_left], [r_left_bot, r_top], color=color_left, lw=LW, solid_capstyle=\"round\")\n    # Right radial bar\n    ax.plot([theta_right, theta_right], [r_right_bot, r_top], color=color_right, lw=LW, solid_capstyle=\"round\")\n    # Arc connecting children at merge radius\n    n_pts = max(3, int(abs(theta_right - theta_left) * 40 / np.pi))\n    theta_arc = np.linspace(theta_left, theta_right, n_pts)\n    ax.plot(theta_arc, np.full(n_pts, r_top), color=color_arc, lw=LW, solid_capstyle=\"round\")\n\n# Leaf dots and gene labels around circumference\nR_DOT = R_LEAF\nR_LABEL = R_LEAF + 0.055\n\nfor disp_pos, leaf_idx in enumerate(leaf_order):\n    angle = x_to_angle(10 * disp_pos + 5)\n    cluster_id = flat[leaf_idx]\n    color = IMPRINT[cluster_id]\n\n    # Colored dot at leaf\n    ax.scatter([angle], [R_DOT], color=color, s=55, zorder=5, linewidths=0)\n\n    # Gene name — rotate to read outward (radially away from center)\n    angle_deg = np.degrees(angle) % 360\n    if angle_deg < 180:\n        ha = \"left\"\n        rot = 90 - angle_deg\n    else:\n        ha = \"right\"\n        rot = 270 - angle_deg\n    ax.text(\n        angle,\n        R_LABEL,\n        gene_labels[leaf_idx],\n        ha=ha,\n        va=\"center\",\n        fontsize=9.5,\n        color=color,\n        fontweight=\"medium\",\n        rotation=rot,\n        rotation_mode=\"anchor\",\n    )\n\n# Pathway labels at outer ring — positioned at angular centroid of each cluster\nR_PATHWAY = R_LEAF + 0.28\nfor c in range(N_CLUSTERS):\n    positions = [disp_pos for disp_pos, leaf_idx in enumerate(leaf_order) if flat[leaf_idx] == c]\n    if not positions:\n        continue\n    center_angle = x_to_angle(10 * np.mean(positions) + 5)\n    angle_deg = np.degrees(center_angle) % 360\n    if angle_deg < 180:\n        rot = 90 - angle_deg\n    else:\n        rot = 270 - angle_deg\n    ax.text(\n        center_angle,\n        R_PATHWAY,\n        pathway_names[c],\n        ha=\"center\",\n        va=\"center\",\n        fontsize=13,\n        fontweight=\"bold\",\n        color=IMPRINT[c],\n        rotation=rot,\n        rotation_mode=\"anchor\",\n    )\n\n# Polar axis cleanup\nax.set_ylim(0, 1.35)\nax.set_xticks([])\nax.set_yticks([])\nax.spines[\"polar\"].set_visible(False)\nax.grid(False)\n\n# Title and subtitle\nfig.text(\n    0.5,\n    0.97,\n    \"dendrogram-radial · seaborn · anyplot.ai\",\n    ha=\"center\",\n    va=\"top\",\n    fontsize=22,\n    fontweight=\"medium\",\n    color=INK,\n)\nfig.text(\n    0.5,\n    0.025,\n    \"Ward linkage · 30 genes × 5 biological pathways · Euclidean distance\",\n    ha=\"center\",\n    va=\"bottom\",\n    fontsize=13,\n    color=INK_MUTED,\n)\n\nplt.tight_layout(rect=[0, 0.04, 1, 0.96])\nplt.savefig(f\"plot-{THEME}.png\", dpi=300, bbox_inches=\"tight\", facecolor=PAGE_BG)\n"}