{"spec_id":"dendrogram-radial","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\ndendrogram-radial: Radial Dendrogram\nLibrary: plotnine 0.15.4 | Python 3.13.13\nQuality: 80/100 | Created: 2026-05-14\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent this script (plotnine.py) from shadowing the plotnine library\n_script_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != _script_dir]\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    coord_equal,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_path,\n    geom_point,\n    geom_segment,\n    ggplot,\n    labs,\n    scale_color_manual,\n    theme,\n)\nfrom scipy.cluster.hierarchy import dendrogram, fcluster, linkage\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\"\n\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\n\n# Data: species morphological traits — 3 well-separated clades\nnp.random.seed(42)\nn_species = 30\n\ngrp_a = np.random.randn(10, 6) + np.array([3.0, -2.0, 1.0, -1.5, 2.0, -0.5])\ngrp_b = np.random.randn(10, 6) + np.array([-2.0, 3.0, -1.0, 2.0, -1.5, 1.5])\ngrp_c = np.random.randn(10, 6) + np.array([0.0, 0.0, -3.0, 0.5, -0.5, 3.0])\nfeatures = np.vstack([grp_a, grp_b, grp_c])\n\n# Hierarchical clustering\nZ = linkage(features, method=\"ward\")\ndend = dendrogram(Z, no_plot=True)\nicoord = np.array(dend[\"icoord\"])\ndcoord = np.array(dend[\"dcoord\"])\nleaves_order = dend[\"leaves\"]\n\n# Cluster assignment (3 clades)\ncluster_assign = fcluster(Z, 3, criterion=\"maxclust\")\nclade_names = {1: \"Clade I\", 2: \"Clade II\", 3: \"Clade III\"}\nclade_colors = {\"Clade I\": IMPRINT[0], \"Clade II\": IMPRINT[1], \"Clade III\": IMPRINT[2]}\n\n# Radial coordinate transforms:\n#   x position → angle (θ), evenly around the circle\n#   y height   → radius (r=1 at leaf edge, r=0 at root center)\nmax_dist = dcoord.max()\n\n\ndef x_to_theta(x):\n    return (x - 5.0) / (10.0 * n_species) * 2.0 * np.pi - np.pi / 2.0\n\n\ndef y_to_r(y):\n    return 1.0 - y / max_dist\n\n\ndef to_xy(r, theta):\n    return r * np.cos(theta), r * np.sin(theta)\n\n\n# Build radial segments (vertical branches) and circular arcs (horizontal connects)\nseg_rows = []\narc_rows = []\narc_id = 0\n\nfor xs, ys in zip(icoord, dcoord, strict=True):\n    xl, xr = xs[0], xs[3]\n    yl, yu, yr = ys[0], ys[1], ys[3]\n\n    theta_l = x_to_theta(xl)\n    theta_r = x_to_theta(xr)\n    r_l, r_u, r_r = y_to_r(yl), y_to_r(yu), y_to_r(yr)\n\n    # Left radial branch\n    x1, y1 = to_xy(r_l, theta_l)\n    x2, y2 = to_xy(r_u, theta_l)\n    seg_rows.append({\"x\": x1, \"y\": y1, \"xend\": x2, \"yend\": y2})\n\n    # Right radial branch\n    x1, y1 = to_xy(r_r, theta_r)\n    x2, y2 = to_xy(r_u, theta_r)\n    seg_rows.append({\"x\": x1, \"y\": y1, \"xend\": x2, \"yend\": y2})\n\n    # Arc at constant r_u spanning theta_l → theta_r\n    n_pts = max(12, int(abs(theta_r - theta_l) * 60))\n    for t in np.linspace(theta_l, theta_r, n_pts):\n        ax, ay = to_xy(r_u, t)\n        arc_rows.append({\"x\": ax, \"y\": ay, \"g\": arc_id})\n    arc_id += 1\n\nsegs_df = pd.DataFrame(seg_rows)\narcs_df = pd.DataFrame(arc_rows)\n\n# Leaf positions and cluster labels\nleaf_thetas = [x_to_theta(5.0 + 10.0 * i) for i in range(n_species)]\nleaf_clades = [clade_names[cluster_assign[leaves_order[i]]] for i in range(n_species)]\n\nleaf_df = pd.DataFrame(\n    {\"x\": [np.cos(t) for t in leaf_thetas], \"y\": [np.sin(t) for t in leaf_thetas], \"clade\": leaf_clades}\n)\n\n# Outer metadata ring — cluster color band just beyond the leaf tips\nring_df = pd.DataFrame(\n    {\"x\": [1.10 * np.cos(t) for t in leaf_thetas], \"y\": [1.10 * np.sin(t) for t in leaf_thetas], \"clade\": leaf_clades}\n)\n\n# Theme: square canvas, all axes hidden (circular layout needs no cartesian chrome)\nanyplot_theme = theme(\n    figure_size=(12, 12),\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_border=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_grid_major=element_blank(),\n    panel_grid_minor=element_blank(),\n    axis_title=element_blank(),\n    axis_text=element_blank(),\n    axis_ticks=element_blank(),\n    axis_line=element_blank(),\n    plot_title=element_text(color=INK, size=24),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_text=element_text(color=INK_SOFT, size=16),\n    legend_title=element_text(color=INK, size=16),\n    legend_position=(0.85, 0.12),\n)\n\n# Plot\nplot = (\n    ggplot()\n    + geom_path(data=arcs_df, mapping=aes(\"x\", \"y\", group=\"g\"), color=INK_SOFT, size=0.6)\n    + geom_segment(data=segs_df, mapping=aes(\"x\", \"y\", xend=\"xend\", yend=\"yend\"), color=INK_SOFT, size=0.6)\n    + geom_point(data=leaf_df, mapping=aes(\"x\", \"y\", color=\"clade\"), size=3, show_legend=False)\n    + geom_point(data=ring_df, mapping=aes(\"x\", \"y\", color=\"clade\"), size=5, shape=\"s\")\n    + scale_color_manual(name=\"Clade\", values=clade_colors)\n    + coord_equal()\n    + labs(title=\"dendrogram-radial · plotnine · anyplot.ai\")\n    + anyplot_theme\n)\n\n# Save\nplot.save(f\"plot-{THEME}.png\", dpi=300, verbose=False)\n"}