{"spec_id":"heatmap-clustered","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nheatmap-clustered: Clustered Heatmap\nLibrary: plotnine 0.15.4 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-05-09\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    coord_cartesian,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_segment,\n    geom_text,\n    geom_tile,\n    ggplot,\n    labs,\n    scale_fill_cmap,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\nfrom scipy.cluster.hierarchy import dendrogram, linkage\n\n\n# Theme tokens (see prompts/default-style-guide.md)\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\n# Data - Gene expression data (12 genes x 8 samples)\nnp.random.seed(42)\n\nn_genes = 12\nn_samples = 8\n\n# Gene names\ngene_names = [f\"Gene{i + 1}\" for i in range(n_genes)]\nsample_names = [f\"Sample{chr(65 + i)}\" for i in range(n_samples)]\n\n# Create expression data with cluster structure\n# Group 1: Genes 1-4 (co-expressed, upregulated in samples A-D)\n# Group 2: Genes 5-8 (co-expressed, upregulated in samples E-H)\n# Group 3: Genes 9-12 (variable expression)\nexpression = np.zeros((n_genes, n_samples))\n\n# Cluster 1 genes - high in first half of samples\nfor i in range(4):\n    expression[i, :4] = np.random.uniform(1.5, 3.0, 4)\n    expression[i, 4:] = np.random.uniform(-2.5, -1.0, 4)\n\n# Cluster 2 genes - high in second half of samples\nfor i in range(4, 8):\n    expression[i, :4] = np.random.uniform(-2.0, -0.5, 4)\n    expression[i, 4:] = np.random.uniform(1.0, 2.5, 4)\n\n# Cluster 3 genes - mixed pattern\nfor i in range(8, 12):\n    expression[i, :] = np.random.uniform(-1.5, 1.5, n_samples)\n    expression[i, i % n_samples] = 2.5  # One strong signal per gene\n\n# Perform hierarchical clustering on rows (genes) and columns (samples)\nrow_linkage = linkage(expression, method=\"ward\")\ncol_linkage = linkage(expression.T, method=\"ward\")\n\n# Get dendrogram coordinates\nrow_dend = dendrogram(row_linkage, no_plot=True)\ncol_dend = dendrogram(col_linkage, no_plot=True)\n\n# Reorder data according to clustering\nrow_order = row_dend[\"leaves\"]\ncol_order = col_dend[\"leaves\"]\nreordered_expr = expression[row_order, :][:, col_order]\nreordered_genes = [gene_names[i] for i in row_order]\nreordered_samples = [sample_names[i] for i in col_order]\n\n# Layout parameters\n# Main heatmap: x from 20 to 100, y from 0 to 60\n# Row dendrogram: x from 0 to 18, y from 0 to 60\n# Col dendrogram: x from 20 to 100, y from 62 to 82\nheatmap_x_start = 20\nheatmap_x_end = 100\nheatmap_y_start = 0\nheatmap_y_end = 60\ncell_width = (heatmap_x_end - heatmap_x_start) / n_samples\ncell_height = (heatmap_y_end - heatmap_y_start) / n_genes\n\n# Create heatmap tile data\ntile_data = []\nfor i, gene in enumerate(reordered_genes):\n    for j, sample in enumerate(reordered_samples):\n        x_pos = heatmap_x_start + (j + 0.5) * cell_width\n        y_pos = heatmap_y_start + (i + 0.5) * cell_height\n        tile_data.append(\n            {\"x\": x_pos, \"y\": y_pos, \"value\": round(reordered_expr[i, j], 2), \"gene\": gene, \"sample\": sample}\n        )\ntile_df = pd.DataFrame(tile_data)\n\n# Create row dendrogram segments (on the left, rotated 90 degrees)\nrow_segments = []\nmax_row_dist = max(row_linkage[:, 2]) if len(row_linkage) > 0 else 1\ndend_width = 16  # Width for row dendrogram\n\nfor xs, ys in zip(row_dend[\"icoord\"], row_dend[\"dcoord\"], strict=True):\n    # In original dendrogram: x is leaf position, y is distance\n    # We rotate: original x -> new y, original y -> new x (from right edge going left)\n    for k in range(3):\n        # Map original x positions (leaf positions) to y coordinates\n        # Original x spans 5, 15, 25, ... for n leaves\n        orig_x1, orig_x2 = xs[k], xs[k + 1]\n        orig_y1, orig_y2 = ys[k], ys[k + 1]\n\n        # Normalize original x to [0, n_genes] and map to heatmap y range\n        new_y1 = heatmap_y_start + (orig_x1 / (n_genes * 10)) * (heatmap_y_end - heatmap_y_start)\n        new_y2 = heatmap_y_start + (orig_x2 / (n_genes * 10)) * (heatmap_y_end - heatmap_y_start)\n\n        # Normalize original y (distance) and map to x (from right edge going left)\n        new_x1 = heatmap_x_start - 2 - (orig_y1 / max_row_dist) * dend_width\n        new_x2 = heatmap_x_start - 2 - (orig_y2 / max_row_dist) * dend_width\n\n        row_segments.append({\"x\": new_x1, \"xend\": new_x2, \"y\": new_y1, \"yend\": new_y2})\n\nrow_seg_df = pd.DataFrame(row_segments)\n\n# Create column dendrogram segments (on top)\ncol_segments = []\nmax_col_dist = max(col_linkage[:, 2]) if len(col_linkage) > 0 else 1\ndend_height = 16  # Height for column dendrogram\n\nfor xs, ys in zip(col_dend[\"icoord\"], col_dend[\"dcoord\"], strict=True):\n    for k in range(3):\n        orig_x1, orig_x2 = xs[k], xs[k + 1]\n        orig_y1, orig_y2 = ys[k], ys[k + 1]\n\n        # Map original x to heatmap x range\n        new_x1 = heatmap_x_start + (orig_x1 / (n_samples * 10)) * (heatmap_x_end - heatmap_x_start)\n        new_x2 = heatmap_x_start + (orig_x2 / (n_samples * 10)) * (heatmap_x_end - heatmap_x_start)\n\n        # Map original y (distance) to y above heatmap\n        new_y1 = heatmap_y_end + 3 + (orig_y1 / max_col_dist) * dend_height\n        new_y2 = heatmap_y_end + 3 + (orig_y2 / max_col_dist) * dend_height\n\n        col_segments.append({\"x\": new_x1, \"xend\": new_x2, \"y\": new_y1, \"yend\": new_y2})\n\ncol_seg_df = pd.DataFrame(col_segments)\n\n# Create row labels (gene names on the right)\nrow_labels = pd.DataFrame(\n    {\n        \"x\": [heatmap_x_end + 2] * n_genes,\n        \"y\": [heatmap_y_start + (i + 0.5) * cell_height for i in range(n_genes)],\n        \"label\": reordered_genes,\n    }\n)\n\n# Create column labels (sample names at bottom)\ncol_labels = pd.DataFrame(\n    {\n        \"x\": [heatmap_x_start + (i + 0.5) * cell_width for i in range(n_samples)],\n        \"y\": [heatmap_y_start - 2] * n_samples,\n        \"label\": reordered_samples,\n    }\n)\n\n# Build the plot\nplot = (\n    ggplot()\n    # Heatmap tiles\n    + geom_tile(aes(x=\"x\", y=\"y\", fill=\"value\"), data=tile_df, width=cell_width * 0.95, height=cell_height * 0.95)\n    # Row dendrogram\n    + geom_segment(aes(x=\"x\", xend=\"xend\", y=\"y\", yend=\"yend\"), data=row_seg_df, color=INK_SOFT, size=1.2)\n    # Column dendrogram\n    + geom_segment(aes(x=\"x\", xend=\"xend\", y=\"y\", yend=\"yend\"), data=col_seg_df, color=INK_SOFT, size=1.2)\n    # Row labels\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=row_labels, ha=\"left\", size=11, color=INK_SOFT)\n    # Column labels\n    + geom_text(\n        aes(x=\"x\", y=\"y\", label=\"label\"), data=col_labels, ha=\"center\", va=\"top\", size=11, color=INK_SOFT, angle=45\n    )\n    # Color scale (BrBG diverging colormap for data centered at zero)\n    + scale_fill_cmap(cmap_name=\"BrBG\", name=\"Expression Level\")\n    # Remove axes\n    + scale_x_continuous(breaks=[], expand=(0.02, 0))\n    + scale_y_continuous(breaks=[], expand=(0.02, 0))\n    + coord_cartesian(xlim=(-5, 120), ylim=(-8, 82))\n    + labs(x=\"\", y=\"\", title=\"heatmap-clustered · plotnine · anyplot.ai\")\n    + theme_minimal()\n    + theme(\n        figure_size=(16, 9),\n        text=element_text(size=14),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        panel_grid_major=element_blank(),\n        panel_grid_minor=element_blank(),\n        plot_title=element_text(size=24, ha=\"center\", color=INK),\n        axis_text=element_blank(),\n        axis_ticks=element_blank(),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_title=element_text(size=14, color=INK),\n        legend_text=element_text(size=12, color=INK_SOFT),\n    )\n)\n\nplot.save(f\"plot-{THEME}.png\", dpi=300, verbose=False)\n"}