{"spec_id":"genome-track-multi","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\ngenome-track-multi: Genome Track Viewer\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 93/100 | Updated: 2026-06-02\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\nfrom plotly.subplots import make_subplots\n\n\n# Theme tokens — Imprint palette + theme-adaptive chrome\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\"\nGRID = \"rgba(26,26,23,0.15)\" if THEME == \"light\" else \"rgba(240,239,232,0.15)\"\nTRACK_BG = \"rgba(26,26,23,0.04)\" if THEME == \"light\" else \"rgba(240,239,232,0.04)\"\n\n# Imprint categorical palette — positions assigned by track role\nEXON_COLOR = \"#009E73\"  # Imprint pos 1 — brand green (gene annotation)\nINTRON_COLOR = INK_SOFT  # theme-adaptive structural chrome\nCOVERAGE_COLOR = \"#4467A3\"  # Imprint pos 3 — blue (expression depth)\nSNP_COLOR = \"#BD8233\"  # Imprint pos 4 — ochre\nINDEL_COLOR = \"#AE3030\"  # Imprint pos 5 — matte red (more impactful variant)\nPROMOTER_COLOR = \"#C475FD\"  # Imprint pos 2 — lavender\nENHANCER_COLOR = \"#2ABCCD\"  # Imprint pos 6 — cyan\nCTCF_COLOR = \"#99B314\"  # Imprint pos 8 — lime\n\n# Data\nnp.random.seed(42)\n\n# Genomic region: chr7, EGFR locus\nchrom = \"chr7\"\nregion_start = 55_086_000\nregion_end = 55_280_000\n\n# Gene track — EGFR structure (28 exons)\ngene_name = \"EGFR\"\nstrand = \"+\"\nexons = [\n    (55_086_714, 55_087_058),\n    (55_152_580, 55_152_770),\n    (55_154_000, 55_154_200),\n    (55_155_830, 55_156_100),\n    (55_160_100, 55_160_450),\n    (55_165_300, 55_165_600),\n    (55_168_500, 55_168_800),\n    (55_174_700, 55_175_100),\n    (55_181_300, 55_181_600),\n    (55_191_700, 55_192_100),\n    (55_198_700, 55_199_200),\n    (55_200_700, 55_201_100),\n    (55_209_900, 55_210_300),\n    (55_211_000, 55_211_400),\n    (55_218_900, 55_219_200),\n    (55_220_200, 55_220_600),\n    (55_223_500, 55_224_000),\n    (55_227_900, 55_228_500),\n    (55_229_200, 55_229_600),\n    (55_231_400, 55_231_800),\n    (55_233_800, 55_234_300),\n    (55_236_300, 55_236_700),\n    (55_238_800, 55_239_200),\n    (55_240_500, 55_241_000),\n    (55_249_000, 55_249_400),\n    (55_259_400, 55_259_800),\n    (55_266_400, 55_266_800),\n    (55_268_800, 55_270_500),\n]\ngene_start = exons[0][0]\ngene_end = exons[-1][1]\n\n# Coverage track — RNA-seq read depth, clipped at 95th percentile\n# to prevent the extreme 3' spike from compressing smaller exon peaks\npositions = np.linspace(region_start, region_end, 2000)\nbase_coverage = np.random.exponential(5, 2000)\nfor ex_start, ex_end in exons:\n    mask = (positions >= ex_start) & (positions <= ex_end)\n    base_coverage[mask] += np.random.exponential(40, mask.sum())\ncoverage = np.convolve(base_coverage, np.ones(15) / 15, mode=\"same\")\ncoverage = np.clip(coverage, 0, np.percentile(coverage, 95))\n\n# Variant track — SNPs and indels with quality scores\nvariant_positions = [\n    55_092_000,\n    55_155_900,\n    55_160_250,\n    55_174_800,\n    55_191_800,\n    55_199_100,\n    55_210_200,\n    55_220_400,\n    55_228_200,\n    55_240_700,\n    55_249_200,\n    55_259_600,\n    55_269_000,\n]\nvariant_types = [\"SNP\", \"SNP\", \"Indel\", \"SNP\", \"SNP\", \"SNP\", \"Indel\", \"SNP\", \"SNP\", \"SNP\", \"SNP\", \"Indel\", \"SNP\"]\nvariant_quality = np.random.uniform(20, 99, len(variant_positions))\nvariant_labels = [\n    \"rs121434568\",\n    \"rs28929495\",\n    \"ins_3bp\",\n    \"rs121913229\",\n    \"rs1050171\",\n    \"rs2227983\",\n    \"del_2bp\",\n    \"rs121434569\",\n    \"rs56289927\",\n    \"rs17290699\",\n    \"rs10241451\",\n    \"ins_5bp\",\n    \"rs11543848\",\n]\n\n# Regulatory track — enhancers, promoters, CTCF binding sites\nreg_elements = [\n    (55_084_500, 55_086_700, \"Promoter\"),\n    (55_100_000, 55_103_000, \"Enhancer\"),\n    (55_130_000, 55_133_000, \"CTCF\"),\n    (55_148_000, 55_151_000, \"Enhancer\"),\n    (55_170_000, 55_172_500, \"Enhancer\"),\n    (55_205_000, 55_208_000, \"CTCF\"),\n    (55_245_000, 55_248_000, \"Enhancer\"),\n    (55_272_000, 55_275_000, \"Promoter\"),\n]\n\n# Plot — 4 tracks sharing x-axis\nfig = make_subplots(rows=4, cols=1, shared_xaxes=True, vertical_spacing=0.06, row_heights=[0.2, 0.35, 0.2, 0.2])\n\n# Track 1: Gene annotations\ngene_y = 0.5\nfig.add_trace(\n    go.Scatter(\n        x=[gene_start, gene_end],\n        y=[gene_y, gene_y],\n        mode=\"lines\",\n        line={\"color\": INTRON_COLOR, \"width\": 2},\n        showlegend=False,\n        hoverinfo=\"skip\",\n    ),\n    row=1,\n    col=1,\n)\nfor ex_start, ex_end in exons:\n    fig.add_shape(\n        type=\"rect\",\n        x0=ex_start,\n        x1=ex_end,\n        y0=0.2,\n        y1=0.8,\n        fillcolor=EXON_COLOR,\n        line={\"color\": EXON_COLOR, \"width\": 1},\n        row=1,\n        col=1,\n    )\narrow_positions = np.linspace(gene_start + 5000, gene_end - 5000, 12)\nfor pos in arrow_positions:\n    in_exon = any(es <= pos <= ee for es, ee in exons)\n    if not in_exon:\n        fig.add_annotation(\n            x=pos,\n            y=gene_y,\n            ax=pos - 2000,\n            ay=gene_y,\n            xref=\"x\",\n            yref=\"y\",\n            axref=\"x\",\n            ayref=\"y\",\n            showarrow=True,\n            arrowhead=2,\n            arrowsize=1.5,\n            arrowwidth=1.5,\n            arrowcolor=INTRON_COLOR,\n            row=1,\n            col=1,\n        )\nfig.add_annotation(\n    x=(gene_start + gene_end) / 2,\n    y=1.1,\n    text=f\"<b>{gene_name}</b> ({strand})\",\n    showarrow=False,\n    font={\"size\": 12, \"color\": EXON_COLOR},\n    xref=\"x\",\n    yref=\"y\",\n    row=1,\n    col=1,\n)\n\n# Track 2: Coverage (filled area)\ncov_fill = \"rgba(68,103,163,0.35)\" if THEME == \"light\" else \"rgba(68,103,163,0.5)\"\nfig.add_trace(\n    go.Scatter(\n        x=positions,\n        y=coverage,\n        mode=\"lines\",\n        fill=\"tozeroy\",\n        fillcolor=cov_fill,\n        line={\"color\": COVERAGE_COLOR, \"width\": 1.5},\n        showlegend=False,\n        hovertemplate=\"Position: %{x:,.0f}<br>Coverage: %{y:.1f}x<extra></extra>\",\n    ),\n    row=2,\n    col=1,\n)\n\n# Track 3: Variants (lollipop markers encoding quality score)\nvar_color_map = {\"SNP\": SNP_COLOR, \"Indel\": INDEL_COLOR}\nfor pos, vtype, qual, vlabel in zip(variant_positions, variant_types, variant_quality, variant_labels, strict=False):\n    color = var_color_map[vtype]\n    fig.add_trace(\n        go.Scatter(\n            x=[pos, pos],\n            y=[0, qual],\n            mode=\"lines\",\n            line={\"color\": color, \"width\": 2},\n            showlegend=False,\n            hoverinfo=\"skip\",\n        ),\n        row=3,\n        col=1,\n    )\n    fig.add_trace(\n        go.Scatter(\n            x=[pos],\n            y=[qual],\n            mode=\"markers\",\n            marker={\n                \"size\": 10 if vtype == \"SNP\" else 12,\n                \"color\": color,\n                \"symbol\": \"circle\" if vtype == \"SNP\" else \"diamond\",\n                \"line\": {\"color\": PAGE_BG, \"width\": 1.5},\n            },\n            showlegend=False,\n            hovertemplate=f\"{vlabel}<br>Type: {vtype}<br>Position: {pos:,}<br>Quality: {qual:.1f}<extra></extra>\",\n        ),\n        row=3,\n        col=1,\n    )\nfig.add_trace(\n    go.Scatter(\n        x=[None], y=[None], mode=\"markers\", marker={\"size\": 10, \"color\": SNP_COLOR, \"symbol\": \"circle\"}, name=\"SNP\"\n    ),\n    row=3,\n    col=1,\n)\nfig.add_trace(\n    go.Scatter(\n        x=[None], y=[None], mode=\"markers\", marker={\"size\": 12, \"color\": INDEL_COLOR, \"symbol\": \"diamond\"}, name=\"Indel\"\n    ),\n    row=3,\n    col=1,\n)\n\n# Track 4: Regulatory elements (add_shape for clean rectangles; invisible traces for legend)\nreg_color_map = {\"Promoter\": PROMOTER_COLOR, \"Enhancer\": ENHANCER_COLOR, \"CTCF\": CTCF_COLOR}\nadded_legend = set()\nfor reg_start, reg_end, reg_type in reg_elements:\n    color = reg_color_map[reg_type]\n    fig.add_shape(\n        type=\"rect\",\n        x0=reg_start,\n        y0=0.1,\n        x1=reg_end,\n        y1=0.9,\n        fillcolor=color,\n        line={\"color\": color, \"width\": 1},\n        opacity=0.85,\n        row=4,\n        col=1,\n    )\n    if reg_type not in added_legend:\n        added_legend.add(reg_type)\n        fig.add_trace(\n            go.Scatter(\n                x=[None],\n                y=[None],\n                mode=\"markers\",\n                marker={\"color\": color, \"size\": 12, \"symbol\": \"square\"},\n                name=reg_type,\n                showlegend=True,\n            ),\n            row=4,\n            col=1,\n        )\n\n# Track y-axis labels\nfor row, label in [(1, \"Genes\"), (2, \"Coverage\"), (4, \"Regulatory\")]:\n    fig.update_yaxes(title={\"text\": f\"<b>{label}</b>\", \"font\": {\"size\": 12, \"color\": INK}}, row=row, col=1)\nfig.update_yaxes(title={\"text\": \"<b>Variants</b><br>Quality\", \"font\": {\"size\": 12, \"color\": INK}}, row=3, col=1)\n\n# Title fontsize scaled to title character length\ntitle_text = \"EGFR Locus (chr7) · genome-track-multi · python · plotly · anyplot.ai\"\nsubtitle_text = \"Multi-track genome browser — chr7:55,086,000–55,280,000\"\nn = len(title_text)\ntitle_fontsize = max(11, round(16 * 67 / n)) if n > 67 else 16\n\n# Layout\nfig.update_layout(\n    autosize=False,\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font={\"color\": INK},\n    title={\n        \"text\": (f'{title_text}<br><span style=\"font-size:10px;color:{INK_MUTED}\">{subtitle_text}</span>'),\n        \"font\": {\"size\": title_fontsize, \"color\": INK},\n        \"x\": 0.5,\n    },\n    legend={\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n        \"font\": {\"size\": 10, \"color\": INK_SOFT},\n        \"orientation\": \"h\",\n        \"yanchor\": \"top\",\n        \"y\": -0.12,\n        \"xanchor\": \"center\",\n        \"x\": 0.5,\n    },\n    margin={\"l\": 90, \"r\": 40, \"t\": 70, \"b\": 100},\n)\n\n# X-axis (bottom track only shows labels and title)\nfig.update_xaxes(\n    title={\"text\": \"Genomic Position (bp)\", \"font\": {\"size\": 12, \"color\": INK}},\n    tickfont={\"size\": 10, \"color\": INK_SOFT},\n    tickformat=\",\",\n    range=[region_start - 2000, region_end + 5000],\n    linecolor=INK_SOFT,\n    gridcolor=GRID,\n    row=4,\n    col=1,\n)\nfor row in range(1, 4):\n    fig.update_xaxes(\n        tickfont={\"size\": 10}, tickformat=\",\", showticklabels=False, showgrid=False, linecolor=INK_SOFT, row=row, col=1\n    )\n\n# Y-axes per track\nfig.update_yaxes(range=[-0.2, 1.4], showticklabels=False, showgrid=False, linecolor=INK_SOFT, row=1, col=1)\nfig.update_yaxes(\n    tickfont={\"size\": 10, \"color\": INK_SOFT}, gridcolor=GRID, gridwidth=1, linecolor=INK_SOFT, row=2, col=1\n)\nfig.update_yaxes(tickfont={\"size\": 10, \"color\": INK_SOFT}, range=[-5, 110], linecolor=INK_SOFT, row=3, col=1)\nfig.update_yaxes(range=[-0.1, 1.1], showticklabels=False, showgrid=False, linecolor=INK_SOFT, row=4, col=1)\n\n# Alternating background shading on tracks 1 and 3\nfor row in [1, 3]:\n    fig.add_shape(\n        type=\"rect\",\n        x0=0,\n        x1=1,\n        y0=0,\n        y1=1,\n        xref=f\"x{row} domain\" if row > 1 else \"x domain\",\n        yref=f\"y{row} domain\" if row > 1 else \"y domain\",\n        fillcolor=TRACK_BG,\n        line={\"width\": 0},\n        layer=\"below\",\n    )\n\n# Colored left-edge accent strips per track\naccent_colors = {1: EXON_COLOR, 2: COVERAGE_COLOR, 3: SNP_COLOR, 4: PROMOTER_COLOR}\nfor row, accent in accent_colors.items():\n    fig.add_shape(\n        type=\"rect\",\n        x0=-0.005,\n        x1=0.0,\n        y0=0,\n        y1=1,\n        xref=f\"x{row} domain\" if row > 1 else \"x domain\",\n        yref=f\"y{row} domain\" if row > 1 else \"y domain\",\n        fillcolor=accent,\n        line={\"width\": 0},\n        layer=\"above\",\n    )\n\n# Track divider lines\nfor row in range(1, 5):\n    fig.add_shape(\n        type=\"line\",\n        x0=0,\n        x1=1,\n        y0=0,\n        y1=0,\n        xref=f\"x{row} domain\" if row > 1 else \"x domain\",\n        yref=f\"y{row} domain\" if row > 1 else \"y domain\",\n        line={\"color\": INK_SOFT, \"width\": 0.8},\n    )\n\n# Save static PNG (no rangeslider — cleaner layout)\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\n\n# Save HTML with rangeslider for interactive navigation\nfig.update_xaxes(rangeslider={\"visible\": True, \"thickness\": 0.06}, row=4, col=1)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}