{"spec_id":"heatmap-clustered","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nheatmap-clustered: Clustered Heatmap\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 94/100 | Updated: 2026-05-09\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\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\"\n\n# Data - Gene expression analysis with 20 genes and 15 samples\nnp.random.seed(42)\n\nn_genes = 20\nn_samples = 15\n\n# Create gene and sample labels\ngene_labels = [f\"Gene_{i + 1:02d}\" for i in range(n_genes)]\nsample_labels = [f\"Sample_{i + 1:02d}\" for i in range(n_samples)]\n\n# Generate realistic gene expression data with natural clusters\nbase_expression = np.random.randn(n_genes, n_samples)\n\n# Cluster 1: Genes 0-4 (upregulated in samples 0-4)\nbase_expression[0:5, 0:5] += 2.5\nbase_expression[0:5, 10:15] -= 1.5\n\n# Cluster 2: Genes 5-9 (upregulated in samples 5-9)\nbase_expression[5:10, 5:10] += 2.0\nbase_expression[5:10, 0:3] -= 1.0\n\n# Cluster 3: Genes 10-14 (upregulated in samples 10-14)\nbase_expression[10:15, 10:15] += 2.5\nbase_expression[10:15, 5:8] -= 1.5\n\n# Cluster 4: Genes 15-19 (varied pattern)\nbase_expression[15:20, 0:5] += 1.5\nbase_expression[15:20, 5:10] -= 2.0\nbase_expression[15:20, 10:15] += 1.0\n\nexpression_data = base_expression\n\n# Perform hierarchical clustering\nrow_linkage = linkage(pdist(expression_data, metric=\"euclidean\"), method=\"ward\")\ncol_linkage = linkage(pdist(expression_data.T, metric=\"euclidean\"), method=\"ward\")\n\n# Get dendrogram ordering\nrow_dendro = dendrogram(row_linkage, no_plot=True)\ncol_dendro = dendrogram(col_linkage, no_plot=True)\n\nrow_order = row_dendro[\"leaves\"]\ncol_order = col_dendro[\"leaves\"]\n\n# Reorder data and labels\nclustered_data = expression_data[row_order, :][:, col_order]\nordered_gene_labels = [gene_labels[i] for i in row_order]\nordered_sample_labels = [sample_labels[i] for i in col_order]\n\n# Create DataFrame for heatmap\nheatmap_data = []\nfor i, gene in enumerate(ordered_gene_labels):\n    for j, sample in enumerate(ordered_sample_labels):\n        heatmap_data.append(\n            {\"Gene\": gene, \"Sample\": sample, \"Expression\": clustered_data[i, j], \"row_idx\": i, \"col_idx\": j}\n        )\n\ndf_heatmap = pd.DataFrame(heatmap_data)\n\n# Create row dendrogram data\nrow_lines = []\nfor i in range(len(row_dendro[\"icoord\"])):\n    xs = row_dendro[\"icoord\"][i]\n    ys = row_dendro[\"dcoord\"][i]\n    for j in range(3):\n        row_lines.append({\"x\": ys[j], \"y\": xs[j], \"x2\": ys[j + 1], \"y2\": xs[j + 1], \"group\": i})\nrow_dendro_lines = pd.DataFrame(row_lines)\n\n# Flip y-coordinates to match heatmap orientation\nmax_y = row_dendro_lines[\"y\"].max()\nrow_dendro_lines[\"y\"] = max_y - row_dendro_lines[\"y\"]\nrow_dendro_lines[\"y2\"] = max_y - row_dendro_lines[\"y2\"]\n\n# Normalize row dendrogram coordinates\nrow_max_height = row_dendro_lines[\"x\"].max()\nrow_dendro_lines[\"x\"] = row_max_height - row_dendro_lines[\"x\"]\nrow_dendro_lines[\"x2\"] = row_max_height - row_dendro_lines[\"x2\"]\nrow_dendro_lines[\"y\"] = row_dendro_lines[\"y\"] / 10 - 0.5\nrow_dendro_lines[\"y2\"] = row_dendro_lines[\"y2\"] / 10 - 0.5\n\n# Create column dendrogram data\ncol_lines = []\nfor i in range(len(col_dendro[\"icoord\"])):\n    xs = col_dendro[\"icoord\"][i]\n    ys = col_dendro[\"dcoord\"][i]\n    for j in range(3):\n        col_lines.append({\"x\": xs[j], \"y\": ys[j], \"x2\": xs[j + 1], \"y2\": ys[j + 1], \"group\": i})\ncol_dendro_lines = pd.DataFrame(col_lines)\n\n# Normalize column dendrogram coordinates\ncol_dendro_lines[\"x\"] = col_dendro_lines[\"x\"] / 10 - 0.5\ncol_dendro_lines[\"x2\"] = col_dendro_lines[\"x2\"] / 10 - 0.5\n\n# Row dendrogram chart (left side)\nrow_dendro_chart = (\n    alt.Chart(row_dendro_lines)\n    .mark_rule(strokeWidth=1.5, color=INK_SOFT)\n    .encode(\n        x=alt.X(\"x:Q\", axis=None, scale=alt.Scale(domain=[0, row_max_height])),\n        y=alt.Y(\"y:Q\", axis=None, scale=alt.Scale(domain=[-0.5, n_genes - 0.5])),\n        x2=\"x2:Q\",\n        y2=\"y2:Q\",\n    )\n    .properties(width=150, height=600)\n)\n\n# Column dendrogram chart (top)\ncol_max_height = col_dendro_lines[\"y\"].max()\ncol_dendro_chart = (\n    alt.Chart(col_dendro_lines)\n    .mark_rule(strokeWidth=1.5, color=INK_SOFT)\n    .encode(\n        x=alt.X(\"x:Q\", axis=None, scale=alt.Scale(domain=[-0.5, n_samples - 0.5])),\n        y=alt.Y(\"y:Q\", axis=None, scale=alt.Scale(domain=[0, col_max_height])),\n        x2=\"x2:Q\",\n        y2=\"y2:Q\",\n    )\n    .properties(width=800, height=120)\n)\n\n# Heatmap with interactive features\nheatmap = (\n    alt.Chart(df_heatmap)\n    .mark_rect()\n    .encode(\n        x=alt.X(\n            \"Sample:N\",\n            sort=ordered_sample_labels,\n            axis=alt.Axis(\n                title=\"Samples\", labelFontSize=18, titleFontSize=22, labelAngle=-45, labelColor=INK_SOFT, titleColor=INK\n            ),\n        ),\n        y=alt.Y(\n            \"Gene:N\",\n            sort=ordered_gene_labels,\n            axis=alt.Axis(title=\"Genes\", labelFontSize=18, titleFontSize=22, labelColor=INK_SOFT, titleColor=INK),\n        ),\n        color=alt.Color(\n            \"Expression:Q\",\n            scale=alt.Scale(scheme=\"brownbluegreen\", domainMid=0),\n            legend=alt.Legend(\n                title=\"Expression\",\n                titleFontSize=18,\n                labelFontSize=16,\n                gradientLength=400,\n                fillColor=ELEVATED_BG,\n                strokeColor=INK_SOFT,\n            ),\n        ),\n        tooltip=[\"Gene:N\", \"Sample:N\", alt.Tooltip(\"Expression:Q\", format=\".2f\")],\n    )\n    .properties(width=800, height=600)\n    .interactive()\n)\n\n# Empty corner space for layout\nempty_corner = (\n    alt.Chart(pd.DataFrame({\"x\": [0]}))\n    .mark_point(opacity=0)\n    .encode(x=alt.X(\"x:Q\", axis=None), y=alt.Y(\"x:Q\", axis=None))\n    .properties(width=150, height=120)\n)\n\n# Combine charts into clustered heatmap layout\ntop_row = alt.hconcat(empty_corner, col_dendro_chart, spacing=0)\nbottom_row = alt.hconcat(row_dendro_chart, heatmap, spacing=0)\n\nchart = (\n    alt.vconcat(top_row, bottom_row, spacing=0)\n    .properties(\n        title=alt.Title(\"heatmap-clustered · altair · anyplot.ai\", fontSize=28, anchor=\"middle\", color=INK),\n        background=PAGE_BG,\n    )\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n    .configure_concat(spacing=0)\n    .configure_axis(domainColor=INK_SOFT, gridColor=INK, gridOpacity=0.10, labelColor=INK_SOFT, titleColor=INK)\n)\n\n# Save outputs\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}