{"spec_id":"ma-differential-expression","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nma-differential-expression: MA Plot for Differential Expression\nLibrary: altair 6.2.1 | Python 3.13.14\nQuality: 86/100 | Updated: 2026-06-21\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\n\n# Theme-adaptive chrome tokens (Imprint palette)\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 categorical palette — semantic assignment for DE context:\n# upregulated (gain) → brand green pos 1; downregulated (loss) → matte red pos 5\nCOLOR_UP = \"#009E73\"  # Imprint pos 1: brand green\nCOLOR_DOWN = \"#AE3030\"  # Imprint pos 5: matte red (semantic anchor for loss/down)\nCOLOR_LOESS = \"#BD8233\"  # Imprint pos 4: ochre (diagnostic overlay)\n\n# Data — simulated RNA-seq differential expression results\nnp.random.seed(42)\nn_genes = 15000\n\nmean_expression = np.concatenate(\n    [\n        np.random.exponential(3, n_genes // 3),\n        np.random.uniform(0.5, 12, n_genes // 3),\n        np.random.normal(6, 2.5, n_genes - 2 * (n_genes // 3)),\n    ]\n)\nmean_expression = np.clip(mean_expression, 0.1, 16)\n\nlog_fold_change = np.random.normal(0, 0.4, n_genes)\nn_up = 400\nn_down = 350\nup_idx = np.random.choice(n_genes, n_up, replace=False)\nremaining = np.setdiff1d(np.arange(n_genes), up_idx)\ndown_idx = np.random.choice(remaining, n_down, replace=False)\nlog_fold_change[up_idx] = np.random.uniform(1.0, 4.5, n_up)\nlog_fold_change[down_idx] = np.random.uniform(-4.5, -1.0, n_down)\n\nsignificant = np.zeros(n_genes, dtype=bool)\nsignificant[up_idx] = True\nsignificant[down_idx] = True\n\ngene_names = [f\"Gene{i}\" for i in range(n_genes)]\n\n# Label top DE genes spread across the expression range\ntop_up_sorted = up_idx[np.argsort(-log_fold_change[up_idx])]\ntop_down_sorted = down_idx[np.argsort(log_fold_change[down_idx])]\nup_names = [\"BRCA1\", \"MYC\", \"EGFR\"]\ndown_names = [\"PTEN\", \"RB1\", \"KRAS\"]\nlabel_idx = np.concatenate([top_up_sorted[:3], top_down_sorted[:3]])\nexample_names = up_names + down_names\nfor i, idx in enumerate(label_idx):\n    gene_names[idx] = example_names[i]\n\ndf = pd.DataFrame(\n    {\n        \"mean_expression\": mean_expression,\n        \"log_fold_change\": log_fold_change,\n        \"significant\": significant,\n        \"gene_name\": gene_names,\n    }\n)\ndf[\"status\"] = np.where(\n    ~df[\"significant\"], \"Not significant\", np.where(df[\"log_fold_change\"] > 0, \"Upregulated\", \"Downregulated\")\n)\n\ndf_labels = df.loc[df[\"gene_name\"].isin(example_names)].copy()\ndf_labels = df_labels.sort_values(\"mean_expression\").reset_index(drop=True)\ndf_labels[\"label_x\"] = df_labels[\"mean_expression\"].values\ndf_labels[\"label_y\"] = df_labels[\"log_fold_change\"].values\nfor i in range(1, len(df_labels)):\n    if abs(df_labels.loc[i, \"label_x\"] - df_labels.loc[i - 1, \"label_x\"]) < 1.5:\n        if abs(df_labels.loc[i, \"label_y\"] - df_labels.loc[i - 1, \"label_y\"]) < 0.8:\n            df_labels.loc[i, \"label_y\"] += 0.9 * (1 if df_labels.loc[i, \"log_fold_change\"] > 0 else -1)\n\ndf_nonsig = df[~df[\"significant\"]].copy()\ndf_sig = df[df[\"significant\"]].copy()\n\n# Background shading for ±1 FC region\nfc_band_data = pd.DataFrame({\"y\": [-1], \"y2\": [1]})\nfc_band = alt.Chart(fc_band_data).mark_rect(color=INK_SOFT, opacity=0.11).encode(y=\"y:Q\", y2=\"y2:Q\")\n\n# Non-significant points — small, faint, muted\npoints_nonsig = (\n    alt.Chart(df_nonsig)\n    .mark_point(filled=True, size=15, opacity=0.2, strokeWidth=0, color=INK_MUTED)\n    .encode(\n        x=alt.X(\"mean_expression:Q\", title=\"Mean Expression (A)\"),\n        y=alt.Y(\"log_fold_change:Q\", title=\"Log₂ Fold Change (M)\"),\n        tooltip=[\n            alt.Tooltip(\"gene_name:N\", title=\"Gene\"),\n            alt.Tooltip(\"mean_expression:Q\", title=\"Mean Expr\", format=\".2f\"),\n            alt.Tooltip(\"log_fold_change:Q\", title=\"Log₂ FC\", format=\".2f\"),\n        ],\n    )\n)\n\n# Hover highlight for significant genes\nhighlight = alt.selection_point(on=\"pointerover\", fields=[\"gene_name\"], empty=False)\n\n# Significant points with color + shape redundant encoding for accessibility\ncolor_scale = alt.Scale(domain=[\"Upregulated\", \"Downregulated\"], range=[COLOR_UP, COLOR_DOWN])\nshape_scale = alt.Scale(domain=[\"Upregulated\", \"Downregulated\"], range=[\"triangle-up\", \"triangle-down\"])\npoints_sig = (\n    alt.Chart(df_sig)\n    .mark_point(filled=True, stroke=PAGE_BG, strokeWidth=0.5)\n    .encode(\n        x=alt.X(\"mean_expression:Q\"),\n        y=alt.Y(\"log_fold_change:Q\"),\n        color=alt.Color(\n            \"status:N\",\n            scale=color_scale,\n            legend=alt.Legend(\n                title=None, labelFontSize=10, symbolSize=80, orient=\"top-right\", direction=\"horizontal\", padding=6\n            ),\n        ),\n        shape=alt.Shape(\"status:N\", scale=shape_scale, legend=None),\n        size=alt.condition(highlight, alt.value(100), alt.value(50)),\n        tooltip=[\n            alt.Tooltip(\"gene_name:N\", title=\"Gene\"),\n            alt.Tooltip(\"mean_expression:Q\", title=\"Mean Expr\", format=\".2f\"),\n            alt.Tooltip(\"log_fold_change:Q\", title=\"Log₂ FC\", format=\".2f\"),\n            alt.Tooltip(\"status:N\", title=\"Status\"),\n        ],\n    )\n    .add_params(highlight)\n)\n\n# Reference lines\nzero_line = alt.Chart(pd.DataFrame({\"y\": [0]})).mark_rule(color=INK, strokeWidth=1.5, opacity=0.7).encode(y=\"y:Q\")\nfc_thresholds = (\n    alt.Chart(pd.DataFrame({\"y\": [-1, 1]}))\n    .mark_rule(color=INK_SOFT, strokeWidth=1.5, strokeDash=[6, 4], opacity=0.6)\n    .encode(y=\"y:Q\")\n)\n\n# LOESS trend to reveal expression-dependent bias\nloess_line = (\n    alt.Chart(df)\n    .transform_loess(\"mean_expression\", \"log_fold_change\", bandwidth=0.3)\n    .mark_line(color=COLOR_LOESS, strokeWidth=2.5, opacity=0.75)\n    .encode(x=\"mean_expression:Q\", y=\"log_fold_change:Q\")\n)\n\n# Gene labels for top DE genes\nlabels = (\n    alt.Chart(df_labels)\n    .mark_text(fontSize=10, fontStyle=\"italic\", fontWeight=\"bold\", color=INK, dy=-12, align=\"center\")\n    .encode(x=\"label_x:Q\", y=\"label_y:Q\", text=\"gene_name:N\")\n)\n\n# Compose chart — landscape canvas: width=620, height=320 → pads to 3200×1800 after save\nchart = (\n    (fc_band + zero_line + fc_thresholds + points_nonsig + points_sig + loess_line + labels)\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        title=alt.Title(\n            \"ma-differential-expression · python · altair · anyplot.ai\",\n            fontSize=16,\n            color=INK,\n            anchor=\"middle\",\n            offset=8,\n            subtitle=\"RNA-seq differential expression: upregulated (green) and downregulated (red) genes\",\n            subtitleFontSize=12,\n            subtitleColor=INK_SOFT,\n            subtitlePadding=4,\n        ),\n    )\n    .configure_view(fill=PAGE_BG, stroke=\"transparent\")\n    .configure_axis(\n        labelFontSize=10,\n        titleFontSize=12,\n        titlePadding=10,\n        titleColor=INK,\n        labelColor=INK_SOFT,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        tickSize=4,\n        grid=True,\n        gridOpacity=0.15,\n        gridColor=INK,\n    )\n    .configure_title(color=INK)\n    .configure_legend(\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=10,\n        titleFontSize=10,\n        symbolSize=80,\n        padding=6,\n    )\n    .interactive()\n)\n\n# Save PNG then pad to exact 3200×1800 target (vl-convert adds title/axis padding outside width/height)\nTW, TH = 3200, 1800\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\nchart.save(f\"plot-{THEME}.html\")\n"}