{"spec_id":"sequence-logo-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nsequence-logo-basic: Sequence Logo for Motif Visualization\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-06-02\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\n# 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\"\n\n# Data - ETS-family transcription factor binding motif (CCGGAAGT core)\nnp.random.seed(42)\n\nfrequencies = [\n    {\"A\": 0.30, \"C\": 0.25, \"G\": 0.20, \"T\": 0.25},  # pos 1: low conservation\n    {\"A\": 0.05, \"C\": 0.80, \"G\": 0.05, \"T\": 0.10},  # pos 2: C dominant\n    {\"A\": 0.02, \"C\": 0.02, \"G\": 0.94, \"T\": 0.02},  # pos 3: G highly conserved\n    {\"A\": 0.02, \"C\": 0.02, \"G\": 0.94, \"T\": 0.02},  # pos 4: G highly conserved\n    {\"A\": 0.90, \"C\": 0.03, \"G\": 0.04, \"T\": 0.03},  # pos 5: A dominant\n    {\"A\": 0.85, \"C\": 0.05, \"G\": 0.05, \"T\": 0.05},  # pos 6: A dominant\n    {\"A\": 0.10, \"C\": 0.10, \"G\": 0.15, \"T\": 0.65},  # pos 7: T dominant\n    {\"A\": 0.25, \"C\": 0.25, \"G\": 0.25, \"T\": 0.25},  # pos 8: no conservation\n    {\"A\": 0.10, \"C\": 0.10, \"G\": 0.10, \"T\": 0.70},  # pos 9: T dominant\n    {\"A\": 0.20, \"C\": 0.30, \"G\": 0.20, \"T\": 0.30},  # pos 10: slight C/T bias\n]\n\nrows = []\nfor pos_idx, freqs in enumerate(frequencies):\n    position = pos_idx + 1\n    entropy = -sum(f * np.log2(f) for f in freqs.values() if f > 0)\n    ic = 2.0 - entropy\n    sorted_letters = sorted(freqs.items(), key=lambda x: x[1])\n    y_start = 0.0\n    for letter, freq in sorted_letters:\n        height = ic * freq\n        rows.append(\n            {\n                \"position\": position,\n                \"letter\": letter,\n                \"height\": round(height, 6),\n                \"y_start\": round(y_start, 6),\n                \"y_end\": round(y_start + height, 6),\n                \"y_mid\": round(y_start + height / 2, 6),\n                \"ic\": round(ic, 4),\n                \"freq\": round(freq, 4),\n                \"is_core\": 2 <= position <= 9,\n            }\n        )\n        y_start += height\n\ndf = pd.DataFrame(rows)\n\n# Standard DNA colors (semantic exception: domain-standard nucleotide convention per spec)\n# A=green, C=blue, G=orange/yellow, T=red\nnuc_colors = {\"A\": \"#2ca02c\", \"C\": \"#1f77b4\", \"G\": \"#F5A623\", \"T\": \"#d62728\"}\ncolor_scale = alt.Scale(domain=[\"A\", \"C\", \"G\", \"T\"], range=list(nuc_colors.values()))\n\n# Hover selection for interactive column highlighting\nposition_hover = alt.selection_point(fields=[\"position\"], on=\"pointerover\", empty=False)\n\ny_scale = alt.Scale(domain=[0, 2.1])\n\n# Stacked colored bars per nucleotide segment\nbars = (\n    alt.Chart(df)\n    .mark_rect(cornerRadius=2)\n    .encode(\n        x=alt.X(\n            \"position:O\",\n            title=\"Position\",\n            axis=alt.Axis(labelFontSize=10, titleFontSize=12, labelAngle=0, tickSize=0, domainWidth=0, titlePadding=10),\n        ),\n        y=alt.Y(\n            \"y_start:Q\",\n            title=\"Information Content (bits)\",\n            scale=y_scale,\n            axis=alt.Axis(\n                labelFontSize=10,\n                titleFontSize=12,\n                grid=True,\n                gridWidth=0.5,\n                tickSize=0,\n                domainWidth=0,\n                titlePadding=10,\n                values=[0, 0.5, 1.0, 1.5, 2.0],\n            ),\n        ),\n        y2=\"y_end:Q\",\n        color=alt.Color(\n            \"letter:N\",\n            scale=color_scale,\n            legend=alt.Legend(\n                title=\"Nucleotide\",\n                titleFontSize=10,\n                labelFontSize=10,\n                orient=\"right\",\n                symbolSize=150,\n                symbolStrokeWidth=0,\n                titlePadding=6,\n                padding=10,\n            ),\n        ),\n        opacity=alt.condition(alt.datum.is_core, alt.value(0.95), alt.value(0.45)),\n        stroke=alt.condition(position_hover, alt.value(INK), alt.value(PAGE_BG)),\n        strokeWidth=alt.condition(position_hover, alt.value(1.5), alt.value(0.4)),\n        tooltip=[\n            alt.Tooltip(\"position:O\", title=\"Position\"),\n            alt.Tooltip(\"letter:N\", title=\"Nucleotide\"),\n            alt.Tooltip(\"freq:Q\", title=\"Frequency\", format=\".0%\"),\n            alt.Tooltip(\"height:Q\", title=\"Height (bits)\", format=\".3f\"),\n            alt.Tooltip(\"ic:Q\", title=\"Total IC (bits)\", format=\".3f\"),\n        ],\n    )\n    .add_params(position_hover)\n)\n\n# Letter glyphs scaled proportional to information height\nletters = (\n    alt.Chart(df)\n    .transform_filter(alt.datum.height > 0.06)\n    .transform_calculate(\n        font_size=\"max(6, min(36, datum.height * 36))\",\n        letter_color=\"datum.letter == 'G' ? '#6B4400' : datum.letter == 'A' ? '#0B5B0B' : datum.letter == 'C' ? '#0A3D6B' : '#8B0000'\",\n    )\n    .mark_text(fontWeight=\"bold\", font=\"Arial Black, Impact, sans-serif\", baseline=\"middle\")\n    .encode(\n        x=\"position:O\",\n        y=alt.Y(\"y_mid:Q\", scale=y_scale),\n        text=\"letter:N\",\n        size=alt.Size(\"font_size:Q\", scale=None, legend=None),\n        color=alt.Color(\"letter_color:N\", scale=None, legend=None),\n        opacity=alt.condition(alt.datum.is_core, alt.value(1.0), alt.value(0.6)),\n    )\n)\n\n# Core region label annotation\ncore_annotation_df = pd.DataFrame([{\"position\": 5, \"y_val\": 1.98, \"label\": \"◀ CCGGAAGT core (pos 2–9) ▶\"}])\ncore_annotation = (\n    alt.Chart(core_annotation_df)\n    .mark_text(fontSize=9, fontWeight=\"bold\", color=INK_MUTED, fontStyle=\"italic\")\n    .encode(x=\"position:O\", y=alt.Y(\"y_val:Q\", scale=y_scale), text=\"label:N\")\n)\n\n# Subtle background shading for core region\ncore_bg_color = \"#C8D8E8\" if THEME == \"light\" else \"#2A3040\"\ncore_bg_df = pd.DataFrame([{\"position\": p, \"y0\": 0.0, \"y1\": 2.1} for p in range(2, 10)])\ncore_bg = (\n    alt.Chart(core_bg_df)\n    .mark_rect(color=core_bg_color, opacity=0.25)\n    .encode(x=\"position:O\", y=alt.Y(\"y0:Q\", scale=y_scale), y2=\"y1:Q\")\n)\n\n# IC summary ticks showing total information content per position\nic_summary_df = df.drop_duplicates(subset=[\"position\"])[[\"position\", \"ic\", \"is_core\"]].copy()\nic_ticks = (\n    alt.Chart(ic_summary_df)\n    .mark_tick(thickness=2, color=INK_MUTED)\n    .encode(\n        x=\"position:O\",\n        y=alt.Y(\"ic:Q\", scale=y_scale),\n        opacity=alt.condition(alt.datum.is_core, alt.value(0.6), alt.value(0.3)),\n    )\n)\n\n# Assemble layered chart — background shading first, then bars, letters, annotations\nchart = (\n    alt.layer(core_bg, bars, letters, ic_ticks, core_annotation)\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        title=alt.Title(\n            \"sequence-logo-basic · python · altair · anyplot.ai\",\n            fontSize=16,\n            fontWeight=\"bold\",\n            anchor=\"middle\",\n            color=INK,\n            subtitle=[\n                \"ETS-family transcription factor binding motif (CCGGAAGT core)\",\n                \"Letter height ∝ information content — taller letters = higher conservation\",\n            ],\n            subtitleFontSize=11,\n            subtitleFontWeight=\"normal\",\n            subtitleColor=INK_SOFT,\n            offset=12,\n        ),\n    )\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n    .configure_axis(\n        domainColor=INK_SOFT, tickColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK, gridColor=INK, gridOpacity=0.15\n    )\n    .configure_title(color=INK)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n    .configure(padding={\"left\": 20, \"right\": 20, \"top\": 16, \"bottom\": 16})\n)\n\n# Save — pad PNG to exact 3200×1800 target (canvas hard rule — landscape)\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\")\nchart.save(f\"plot-{THEME}.html\")\n"}