{"spec_id":"tree-phylogenetic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\ntree-phylogenetic: Phylogenetic Tree Diagram\nLibrary: letsplot 4.9.0 | Python 3.13.13\nQuality: 93/100 | Updated: 2026-05-15\n\"\"\"\n\nimport os\nimport re\n\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\"\nGRID_COLOR = \"rgba(26,26,23,0.10)\" if THEME == \"light\" else \"rgba(240,239,232,0.10)\"\n\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\"]\n\n\ndef parse_newick(newick_str):\n    \"\"\"Parse Newick format string into tree structure.\"\"\"\n    newick_str = newick_str.strip().rstrip(\";\")\n    node_id = [0]\n\n    def parse_node(s, parent_id=None, depth=0):\n        nodes = []\n        s = s.strip()\n\n        if \"(\" not in s:\n            match = re.match(r\"([^:]*):?([\\d.]*)\", s)\n            name = match.group(1) if match else s\n            length = float(match.group(2)) if match and match.group(2) else 0.1\n            node_id[0] += 1\n            return [\n                {\"id\": node_id[0], \"name\": name, \"length\": length, \"parent\": parent_id, \"depth\": depth, \"children\": []}\n            ]\n\n        if s.startswith(\"(\"):\n            level = 0\n            children_str = \"\"\n            remaining = \"\"\n            for i, c in enumerate(s):\n                if c == \"(\":\n                    level += 1\n                elif c == \")\":\n                    level -= 1\n                    if level == 0:\n                        children_str = s[1:i]\n                        remaining = s[i + 1 :]\n                        break\n\n            match = re.match(r\":?([\\d.]*)\", remaining)\n            length = float(match.group(1)) if match and match.group(1) else 0.1\n\n            node_id[0] += 1\n            current_id = node_id[0]\n            current_node = {\n                \"id\": current_id,\n                \"name\": \"\",\n                \"length\": length,\n                \"parent\": parent_id,\n                \"depth\": depth,\n                \"children\": [],\n            }\n            nodes.append(current_node)\n\n            children = []\n            level = 0\n            current = \"\"\n            for c in children_str:\n                if c == \"(\":\n                    level += 1\n                elif c == \")\":\n                    level -= 1\n                if c == \",\" and level == 0:\n                    children.append(current.strip())\n                    current = \"\"\n                else:\n                    current += c\n            if current.strip():\n                children.append(current.strip())\n\n            for child_str in children:\n                child_nodes = parse_node(child_str, current_id, depth + 1)\n                nodes.extend(child_nodes)\n                current_node[\"children\"].extend([n[\"id\"] for n in child_nodes if n[\"parent\"] == current_id])\n\n        return nodes\n\n    return parse_node(newick_str)\n\n\nnewick = \"((((Human:0.1,Chimpanzee:0.12):0.08,Gorilla:0.2):0.15,(Orangutan:0.25,Gibbon:0.28):0.1):0.2,(Macaque:0.35,(Baboon:0.3,Mandrill:0.32):0.05):0.15)\"\n\nnodes = parse_newick(newick)\n\nnode_dict = {n[\"id\"]: n for n in nodes}\n\n\ndef calc_x_positions(node_dict):\n    root = [n for n in node_dict.values() if n[\"parent\"] is None][0]\n\n    def assign_x(node_id, parent_x=0):\n        node = node_dict[node_id]\n        node[\"x\"] = parent_x + node[\"length\"]\n        for child_id in node[\"children\"]:\n            assign_x(child_id, node[\"x\"])\n\n    assign_x(root[\"id\"], 0)\n\n\ndef calc_y_positions(node_dict):\n    leaves = [n for n in node_dict.values() if not n[\"children\"]]\n    leaves.sort(key=lambda n: n[\"id\"])\n\n    for i, leaf in enumerate(leaves):\n        leaf[\"y\"] = i\n\n    def get_y(node_id):\n        node = node_dict[node_id]\n        if \"y\" in node:\n            return node[\"y\"]\n        child_ys = [get_y(cid) for cid in node[\"children\"]]\n        node[\"y\"] = sum(child_ys) / len(child_ys)\n        return node[\"y\"]\n\n    for node in node_dict.values():\n        get_y(node[\"id\"])\n\n\ncalc_x_positions(node_dict)\ncalc_y_positions(node_dict)\n\nsegments = []\nfor node in node_dict.values():\n    if node[\"parent\"] is not None:\n        parent = node_dict[node[\"parent\"]]\n        segments.append({\"x\": parent[\"x\"], \"xend\": node[\"x\"], \"y\": node[\"y\"], \"yend\": node[\"y\"], \"type\": \"horizontal\"})\n        segments.append(\n            {\"x\": parent[\"x\"], \"xend\": parent[\"x\"], \"y\": parent[\"y\"], \"yend\": node[\"y\"], \"type\": \"vertical\"}\n        )\n\ndf_segments = pd.DataFrame(segments)\n\nleaves = [n for n in node_dict.values() if not n[\"children\"]]\ndf_labels = pd.DataFrame([{\"x\": n[\"x\"] + 0.02, \"y\": n[\"y\"], \"label\": n[\"name\"]} for n in leaves])\n\ndf_nodes = pd.DataFrame([{\"x\": n[\"x\"], \"y\": n[\"y\"]} for n in node_dict.values()])\n\nclade_colors = {\n    \"Human\": IMPRINT[0],\n    \"Chimpanzee\": IMPRINT[0],\n    \"Gorilla\": IMPRINT[0],\n    \"Orangutan\": IMPRINT[1],\n    \"Gibbon\": IMPRINT[1],\n    \"Macaque\": IMPRINT[2],\n    \"Baboon\": IMPRINT[2],\n    \"Mandrill\": IMPRINT[2],\n}\n\ndf_labels[\"color\"] = df_labels[\"label\"].map(clade_colors)\n\nanyplot_theme = theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_grid_major_x=element_line(color=GRID_COLOR, size=0.3),\n    panel_grid_major_y=element_blank(),\n    panel_grid_minor=element_blank(),\n    axis_title_x=element_text(size=20, color=INK),\n    axis_title_y=element_blank(),\n    axis_text_x=element_text(size=16, color=INK_SOFT),\n    axis_text_y=element_blank(),\n    axis_ticks_y=element_blank(),\n    axis_line_y=element_blank(),\n    plot_title=element_text(size=24, face=\"bold\", color=INK),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_text=element_text(size=16, color=INK_SOFT),\n)\n\nplot = (\n    ggplot()\n    + geom_segment(aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), data=df_segments, color=IMPRINT[0], size=1.5)\n    + geom_point(aes(x=\"x\", y=\"y\"), data=df_nodes, color=IMPRINT[0], size=4)\n    + geom_point(aes(x=\"x\", y=\"y\", color=\"color\"), data=df_labels, size=6, show_legend=False)\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=df_labels, hjust=0, size=14, color=INK_SOFT, family=\"sans-serif\")\n    + scale_color_identity()\n    + scale_x_continuous(limits=[0, 0.85])\n    + labs(\n        title=\"Primate Evolution · tree-phylogenetic · letsplot · anyplot.ai\",\n        x=\"Evolutionary Distance (substitutions per site)\",\n    )\n    + theme_minimal()\n    + anyplot_theme\n    + ggsize(1600, 900)\n)\n\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=3)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}