{"spec_id":"genome-track-multi","library":"altair","language":"python","code":"\"\"\" anyplot.ai\ngenome-track-multi: Genome Track Viewer\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-06-02\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent self-shadowing: this file is named altair.py, same as the library.\n# Python inserts the script's absolute directory into sys.path[0]; remove it\n# so that `import altair` finds the installed package, not this file.\n_this_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != _this_dir and p not in (\"\", \".\")]\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nPAGE_BG_RGB = (0xFA, 0xF8, 0xF1) if THEME == \"light\" else (0x1A, 0x1A, 0x17)\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 — hybrid-v3 sort order\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Data: chr7 ~50 kb window around EGFR/VOPP1\nnp.random.seed(42)\nchrom = \"chr7\"\nregion_start = 55_140_000\nregion_end = 55_190_000\n\ngene_bodies = pd.DataFrame(\n    {\n        \"start\": [55_142_000, 55_160_000],\n        \"end\": [55_155_000, 55_178_000],\n        \"gene\": [\"EGFR\", \"VOPP1\"],\n        \"strand\": [\"+\", \"-\"],\n        \"y_pos\": [1, 0],\n    }\n)\n\nexons = pd.DataFrame(\n    {\n        \"start\": [\n            55_142_000,\n            55_144_500,\n            55_148_000,\n            55_152_000,\n            55_160_000,\n            55_163_000,\n            55_167_000,\n            55_171_000,\n            55_175_000,\n        ],\n        \"end\": [\n            55_143_200,\n            55_145_800,\n            55_149_500,\n            55_154_800,\n            55_161_500,\n            55_164_200,\n            55_168_800,\n            55_172_500,\n            55_177_800,\n        ],\n        \"gene\": [\"EGFR\", \"EGFR\", \"EGFR\", \"EGFR\", \"VOPP1\", \"VOPP1\", \"VOPP1\", \"VOPP1\", \"VOPP1\"],\n        \"y_pos\": [1, 1, 1, 1, 0, 0, 0, 0, 0],\n    }\n)\n\ncoverage_positions = np.arange(region_start, region_end, 200)\nbase_coverage = np.random.exponential(15, len(coverage_positions))\nfor _, row in exons.iterrows():\n    mask = (coverage_positions >= row[\"start\"]) & (coverage_positions <= row[\"end\"])\n    base_coverage[mask] += np.random.uniform(30, 60, mask.sum())\ncoverage_df = pd.DataFrame({\"position\": coverage_positions, \"depth\": base_coverage})\n\nn_variants = 18\nvariant_positions = np.sort(np.random.randint(region_start + 1000, region_end - 1000, n_variants))\nvariant_df = pd.DataFrame(\n    {\n        \"position\": variant_positions,\n        \"quality\": np.random.uniform(20, 100, n_variants),\n        \"variant_type\": np.random.choice([\"SNP\", \"Indel\"], n_variants, p=[0.8, 0.2]),\n    }\n)\n\nregulatory_df = pd.DataFrame(\n    {\n        \"start\": [55_140_500, 55_143_800, 55_158_000, 55_169_500, 55_180_000],\n        \"end\": [55_141_800, 55_144_400, 55_159_500, 55_170_800, 55_182_000],\n        \"element_type\": [\"Promoter\", \"Enhancer\", \"Promoter\", \"Enhancer\", \"Enhancer\"],\n        \"y_pos\": [0, 0, 0, 0, 0],\n    }\n)\n\nx_domain = [region_start, region_end]\nW = 620\n\n# Background data for alternating track tints (Coverage = track 2, Regulatory = track 4)\n_even_bg_df = pd.DataFrame({\"xs\": [region_start], \"xe\": [region_end]})\n\n# Track 1: Genes — intron lines + exon rectangles + strand arrows + gene names\nintron_lines = (\n    alt.Chart(gene_bodies)\n    .mark_rule(strokeWidth=2)\n    .encode(\n        x=alt.X(\"start:Q\", scale=alt.Scale(domain=x_domain), axis=None),\n        x2=\"end:Q\",\n        y=alt.Y(\"y_pos:Q\", scale=alt.Scale(domain=[-0.5, 1.5]), axis=None),\n        color=alt.value(IMPRINT_PALETTE[2]),\n    )\n)\n\nexon_bars = (\n    alt.Chart(exons)\n    .mark_bar(height=20)\n    .encode(\n        x=alt.X(\"start:Q\", scale=alt.Scale(domain=x_domain), axis=None),\n        x2=\"end:Q\",\n        y=alt.Y(\"y_pos:Q\", scale=alt.Scale(domain=[-0.5, 1.5]), axis=None),\n        color=alt.value(IMPRINT_PALETTE[2]),\n        tooltip=[alt.Tooltip(\"gene:N\", title=\"Gene\")],\n    )\n)\n\ngene_labels = (\n    alt.Chart(gene_bodies)\n    .mark_text(fontSize=12, fontWeight=\"bold\", align=\"left\", dx=4, dy=-14)\n    .encode(\n        x=alt.X(\"start:Q\", scale=alt.Scale(domain=x_domain), axis=None),\n        y=alt.Y(\"y_pos:Q\", scale=alt.Scale(domain=[-0.5, 1.5]), axis=None),\n        text=alt.Text(\"gene:N\"),\n        color=alt.value(INK),\n    )\n)\n\nstrand_data = []\nfor _, row in gene_bodies.iterrows():\n    for pos in np.linspace(row[\"start\"] + 1000, row[\"end\"] - 1000, 5):\n        strand_data.append({\"position\": pos, \"y_pos\": row[\"y_pos\"], \"angle\": 0 if row[\"strand\"] == \"+\" else 180})\nstrand_df = pd.DataFrame(strand_data)\n\nstrand_marks = (\n    alt.Chart(strand_df)\n    .mark_point(shape=\"triangle-right\", size=110, filled=True, opacity=0.75)\n    .encode(\n        x=alt.X(\"position:Q\", scale=alt.Scale(domain=x_domain), axis=None),\n        y=alt.Y(\"y_pos:Q\", scale=alt.Scale(domain=[-0.5, 1.5]), axis=None),\n        angle=alt.Angle(\"angle:Q\", scale=None),\n        color=alt.value(IMPRINT_PALETTE[5]),\n    )\n)\n\ngene_track = (intron_lines + exon_bars + gene_labels + strand_marks).properties(\n    width=W, height=58, title=alt.Title(\"Genes\", anchor=\"start\", fontSize=12, color=INK_MUTED)\n)\n\n# Track 2: Coverage — alternating background + filled area with the brand-green primary series\n_cov_bg = (\n    alt.Chart(_even_bg_df)\n    .mark_rect(fill=ELEVATED_BG, opacity=1.0)\n    .encode(\n        x=alt.X(\"xs:Q\", scale=alt.Scale(domain=x_domain), axis=None),\n        x2=alt.X2(\"xe:Q\"),\n        y=alt.value(0),\n        y2=alt.value(80),\n    )\n)\ncoverage_track = (\n    _cov_bg\n    + alt.Chart(coverage_df)\n    .mark_area(interpolate=\"monotone\", opacity=0.55, line={\"color\": IMPRINT_PALETTE[0], \"strokeWidth\": 1.5})\n    .encode(\n        x=alt.X(\"position:Q\", scale=alt.Scale(domain=x_domain), axis=None),\n        y=alt.Y(\"depth:Q\", axis=alt.Axis(title=\"Read Depth\", tickCount=4, grid=False)),\n        color=alt.value(IMPRINT_PALETTE[0]),\n        tooltip=[\n            alt.Tooltip(\"position:Q\", title=\"Position\", format=\",\"),\n            alt.Tooltip(\"depth:Q\", title=\"Depth\", format=\".1f\"),\n        ],\n    )\n).properties(width=W, height=80, title=alt.Title(\"Coverage\", anchor=\"start\", fontSize=12, color=INK_MUTED))\n\n# Track 3: Variants — circles, quality on y-axis, legend bottom to save right-side space\nvariant_track = (\n    alt.Chart(variant_df)\n    .mark_circle(size=160, opacity=0.75)\n    .encode(\n        x=alt.X(\"position:Q\", scale=alt.Scale(domain=x_domain), axis=None),\n        y=alt.Y(\"quality:Q\", axis=alt.Axis(title=\"Quality\", tickCount=4, grid=False)),\n        color=alt.Color(\n            \"variant_type:N\",\n            scale=alt.Scale(domain=[\"SNP\", \"Indel\"], range=[IMPRINT_PALETTE[0], IMPRINT_PALETTE[1]]),\n            legend=alt.Legend(\n                title=\"Variant\",\n                orient=\"bottom-right\",\n                direction=\"horizontal\",\n                labelFontSize=9,\n                titleFontSize=9,\n                fillColor=ELEVATED_BG,\n                strokeColor=INK_SOFT,\n                labelColor=INK_SOFT,\n                titleColor=INK,\n                padding=4,\n            ),\n        ),\n        tooltip=[\n            alt.Tooltip(\"position:Q\", title=\"Position\", format=\",\"),\n            alt.Tooltip(\"quality:Q\", title=\"Quality\", format=\".1f\"),\n            alt.Tooltip(\"variant_type:N\", title=\"Type\"),\n        ],\n    )\n    .properties(width=W, height=72, title=alt.Title(\"Variants\", anchor=\"start\", fontSize=12, color=INK_MUTED))\n)\n\n# Track 4: Regulatory — alternating background + taller bars, legend bottom-right\n_reg_bg = (\n    alt.Chart(_even_bg_df)\n    .mark_rect(fill=ELEVATED_BG, opacity=1.0)\n    .encode(\n        x=alt.X(\"xs:Q\", scale=alt.Scale(domain=x_domain), axis=None),\n        x2=alt.X2(\"xe:Q\"),\n        y=alt.value(0),\n        y2=alt.value(58),\n    )\n)\nregulatory_track = (\n    _reg_bg\n    + alt.Chart(regulatory_df)\n    .mark_bar(height=38, cornerRadius=4)\n    .encode(\n        x=alt.X(\n            \"start:Q\",\n            scale=alt.Scale(domain=x_domain),\n            axis=alt.Axis(\n                title=f\"Genomic Position ({chrom})\", labelExpr=\"format(datum.value, ',.0f')\", tickCount=6, grid=False\n            ),\n        ),\n        x2=\"end:Q\",\n        y=alt.Y(\"y_pos:Q\", scale=alt.Scale(domain=[-0.5, 0.5]), axis=None),\n        color=alt.Color(\n            \"element_type:N\",\n            scale=alt.Scale(domain=[\"Promoter\", \"Enhancer\"], range=[IMPRINT_PALETTE[4], IMPRINT_PALETTE[3]]),\n            legend=alt.Legend(\n                title=\"Regulatory\",\n                orient=\"bottom-right\",\n                direction=\"horizontal\",\n                labelFontSize=9,\n                titleFontSize=9,\n                fillColor=ELEVATED_BG,\n                strokeColor=INK_SOFT,\n                labelColor=INK_SOFT,\n                titleColor=INK,\n                padding=4,\n            ),\n        ),\n        tooltip=[\n            alt.Tooltip(\"element_type:N\", title=\"Type\"),\n            alt.Tooltip(\"start:Q\", title=\"Start\", format=\",\"),\n            alt.Tooltip(\"end:Q\", title=\"End\", format=\",\"),\n        ],\n    )\n).properties(width=W, height=58, title=alt.Title(\"Regulatory\", anchor=\"start\", fontSize=12, color=INK_MUTED))\n\n# Combine tracks\ntitle_str = \"genome-track-multi · python · altair · anyplot.ai\"\n# len(title_str) = 49 < 67, default fontSize=16 is fine\n\nchart = (\n    alt.vconcat(gene_track, coverage_track, variant_track, regulatory_track, spacing=6)\n    .resolve_scale(color=\"independent\")\n    .properties(background=PAGE_BG, title=alt.Title(title_str, fontSize=16, anchor=\"middle\", color=INK))\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n    .configure_axis(\n        labelFontSize=10,\n        titleFontSize=12,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.12,\n    )\n    .configure_title(color=INK)\n    .configure_concat(spacing=6)\n)\n\n# Save PNG and apply pad-only-to-target (landscape 3200×1800)\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\nTW, TH = 3200, 1800\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        \"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_RGB)\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"}