{"spec_id":"sequence-logo-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nsequence-logo-basic: Sequence Logo for Motif Visualization\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 83/100 | Updated: 2026-06-02\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\n\n\nLetsPlot.setup_html()\n\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\"\nRULE = \"rgba(26,26,23,0.15)\" if THEME == \"light\" else \"rgba(240,239,232,0.15)\"\n\n# Imprint palette members for DNA nucleotides (A=green, C=blue, G=ochre, T=red)\ncolor_map = {\"A\": \"#009E73\", \"C\": \"#4467A3\", \"G\": \"#BD8233\", \"T\": \"#AE3030\"}\n\n# 10-position TATA-box transcription factor binding site motif\npositions = list(range(1, 11))\nfrequencies = {\n    1: {\"A\": 0.25, \"C\": 0.25, \"G\": 0.25, \"T\": 0.25},\n    2: {\"A\": 0.10, \"C\": 0.05, \"G\": 0.05, \"T\": 0.80},\n    3: {\"A\": 0.85, \"C\": 0.05, \"G\": 0.05, \"T\": 0.05},\n    4: {\"A\": 0.05, \"C\": 0.05, \"G\": 0.05, \"T\": 0.85},\n    5: {\"A\": 0.90, \"C\": 0.02, \"G\": 0.02, \"T\": 0.06},\n    6: {\"A\": 0.60, \"C\": 0.05, \"G\": 0.05, \"T\": 0.30},\n    7: {\"A\": 0.15, \"C\": 0.05, \"G\": 0.70, \"T\": 0.10},\n    8: {\"A\": 0.05, \"C\": 0.80, \"G\": 0.10, \"T\": 0.05},\n    9: {\"A\": 0.30, \"C\": 0.30, \"G\": 0.20, \"T\": 0.20},\n    10: {\"A\": 0.25, \"C\": 0.25, \"G\": 0.25, \"T\": 0.25},\n}\n\n# Calculate information content and build stacked letter segments\nrows = []\nmax_info = 0.0\nfor pos in positions:\n    freqs = frequencies[pos]\n    entropy = -sum(f * np.log2(f) for f in freqs.values() if f > 0)\n    info_content = 2.0 - entropy\n\n    # Stack lowest-frequency letters at base, highest on top\n    sorted_letters = sorted(freqs.items(), key=lambda x: x[1])\n    y_bottom = 0.0\n    for letter, freq in sorted_letters:\n        height = freq * info_content\n        if height < 0.02:\n            y_bottom += height\n            continue\n        rows.append(\n            {\n                \"position\": pos,\n                \"xmin\": pos - 0.45,\n                \"xmax\": pos + 0.45,\n                \"ymin\": y_bottom,\n                \"ymax\": y_bottom + height,\n                \"ymid\": y_bottom + height / 2,\n                \"height\": height,\n                \"letter\": letter,\n                \"frequency\": freq,\n                \"info_bits\": round(info_content, 3),\n            }\n        )\n        y_bottom += height\n    if info_content > max_info:\n        max_info = info_content\n\ndf = pd.DataFrame(rows)\n# Only label blocks tall enough to show text legibly\ndf_labeled = df[df[\"height\"] > 0.08].copy()\n# Scale text size proportional to block height so letters visually fill allocated space\ndf_labeled[\"text_size\"] = (df_labeled[\"height\"] / max_info * 9).clip(2, 9)\n\n# Invisible points for building a proper fill legend with square symbols\nlegend_df = pd.DataFrame({\"x\": [0] * 4, \"y\": [0] * 4, \"letter\": [\"A\", \"C\", \"G\", \"T\"]})\n\ny_max = np.ceil(max_info * 5) / 5 + 0.05\n\ntitle = \"sequence-logo-basic · python · letsplot · anyplot.ai\"\ntitle_size = round(16 * min(1.0, 67 / len(title)))\n\nplot = (\n    ggplot()\n    # Solid colored blocks fill their allocated height — primary sequence logo encoding\n    + geom_rect(\n        aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\", fill=\"letter\"),\n        data=df,\n        alpha=0.90,\n        color=PAGE_BG,\n        size=0.5,\n        show_legend=False,\n    )\n    # Letter labels with size proportional to block height so they visually fill allocated space\n    + geom_text(\n        aes(x=\"position\", y=\"ymid\", label=\"letter\", size=\"text_size\"),\n        data=df_labeled,\n        fontface=\"bold\",\n        color=\"white\",\n        show_legend=False,\n        tooltips=layer_tooltips()\n        .format(\"@frequency\", \".0%\")\n        .format(\"@info_bits\", \".3f\")\n        .line(\"@letter\")\n        .line(\"Frequency: @frequency\")\n        .line(\"Info content: @info_bits bits\"),\n    )\n    + scale_size_identity()\n    # Invisible points — carry fill mapping so the legend renders colored squares\n    + geom_point(\n        aes(x=\"x\", y=\"y\", fill=\"letter\"),\n        data=legend_df,\n        size=6,\n        shape=22,\n        color=\"rgba(0,0,0,0)\",\n        alpha=0,\n        tooltips=\"none\",\n    )\n    + scale_fill_manual(values=color_map, name=\"Nucleotide\", breaks=[\"A\", \"C\", \"G\", \"T\"])\n    + scale_x_continuous(breaks=positions, limits=[0.3, 10.7])\n    + scale_y_continuous(limits=[0, y_max], breaks=[0.0, 0.5, 1.0, 1.5, 2.0])\n    + guides(fill=guide_legend(override_aes={\"size\": 12, \"alpha\": 1.0}))\n    + labs(x=\"Position\", y=\"Information content (bits)\", title=title)\n    + theme_minimal()\n    + theme(\n        plot_title=element_text(size=title_size, face=\"bold\", color=INK),\n        axis_title=element_text(size=12, color=INK),\n        axis_text=element_text(size=10, color=INK_SOFT),\n        legend_title=element_text(size=11, face=\"bold\", color=INK),\n        legend_text=element_text(size=10, color=INK_SOFT),\n        axis_line=element_line(color=INK_SOFT, size=0.5),\n        panel_grid_major_x=element_blank(),\n        panel_grid_minor=element_blank(),\n        panel_grid_major_y=element_line(color=RULE, size=0.5),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    )\n    + ggsize(800, 450)\n)\n\nggsave(plot, f\"plot-{THEME}.png\", scale=4, path=\".\")\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}