{"spec_id":"dendrogram-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\ndendrogram-basic: Basic Dendrogram\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 87/100 | Updated: 2026-06-18\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_hline,\n    geom_point,\n    geom_segment,\n    geom_text,\n    ggplot,\n    ggsize,\n    labs,\n    layer_tooltips,\n    scale_color_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_void,\n)\nfrom lets_plot.export import ggsave\nfrom scipy.cluster.hierarchy import linkage\nfrom sklearn.datasets import load_iris\n\n\nLetsPlot.setup_html()\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\n# Imprint palette — positions 1-4 in canonical order\nCLUSTER_COLORS = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\nCLUSTER_BREAKS = [\"Setosa\", \"Versicolor\", \"Virginica\", \"Cross-cluster\"]\n\n# Data — Iris flower measurements (15 samples, 3 species)\niris = load_iris()\nnp.random.seed(42)\nindices = np.sort(np.concatenate([np.random.choice(np.where(iris.target == k)[0], 5, replace=False) for k in range(3)]))\nfeatures = iris.data[indices]\nspecies_names = [\"Setosa\", \"Versicolor\", \"Virginica\"]\nlabels = [f\"{species_names[iris.target[i]][:3]}-{j + 1}\" for j, i in enumerate(indices)]\n\n# Hierarchical clustering (Ward's method)\nlinkage_matrix = linkage(features, method=\"ward\")\n\n# Build dendrogram segment coordinates from linkage matrix\nn = len(labels)\nleaf_positions = {i: float(i) for i in range(n)}\nnode_heights = dict.fromkeys(range(n), 0.0)\nsegments = []\n\n# Color threshold at 70% of max distance — splits into 3 major species clusters\nmax_dist = linkage_matrix[:, 2].max()\ncolor_threshold = 0.7 * max_dist\n\nprefix_to_species = {\"Set\": \"Setosa\", \"Ver\": \"Versicolor\", \"Vir\": \"Virginica\"}\nnode_cluster = {i: prefix_to_species[labels[i].split(\"-\")[0]] for i in range(n)}\n\nfor i, (left, right, dist, _) in enumerate(linkage_matrix):\n    left, right = int(left), int(right)\n    new_node = n + i\n\n    left_pos = leaf_positions[left]\n    right_pos = leaf_positions[right]\n    leaf_positions[new_node] = (left_pos + right_pos) / 2\n    node_heights[new_node] = dist\n\n    left_cl, right_cl = node_cluster[left], node_cluster[right]\n    node_cluster[new_node] = left_cl if left_cl == right_cl else \"Cross-cluster\"\n    cluster_label = node_cluster[new_node] if dist < color_threshold else \"Cross-cluster\"\n\n    lh, rh = node_heights[left], node_heights[right]\n    for seg in [(left_pos, lh, left_pos, dist), (right_pos, rh, right_pos, dist), (left_pos, dist, right_pos, dist)]:\n        segments.append(\n            {\n                \"x\": seg[0],\n                \"y\": seg[1],\n                \"xend\": seg[2],\n                \"yend\": seg[3],\n                \"cluster\": cluster_label,\n                \"merge_dist\": round(dist, 2),\n            }\n        )\n\nsegment_df = pd.DataFrame(segments)\n\nleaf_data = [\n    {\"x\": leaf_positions[i], \"y\": 0, \"label\": labels[i], \"cluster\": prefix_to_species[labels[i].split(\"-\")[0]]}\n    for i in range(n)\n]\nlabel_df = pd.DataFrame(leaf_data)\n\nplot = (\n    ggplot()\n    + geom_segment(\n        aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\", color=\"cluster\"),\n        data=segment_df,\n        size=1.5,\n        tooltips=layer_tooltips().title(\"@cluster\").line(\"Merge distance|@merge_dist\").min_width(180),\n    )\n    + geom_point(\n        aes(x=\"x\", y=\"y\", color=\"cluster\"),\n        data=label_df,\n        size=2.5,\n        shape=16,\n        show_legend=False,\n        tooltips=layer_tooltips().title(\"@cluster\").line(\"Sample|@label\"),\n    )\n    + geom_text(\n        aes(x=\"x\", y=\"y\", label=\"label\", color=\"cluster\"),\n        data=label_df.assign(y=-max_dist * 0.05),\n        angle=45,\n        hjust=1,\n        vjust=1,\n        size=4,\n        family=\"monospace\",\n        show_legend=False,\n    )\n    + geom_hline(yintercept=color_threshold, linetype=\"dashed\", color=INK_MUTED, size=0.8)\n    + geom_text(\n        aes(x=\"x\", y=\"y\", label=\"label\"),\n        data=pd.DataFrame(\n            [{\"x\": n - 1.8, \"y\": color_threshold + max_dist * 0.03, \"label\": f\"threshold = {color_threshold:.1f}\"}]\n        ),\n        size=3.5,\n        color=INK_MUTED,\n        hjust=1,\n        family=\"monospace\",\n    )\n    + scale_color_manual(values=CLUSTER_COLORS, breaks=CLUSTER_BREAKS, name=\"Cluster\")\n    + scale_x_continuous(expand=[0.06, 0.02])\n    + scale_y_continuous(\n        name=\"Ward Linkage Distance\",\n        limits=[-max_dist * 0.14, max_dist * 1.07],\n        expand=[0, 0],\n        breaks=[0, 2, 4, 6, 8, 10, 12],\n    )\n    + labs(x=\"\", title=\"dendrogram-basic · python · letsplot · anyplot.ai\")\n    + theme_void()\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        plot_title=element_text(size=16, face=\"bold\", color=INK),\n        axis_title_y=element_text(size=12, color=INK_SOFT, margin=[0, 8, 0, 0]),\n        axis_text_y=element_text(size=10, color=INK_SOFT),\n        axis_text_x=element_blank(),\n        axis_ticks_x=element_blank(),\n        axis_ticks_y=element_line(size=0.4, color=INK_SOFT),\n        axis_line_y=element_line(size=0.6, color=INK_SOFT),\n        panel_grid_major_y=element_line(size=0.3, color=INK_MUTED),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_text=element_text(size=10, color=INK_SOFT),\n        legend_title=element_text(size=10, color=INK),\n        plot_margin=[25, 15, 20, 10],\n    )\n    + ggsize(800, 450)\n)\n\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}