{"spec_id":"genome-track-multi","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\ngenome-track-multi: Genome Track Viewer\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-06-02\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# Remove script's own directory from sys.path so 'bokeh' resolves to the installed package\n_this_dir = os.path.dirname(os.path.abspath(__file__))\nif _this_dir in sys.path:\n    sys.path.remove(_this_dir)\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.layouts import column\nfrom bokeh.models import ColumnDataSource, HoverTool, Label, NumeralTickFormatter, Range1d\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\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 (canonical order)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Data-series color assignments (Imprint palette — semantic roles)\nCOLOR_COVERAGE = IMPRINT[0]  # #009E73 brand green — primary data series (coverage)\nCOLOR_EXON = IMPRINT[2]  # #4467A3 blue — coding exon blocks\nCOLOR_UTR = IMPRINT[5]  # #2ABCCD cyan — UTR sub-features\nCOLOR_SNP = IMPRINT[3]  # #BD8233 ochre — SNP variants\nCOLOR_INDEL = IMPRINT[1]  # #C475FD lavender — Indel variants\nCOLOR_PROMOTER = IMPRINT[4]  # #AE3030 red — promoter (activating regulatory)\nCOLOR_ENHANCER = IMPRINT[5]  # #2ABCCD cyan — enhancers\nCOLOR_CTCF = IMPRINT[6]  # #954477 rose — CTCF binding sites\n\nnp.random.seed(42)\n\nchrom = \"chr7\"\nregion_start = 55_086_000\nregion_end = 55_212_000\n\n# --- Gene Track Data ---\ngenes = [{\"name\": \"EGFR\", \"strand\": \"+\", \"tx_start\": 55_086_714, \"tx_end\": 55_205_000}]\nexons = [\n    (55_086_714, 55_087_200),\n    (55_088_900, 55_089_300),\n    (55_092_100, 55_092_500),\n    (55_096_200, 55_096_700),\n    (55_100_300, 55_100_700),\n    (55_105_500, 55_105_900),\n    (55_110_800, 55_111_300),\n    (55_117_500, 55_118_000),\n    (55_122_400, 55_122_800),\n    (55_127_300, 55_127_700),\n    (55_131_500, 55_131_900),\n    (55_136_800, 55_137_200),\n    (55_140_100, 55_140_500),\n    (55_143_700, 55_144_200),\n    (55_148_000, 55_148_400),\n    (55_151_800, 55_152_300),\n    (55_155_500, 55_155_900),\n    (55_160_200, 55_160_700),\n    (55_165_300, 55_165_800),\n    (55_170_800, 55_171_400),\n    (55_174_200, 55_174_700),\n    (55_181_300, 55_181_800),\n    (55_189_200, 55_189_700),\n    (55_196_400, 55_196_900),\n    (55_200_500, 55_201_000),\n    (55_203_800, 55_205_000),\n]\nutrs_5 = [(55_086_714, 55_087_000)]\nutrs_3 = [(55_204_500, 55_205_000)]\n\n# --- Coverage Track Data ---\npositions = np.linspace(region_start, region_end, 2000)\nbase_coverage = np.random.exponential(15, 2000)\nfor ex_start, ex_end in exons:\n    mask = (positions >= ex_start) & (positions <= ex_end)\n    base_coverage[mask] += np.random.exponential(45, mask.sum())\nkernel = np.exp(-0.5 * np.linspace(-3, 3, 31) ** 2)\nkernel /= kernel.sum()\ncoverage = np.clip(np.convolve(base_coverage, kernel, mode=\"same\"), 0, None)\ncov_max = float(coverage.max()) * 1.1\n\n# --- Variant Track Data ---\nvariant_positions = [\n    55_088_100,\n    55_092_300,\n    55_100_500,\n    55_111_000,\n    55_118_200,\n    55_127_500,\n    55_137_000,\n    55_144_000,\n    55_155_700,\n    55_165_500,\n    55_174_400,\n    55_189_500,\n    55_196_700,\n    55_201_200,\n    55_105_700,\n    55_140_300,\n    55_152_100,\n    55_160_400,\n    55_181_500,\n    55_170_900,\n]\nvariant_types = [\"SNP\"] * 14 + [\"Indel\"] * 6\nvariant_quality = np.random.uniform(20, 100, len(variant_positions))\nvar_max = 110.0\n\n# --- Regulatory Track Data ---\nreg_elements = [\n    {\"start\": 55_086_000, \"end\": 55_086_600, \"type\": \"Promoter\"},\n    {\"start\": 55_094_000, \"end\": 55_095_500, \"type\": \"Enhancer\"},\n    {\"start\": 55_113_000, \"end\": 55_114_500, \"type\": \"Enhancer\"},\n    {\"start\": 55_133_500, \"end\": 55_135_000, \"type\": \"CTCF Binding\"},\n    {\"start\": 55_157_000, \"end\": 55_158_500, \"type\": \"Enhancer\"},\n    {\"start\": 55_176_000, \"end\": 55_177_500, \"type\": \"Promoter\"},\n    {\"start\": 55_193_000, \"end\": 55_194_500, \"type\": \"CTCF Binding\"},\n    {\"start\": 55_207_000, \"end\": 55_209_000, \"type\": \"Enhancer\"},\n]\nreg_color_map = {\"Promoter\": COLOR_PROMOTER, \"Enhancer\": COLOR_ENHANCER, \"CTCF Binding\": COLOR_CTCF}\n\nshared_x_range = Range1d(start=region_start, end=region_end)\nW = 3200\nBL = 220  # min_border_left — room for y-axis label + tick labels at 32pt+26pt\nBR = 60  # min_border_right\n\n# ============================================================\n# Track 1: Gene Annotations (top — carries the chart title)\n# ============================================================\np_gene = figure(\n    width=W,\n    height=380,\n    x_range=shared_x_range,\n    y_range=Range1d(-1.8, 2.2),\n    title=\"genome-track-multi · python · bokeh · anyplot.ai\",\n    tools=\"\",\n    toolbar_location=None,\n    min_border_top=140,\n    min_border_bottom=5,\n    min_border_left=BL,\n    min_border_right=BR,\n)\n\n# Intron backbone\np_gene.segment(x0=[genes[0][\"tx_start\"]], y0=[0], x1=[genes[0][\"tx_end\"]], y1=[0], line_color=INK_SOFT, line_width=3)\n\n# Exon blocks\nexon_src = ColumnDataSource(\n    data={\n        \"left\": [e[0] for e in exons],\n        \"right\": [e[1] for e in exons],\n        \"top\": [0.5] * len(exons),\n        \"bottom\": [-0.5] * len(exons),\n    }\n)\np_gene.quad(\n    left=\"left\",\n    right=\"right\",\n    top=\"top\",\n    bottom=\"bottom\",\n    source=exon_src,\n    fill_color=COLOR_EXON,\n    line_color=PAGE_BG,\n    line_width=1.5,\n    alpha=0.9,\n)\n\n# UTR blocks (thinner)\nfor utr_s, utr_e in utrs_5 + utrs_3:\n    p_gene.quad(\n        left=[utr_s],\n        right=[utr_e],\n        top=[0.25],\n        bottom=[-0.25],\n        fill_color=COLOR_UTR,\n        line_color=PAGE_BG,\n        line_width=1,\n        alpha=0.8,\n    )\n\n# Strand direction indicators\nfor pos in np.arange(region_start + 4000, region_end - 4000, 5500):\n    p_gene.scatter(\n        x=[pos], y=[0], size=16, angle=-np.pi / 2, marker=\"triangle\", fill_color=INK_MUTED, line_color=None, alpha=0.55\n    )\n\n# Gene label\np_gene.text(\n    x=[(genes[0][\"tx_start\"] + genes[0][\"tx_end\"]) / 2],\n    y=[1.1],\n    text=[\"EGFR (+)\"],\n    text_font_size=\"28pt\",\n    text_align=\"center\",\n    text_color=INK,\n    text_font_style=\"italic\",\n)\np_gene.add_layout(\n    Label(\n        x=region_start + 1800, y=1.6, text=\"Genes\", text_font_size=\"28pt\", text_color=INK_SOFT, text_font_style=\"bold\"\n    )\n)\n\np_gene.title.text_font_size = \"46pt\"\np_gene.title.text_color = INK\np_gene.xaxis.visible = False\np_gene.yaxis.visible = False\np_gene.xgrid.grid_line_color = None\np_gene.ygrid.grid_line_color = None\np_gene.background_fill_color = PAGE_BG\np_gene.border_fill_color = PAGE_BG\np_gene.outline_line_color = None\n\n# ============================================================\n# Track 2: Read Coverage\n# ============================================================\np_cov = figure(\n    width=W,\n    height=525,\n    x_range=shared_x_range,\n    y_range=Range1d(0, cov_max),\n    tools=\"\",\n    toolbar_location=None,\n    min_border_top=5,\n    min_border_bottom=5,\n    min_border_left=BL,\n    min_border_right=BR,\n)\n\n# Subtle exon position shading — visually ties coverage peaks to gene structure\nfor ex_start, ex_end in exons:\n    p_cov.quad(\n        left=[ex_start],\n        right=[ex_end],\n        top=[cov_max],\n        bottom=[0],\n        fill_color=COLOR_EXON,\n        fill_alpha=0.06,\n        line_color=None,\n    )\n\ncov_src = ColumnDataSource(\n    data={\n        \"x\": positions,\n        \"y\": coverage,\n        \"pos_fmt\": [f\"{int(p):,}\" for p in positions],\n        \"depth_fmt\": [f\"{d:.1f}\" for d in coverage],\n    }\n)\np_cov.varea(x=\"x\", y1=0, y2=\"y\", source=cov_src, fill_color=COLOR_COVERAGE, fill_alpha=0.30)\np_cov.line(x=\"x\", y=\"y\", source=cov_src, line_color=COLOR_COVERAGE, line_width=2.5, alpha=0.9)\n\np_cov.add_tools(HoverTool(tooltips=[(\"Position\", \"@pos_fmt\"), (\"Depth\", \"@depth_fmt×\")], mode=\"vline\"))\np_cov.add_layout(\n    Label(\n        x=region_start + 1800,\n        y=cov_max * 0.88,\n        text=\"Coverage\",\n        text_font_size=\"28pt\",\n        text_color=INK_SOFT,\n        text_font_style=\"bold\",\n    )\n)\n\np_cov.xaxis.visible = False\np_cov.yaxis.axis_label = \"Read Depth\"\np_cov.yaxis.axis_label_text_font_size = \"32pt\"\np_cov.yaxis.axis_label_text_font_style = \"normal\"\np_cov.yaxis.axis_label_text_color = INK\np_cov.yaxis.major_label_text_font_size = \"26pt\"\np_cov.yaxis.major_label_text_color = INK_SOFT\np_cov.yaxis.axis_line_color = INK_SOFT\np_cov.yaxis.minor_tick_line_color = None\np_cov.yaxis.major_tick_line_color = INK_SOFT\np_cov.xgrid.grid_line_color = None\np_cov.ygrid.grid_line_color = INK\np_cov.ygrid.grid_line_alpha = 0.15\np_cov.ygrid.grid_line_dash = [4, 4]\np_cov.background_fill_color = PAGE_BG\np_cov.border_fill_color = PAGE_BG\np_cov.outline_line_color = None\n\n# ============================================================\n# Track 3: Variants (lollipop markers)\n# ============================================================\np_var = figure(\n    width=W,\n    height=460,\n    x_range=shared_x_range,\n    y_range=Range1d(0, var_max),\n    tools=\"\",\n    toolbar_location=None,\n    min_border_top=5,\n    min_border_bottom=5,\n    min_border_left=BL,\n    min_border_right=BR,\n)\n\n# Subtle exon position shading — visually ties variants to gene structure\nfor ex_start, ex_end in exons:\n    p_var.quad(\n        left=[ex_start],\n        right=[ex_end],\n        top=[var_max],\n        bottom=[0],\n        fill_color=COLOR_EXON,\n        fill_alpha=0.06,\n        line_color=None,\n    )\n\n# Lollipop stems\nfor i, vp in enumerate(variant_positions):\n    p_var.segment(x0=[vp], y0=[0], x1=[vp], y1=[variant_quality[i]], line_color=INK_MUTED, line_width=2)\n\nsnp_idx = [i for i, t in enumerate(variant_types) if t == \"SNP\"]\nsnp_src = ColumnDataSource(\n    data={\n        \"x\": [variant_positions[i] for i in snp_idx],\n        \"y\": [variant_quality[i] for i in snp_idx],\n        \"pos_fmt\": [f\"{variant_positions[i]:,}\" for i in snp_idx],\n        \"qual_fmt\": [f\"{variant_quality[i]:.1f}\" for i in snp_idx],\n        \"type\": [\"SNP\"] * len(snp_idx),\n    }\n)\np_var.scatter(\n    x=\"x\",\n    y=\"y\",\n    source=snp_src,\n    size=22,\n    color=COLOR_SNP,\n    alpha=0.9,\n    line_color=PAGE_BG,\n    line_width=2,\n    legend_label=\"SNP\",\n)\n\nindel_idx = [i for i, t in enumerate(variant_types) if t == \"Indel\"]\nindel_src = ColumnDataSource(\n    data={\n        \"x\": [variant_positions[i] for i in indel_idx],\n        \"y\": [variant_quality[i] for i in indel_idx],\n        \"pos_fmt\": [f\"{variant_positions[i]:,}\" for i in indel_idx],\n        \"qual_fmt\": [f\"{variant_quality[i]:.1f}\" for i in indel_idx],\n        \"type\": [\"Indel\"] * len(indel_idx),\n    }\n)\np_var.scatter(\n    x=\"x\",\n    y=\"y\",\n    source=indel_src,\n    size=24,\n    color=COLOR_INDEL,\n    marker=\"diamond\",\n    alpha=0.9,\n    line_color=PAGE_BG,\n    line_width=2,\n    legend_label=\"Indel\",\n)\n\np_var.add_tools(HoverTool(tooltips=[(\"Type\", \"@type\"), (\"Position\", \"@pos_fmt\"), (\"Quality\", \"@qual_fmt\")]))\np_var.add_layout(\n    Label(\n        x=region_start + 1800,\n        y=var_max * 0.87,\n        text=\"Variants\",\n        text_font_size=\"28pt\",\n        text_color=INK_SOFT,\n        text_font_style=\"bold\",\n    )\n)\n\np_var.xaxis.visible = False\np_var.yaxis.axis_label = \"Quality Score\"\np_var.yaxis.axis_label_text_font_size = \"32pt\"\np_var.yaxis.axis_label_text_font_style = \"normal\"\np_var.yaxis.axis_label_text_color = INK\np_var.yaxis.major_label_text_font_size = \"26pt\"\np_var.yaxis.major_label_text_color = INK_SOFT\np_var.yaxis.axis_line_color = INK_SOFT\np_var.yaxis.minor_tick_line_color = None\np_var.yaxis.major_tick_line_color = INK_SOFT\np_var.xgrid.grid_line_color = None\np_var.ygrid.grid_line_color = INK\np_var.ygrid.grid_line_alpha = 0.15\np_var.ygrid.grid_line_dash = [4, 4]\np_var.background_fill_color = PAGE_BG\np_var.border_fill_color = PAGE_BG\np_var.outline_line_color = None\np_var.legend.label_text_font_size = \"26pt\"\np_var.legend.label_text_color = INK_SOFT\np_var.legend.glyph_height = 28\np_var.legend.glyph_width = 28\np_var.legend.spacing = 12\np_var.legend.padding = 16\np_var.legend.background_fill_alpha = 0.9\np_var.legend.background_fill_color = ELEVATED_BG\np_var.legend.border_line_color = INK_SOFT\np_var.legend.location = \"top_right\"\n\n# ============================================================\n# Track 4: Regulatory Elements (bottom — has x-axis labels)\n# ============================================================\np_reg = figure(\n    width=W,\n    height=420,\n    x_range=shared_x_range,\n    y_range=Range1d(-1.2, 1.8),\n    tools=\"\",\n    toolbar_location=None,\n    min_border_top=5,\n    min_border_bottom=200,\n    min_border_left=BL,\n    min_border_right=BR,\n)\n\nreg_src = ColumnDataSource(\n    data={\n        \"left\": [e[\"start\"] for e in reg_elements],\n        \"right\": [e[\"end\"] for e in reg_elements],\n        \"top\": [0.55] * len(reg_elements),\n        \"bottom\": [-0.55] * len(reg_elements),\n        \"type\": [e[\"type\"] for e in reg_elements],\n        \"color\": [reg_color_map[e[\"type\"]] for e in reg_elements],\n        \"start_fmt\": [f\"{e['start']:,}\" for e in reg_elements],\n        \"end_fmt\": [f\"{e['end']:,}\" for e in reg_elements],\n    }\n)\np_reg.quad(\n    left=\"left\",\n    right=\"right\",\n    top=\"top\",\n    bottom=\"bottom\",\n    source=reg_src,\n    fill_color=\"color\",\n    line_color=PAGE_BG,\n    line_width=2,\n    alpha=0.9,\n)\n\np_reg.add_tools(HoverTool(tooltips=[(\"Type\", \"@type\"), (\"Start\", \"@start_fmt\"), (\"End\", \"@end_fmt\")]))\n\n# Legend via invisible glyphs (one per reg type)\nfor reg_type, reg_col in reg_color_map.items():\n    p_reg.quad(\n        left=[0], right=[0], top=[0], bottom=[0], fill_color=reg_col, line_color=reg_col, alpha=0, legend_label=reg_type\n    )\n\np_reg.add_layout(\n    Label(\n        x=region_start + 1800,\n        y=1.25,\n        text=\"Regulatory\",\n        text_font_size=\"28pt\",\n        text_color=INK_SOFT,\n        text_font_style=\"bold\",\n    )\n)\n\np_reg.yaxis.visible = False\np_reg.xaxis.axis_label = f\"Genomic Position ({chrom})\"\np_reg.xaxis.axis_label_text_font_size = \"32pt\"\np_reg.xaxis.axis_label_text_font_style = \"normal\"\np_reg.xaxis.axis_label_text_color = INK\np_reg.xaxis.major_label_text_font_size = \"26pt\"\np_reg.xaxis.major_label_text_color = INK_SOFT\np_reg.xaxis.formatter = NumeralTickFormatter(format=\"0,0\")\np_reg.xaxis.axis_line_width = 2\np_reg.xaxis.axis_line_color = INK_SOFT\np_reg.xaxis.minor_tick_line_color = None\np_reg.xaxis.major_tick_line_color = INK_SOFT\np_reg.xgrid.grid_line_color = None\np_reg.ygrid.grid_line_color = None\np_reg.background_fill_color = PAGE_BG\np_reg.border_fill_color = PAGE_BG\np_reg.outline_line_color = None\np_reg.legend.label_text_font_size = \"26pt\"\np_reg.legend.label_text_color = INK_SOFT\np_reg.legend.glyph_height = 28\np_reg.legend.glyph_width = 36\np_reg.legend.spacing = 12\np_reg.legend.padding = 16\np_reg.legend.background_fill_alpha = 0.9\np_reg.legend.background_fill_color = ELEVATED_BG\np_reg.legend.border_line_color = INK_SOFT\np_reg.legend.location = \"top_right\"\np_reg.legend.orientation = \"horizontal\"\n\n# Stack all four tracks; heights sum to ~1800px with spacing\nlayout = column(p_gene, p_cov, p_var, p_reg, spacing=5)\n\n# Save interactive HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(layout)\n\n# Screenshot with headless Chrome via Selenium — avoids broken export_png chromedriver path.\n# Use a taller window to accommodate browser chrome, then CDP-override viewport to exactly W×H.\nW_PX, H_PX = 3200, 1800\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W_PX},{H_PX + 200}\",\n    \"--hide-scrollbars\",\n    \"--force-device-scale-factor=1\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W_PX, \"height\": H_PX, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}