{"spec_id":"sequence-logo-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nsequence-logo-basic: Sequence Logo for Motif Visualization\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-06-02\n\"\"\"\n\nimport sys\n\n\n# Script is named bokeh.py; move its directory to end of path so the\n# real bokeh package in site-packages is found first.\nif sys.path and sys.path[0] != \"\":\n    sys.path.append(sys.path.pop(0))\n\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import BoxAnnotation, ColumnDataSource, HoverTool, Label, Legend, LegendItem, Range1d, Span\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\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# DNA base colors — Imprint palette, semantic mapping (A=green, C=blue, G=ochre, T=red)\nBASE_COLORS = {\n    \"A\": \"#009E73\",  # Imprint green (brand)\n    \"C\": \"#4467A3\",  # Imprint blue\n    \"G\": \"#BD8233\",  # Imprint ochre\n    \"T\": \"#AE3030\",  # Imprint matte red\n}\n\n# Data: CREB1 transcription factor binding site motif (10 positions)\npositions = list(range(1, 11))\nbases = [\"A\", \"C\", \"G\", \"T\"]\n\nfrequencies = np.array(\n    [\n        [0.25, 0.15, 0.35, 0.25],  # Pos 1: weak preference\n        [0.10, 0.10, 0.10, 0.70],  # Pos 2: strong T\n        [0.05, 0.05, 0.85, 0.05],  # Pos 3: strong G\n        [0.80, 0.05, 0.10, 0.05],  # Pos 4: strong A\n        [0.05, 0.80, 0.05, 0.10],  # Pos 5: strong C\n        [0.05, 0.05, 0.80, 0.10],  # Pos 6: strong G\n        [0.15, 0.10, 0.10, 0.65],  # Pos 7: strong T\n        [0.05, 0.80, 0.10, 0.05],  # Pos 8: strong C\n        [0.70, 0.10, 0.10, 0.10],  # Pos 9: strong A\n        [0.30, 0.20, 0.25, 0.25],  # Pos 10: weak preference\n    ]\n)\n\n# Information content per position: IC = log2(4) - Shannon_entropy\nmax_bits = np.log2(len(bases))\nentropy = np.array([-np.sum(f * np.log2(np.where(f > 0, f, 1))) for f in frequencies])\ninformation_content = max_bits - entropy\n\n# Build glyph data for colored rectangles and letter overlays\nrect_x, rect_y, rect_w, rect_h, rect_color = [], [], [], [], []\nrect_base, rect_freq, rect_ic, rect_pos = [], [], [], []\ntext_x, text_y, text_letter, text_size = [], [], [], []\n\nmax_ic = float(np.max(information_content))\ny_top = max_ic + 0.10\n\n# Canvas: 3200×1800, borders: top=110, bottom=160 → effective plot height ≈ 1530 px\nPLOT_PX_HEIGHT = 1530.0\nPX_PER_UNIT = PLOT_PX_HEIGHT / y_top\n# CSS 1pt ≈ 1.333 px; cap-height ≈ 70% of em; fill_factor=0.85 for visual fit\nPT_SCALE = PX_PER_UNIT / (1.333 * 0.70)\n# Show letter glyphs only for rectangle heights large enough to be legible\nMIN_RECT_HEIGHT = 0.005\nMIN_TEXT_HEIGHT = 0.04\nCOLUMN_WIDTH = 0.82\n# Width-based cap: letters must not overflow column boundaries\n# X inner width: 3200 - 180(left) - 80(right) = 2940 px; x range = 10.7 - 0.3 = 10.4 units\nX_INNER_PX = 3200 - 180 - 80\nX_DATA_RANGE = 10.7 - 0.3\nX_PX_PER_UNIT = X_INNER_PX / X_DATA_RANGE\nWIDTH_BASED_PT = int(COLUMN_WIDTH * X_PX_PER_UNIT / 1.333)\n\nfor i, pos in enumerate(positions):\n    ic = information_content[i]\n    freqs = frequencies[i]\n    # Ascending sort: least frequent letter at bottom of stack\n    sorted_indices = np.argsort(freqs)\n    y_bottom = 0.0\n\n    for idx in sorted_indices:\n        letter = bases[idx]\n        height = freqs[idx] * ic\n        if height < MIN_RECT_HEIGHT:\n            y_bottom += height\n            continue\n\n        center_y = y_bottom + height / 2\n\n        rect_x.append(pos)\n        rect_y.append(center_y)\n        rect_w.append(COLUMN_WIDTH)\n        rect_h.append(height)\n        rect_color.append(BASE_COLORS[letter])\n        rect_base.append(letter)\n        rect_freq.append(f\"{freqs[idx]:.0%}\")\n        rect_ic.append(f\"{height:.3f}\")\n        rect_pos.append(str(pos))\n\n        # Only overlay letter text when the bar is tall enough to read\n        if height >= MIN_TEXT_HEIGHT:\n            text_x.append(pos)\n            text_y.append(center_y)\n            text_letter.append(letter)\n            font_pt = max(14, min(int(height * PT_SCALE * 0.85), WIDTH_BASED_PT))\n            text_size.append(f\"{font_pt}pt\")\n\n        y_bottom += height\n\n# Title — scale fontsize down for 71-char title (floor 34pt per bokeh prompt)\ntitle_text = \"CREB1 Binding Motif · sequence-logo-basic · python · bokeh · anyplot.ai\"\ntitle_n = len(title_text)\ntitle_fontsize = f\"{max(34, round(50 * 67 / title_n))}pt\"\n\n# Plot\np = figure(\n    width=3200,\n    height=1800,\n    title=title_text,\n    x_axis_label=\"Position\",\n    y_axis_label=\"Information content (bits)\",\n    toolbar_location=None,\n    x_range=Range1d(0.3, 10.7),\n    y_range=Range1d(-0.02, y_top),\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=80,\n)\n\n# Colored rectangles — the primary visual element of the sequence logo\nrect_source = ColumnDataSource(\n    data={\n        \"x\": rect_x,\n        \"y\": rect_y,\n        \"width\": rect_w,\n        \"height\": rect_h,\n        \"color\": rect_color,\n        \"base\": rect_base,\n        \"freq\": rect_freq,\n        \"ic\": rect_ic,\n        \"pos\": rect_pos,\n    }\n)\nrects = p.rect(\n    x=\"x\",\n    y=\"y\",\n    width=\"width\",\n    height=\"height\",\n    source=rect_source,\n    fill_color=\"color\",\n    fill_alpha=0.92,\n    line_color=PAGE_BG,\n    line_width=1.0,\n)\n\n# HoverTool — interactive tooltips for the HTML artifact\nhover_tool = HoverTool(\n    renderers=[rects],\n    tooltips=[(\"Position\", \"@pos\"), (\"Base\", \"@base\"), (\"Frequency\", \"@freq\"), (\"IC contribution\", \"@ic bits\")],\n)\np.add_tools(hover_tool)\n\n# White letter glyphs centered on each visible rectangle\ntext_source = ColumnDataSource(data={\"x\": text_x, \"y\": text_y, \"text\": text_letter, \"size\": text_size})\np.text(\n    x=\"x\",\n    y=\"y\",\n    text=\"text\",\n    source=text_source,\n    text_color=\"white\",\n    text_font_size=\"size\",\n    text_font_style=\"bold\",\n    text_align=\"center\",\n    text_baseline=\"middle\",\n)\n\n# Legend — off-screen dummy glyphs map each base to its color\nlegend_items = []\nfor base in bases:\n    src = ColumnDataSource(data={\"x\": [-9999], \"y\": [-9999]})\n    r = p.rect(\n        x=\"x\", y=\"y\", width=0.01, height=0.01, source=src, fill_color=BASE_COLORS[base], line_color=BASE_COLORS[base]\n    )\n    legend_items.append(LegendItem(label=base, renderers=[r]))\n\nlegend = Legend(\n    items=legend_items,\n    location=\"top_right\",\n    label_text_font_size=\"34pt\",\n    label_text_color=INK_SOFT,\n    glyph_width=50,\n    glyph_height=50,\n    spacing=14,\n    padding=20,\n    margin=20,\n    background_fill_color=ELEVATED_BG,\n    background_fill_alpha=0.9,\n    border_line_color=None,\n)\np.add_layout(legend, \"right\")\n\n# Theme-adaptive chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\n\np.title.text_font_size = title_fontsize\np.title.text_font_style = \"bold\"\np.title.text_color = INK\n\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\n\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\n\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\np.xaxis.minor_tick_line_color = None\np.yaxis.minor_tick_line_color = None\n\np.xaxis.ticker = positions\np.xgrid.grid_line_color = None\np.ygrid.grid_line_color = INK\np.ygrid.grid_line_alpha = 0.15\n\n# Reference line at IC=1 bit — helps readers interpret the conservation scale\nref_line = Span(location=1.0, dimension=\"width\", line_color=INK_SOFT, line_alpha=0.5, line_width=2, line_dash=\"dashed\")\np.add_layout(ref_line)\nref_label = Label(\n    x=10.55,\n    y=1.03,\n    text=\"1 bit\",\n    text_color=INK_SOFT,\n    text_font_size=\"28pt\",\n    text_font_style=\"italic\",\n    text_align=\"right\",\n)\np.add_layout(ref_label)\n\n# Subtle highlight for the conserved core positions (IC ≥ 0.9)\nfor i, pos in enumerate(positions):\n    if information_content[i] >= 0.9:\n        p.add_layout(BoxAnnotation(left=pos - 0.45, right=pos + 0.45, fill_color=INK, fill_alpha=0.04, line_color=None))\n\n# Save interactive HTML (catalog artifact)\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome — Selenium 4 auto-resolves driver\nW, H = 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},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\n\n# Headless Chrome reserves ~139 px for internal UI even with no visible toolbar.\n# Measure the actual viewport height and compensate so the rendered figure\n# fills exactly W × H pixels before taking the screenshot.\ninner_h = driver.execute_script(\"return window.innerHeight\")\nif inner_h != H:\n    driver.set_window_size(W, H + (H - inner_h))\n    time.sleep(1)\n\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}