{"spec_id":"dendrogram-radial","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\ndendrogram-radial: Radial Dendrogram\nLibrary: matplotlib 3.10.9 | 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\nfrom matplotlib.lines import Line2D\nfrom scipy.cluster.hierarchy import dendrogram, fcluster, linkage\nfrom scipy.spatial.distance import pdist\n\n\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\nfamily_names = [\"Asteraceae\", \"Rosaceae\", \"Fabaceae\", \"Poaceae\", \"Lamiaceae\"]\n# Abbreviated species labels: As01-As10, Ro01-Ro10, Fa01-Fa10, Po01-Po10, La01-La10\nprefixes = [\"As\", \"Ro\", \"Fa\", \"Po\", \"La\"]\nspecies_labels = [f\"{prefixes[i // 10]}{(i % 10) + 1:02d}\" for i in range(50)]\n\n# Data: morphological trait measurements for 50 plant species across 5 families\nnp.random.seed(42)\nn_species = 50\nn_traits = 12\nk_families = 5\nspecies_per_family = 10\n\nfamily_centers = np.random.randn(k_families, n_traits) * 2.5\ndata = np.vstack([family_centers[f] + np.random.randn(species_per_family, n_traits) * 0.5 for f in range(k_families)])\n\n# Hierarchical clustering (Ward linkage)\nZ = linkage(pdist(data, metric=\"euclidean\"), method=\"ward\")\ncluster_labels = fcluster(Z, k_families, criterion=\"maxclust\")\n\n# Build subtree cluster membership for branch coloring\nnode_cluster = {i: cluster_labels[i] for i in range(n_species)}\nfor i in range(len(Z)):\n    left, right = int(Z[i, 0]), int(Z[i, 1])\n    node_id = n_species + i\n    lc = node_cluster.get(left, -1)\n    rc = node_cluster.get(right, -1)\n    node_cluster[node_id] = lc if lc == rc else -1\n\n\ndef link_color_func(node_id):\n    c = node_cluster.get(node_id, -1)\n    return IMPRINT[c - 1] if c != -1 else INK_SOFT\n\n\n# Compute dendrogram layout with colored links\ndendro = dendrogram(Z, no_plot=True, link_color_func=link_color_func)\nicoord = np.array(dendro[\"icoord\"])\ndcoord = np.array(dendro[\"dcoord\"])\nlink_colors = dendro[\"color_list\"]\nleaves = dendro[\"leaves\"]\n\nleaf_colors = [IMPRINT[cluster_labels[leaf] - 1] for leaf in leaves]\n\n# Radial coordinate helpers — inner_r pads the root away from center\nx_max = n_species * 10.0\nmax_d = float(dcoord.max())\nouter_r = 1.0\ninner_r = 0.18  # root converges here instead of the origin\n\n\ndef to_xy(x, y):\n    theta = (x / x_max) * 2 * np.pi\n    r = inner_r + (outer_r - inner_r) * (1.0 - y / max_d)\n    return r * np.cos(theta), r * np.sin(theta)\n\n\n# Plot\nfig, ax = plt.subplots(figsize=(12, 12), facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\nax.set_aspect(\"equal\")\n\n# Draw dendrogram branches\nlw = 1.2\nfor i in range(len(icoord)):\n    xs, ys = icoord[i], dcoord[i]\n    color = link_colors[i]\n\n    # Left vertical arm\n    ax0, ay0 = to_xy(xs[0], ys[0])\n    ax1, ay1 = to_xy(xs[1], ys[1])\n    ax.plot([ax0, ax1], [ay0, ay1], color=color, linewidth=lw, solid_capstyle=\"round\")\n\n    # Arc at merge height (constant radius, sweep angle)\n    theta1 = (xs[1] / x_max) * 2 * np.pi\n    theta2 = (xs[2] / x_max) * 2 * np.pi\n    r_merge = inner_r + (outer_r - inner_r) * (1.0 - ys[1] / max_d)\n    arc_angles = np.linspace(theta1, theta2, 80)\n    ax.plot(r_merge * np.cos(arc_angles), r_merge * np.sin(arc_angles), color=color, linewidth=lw)\n\n    # Right vertical arm\n    ax2, ay2 = to_xy(xs[2], ys[2])\n    ax3, ay3 = to_xy(xs[3], ys[3])\n    ax.plot([ax2, ax3], [ay2, ay3], color=color, linewidth=lw, solid_capstyle=\"round\")\n\n# Leaf markers and circumference labels\nlabel_r = outer_r + 0.07\nfor j in range(n_species):\n    theta = ((5.0 + j * 10.0) / x_max) * 2 * np.pi\n    ax.scatter(outer_r * np.cos(theta), outer_r * np.sin(theta), color=leaf_colors[j], s=110, zorder=5, linewidths=0)\n\n    theta_deg = np.degrees(theta)\n    lx = label_r * np.cos(theta)\n    ly = label_r * np.sin(theta)\n    # Flip labels on the left half so they read outward in both halves\n    if np.cos(theta) >= 0:\n        ha, rotation = \"left\", theta_deg\n    else:\n        ha, rotation = \"right\", theta_deg - 180\n    ax.text(\n        lx,\n        ly,\n        species_labels[leaves[j]],\n        fontsize=6,\n        color=INK_SOFT,\n        rotation=rotation,\n        rotation_mode=\"anchor\",\n        ha=ha,\n        va=\"center\",\n    )\n\n# Style\nax.set_xlim(-1.6, 1.6)\nax.set_ylim(-1.6, 1.6)\nax.axis(\"off\")\n\nax.set_title(\"dendrogram-radial · matplotlib · anyplot.ai\", fontsize=24, fontweight=\"medium\", color=INK, pad=16)\n\n# Legend\nlegend_elements = [\n    Line2D([0], [0], marker=\"o\", linestyle=\"None\", markerfacecolor=IMPRINT[i], markersize=14, label=family_names[i])\n    for i in range(k_families)\n]\nleg = ax.legend(\n    handles=legend_elements,\n    loc=\"lower center\",\n    bbox_to_anchor=(0.5, -0.06),\n    ncol=k_families,\n    fontsize=16,\n    frameon=True,\n    handletextpad=0.4,\n    columnspacing=1.0,\n)\nleg.get_frame().set_facecolor(ELEVATED_BG)\nleg.get_frame().set_edgecolor(INK_SOFT)\nplt.setp(leg.get_texts(), color=INK_SOFT)\n\nplt.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=300, bbox_inches=\"tight\", facecolor=PAGE_BG)\n"}