{"spec_id":"tree-phylogenetic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\ntree-phylogenetic: Phylogenetic Tree Diagram\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 94/100 | Updated: 2026-05-15\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\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# Okabe-Ito palette for clade coloring\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\"]\n\n# Primate phylogenetic tree with clade assignments\nnp.random.seed(42)\n\nedges = [\n    (\"Root\", \"Hominoidea\", 0.15, \"Apes\"),\n    (\"Root\", \"Cercopithecidae\", 0.18, \"Old World Monkeys\"),\n    (\"Hominoidea\", \"Hominidae\", 0.08, \"Apes\"),\n    (\"Hominoidea\", \"Hylobatidae\", 0.12, \"Lesser Apes\"),\n    (\"Hominidae\", \"Homininae\", 0.05, \"Apes\"),\n    (\"Hominidae\", \"Pongo pygmaeus\", 0.09, \"Apes\"),\n    (\"Homininae\", \"Homo sapiens\", 0.03, \"Apes\"),\n    (\"Homininae\", \"Pan\", 0.02, \"Apes\"),\n    (\"Pan\", \"Pan troglodytes\", 0.015, \"Apes\"),\n    (\"Pan\", \"Pan paniscus\", 0.015, \"Apes\"),\n    (\"Hylobatidae\", \"Hylobates lar\", 0.06, \"Lesser Apes\"),\n    (\"Cercopithecidae\", \"Macaca mulatta\", 0.10, \"Old World Monkeys\"),\n    (\"Cercopithecidae\", \"Papio anubis\", 0.11, \"Old World Monkeys\"),\n]\n\nleaf_nodes = {\n    \"Homo sapiens\": \"Human\",\n    \"Pan troglodytes\": \"Chimpanzee\",\n    \"Pan paniscus\": \"Bonobo\",\n    \"Pongo pygmaeus\": \"Orangutan\",\n    \"Hylobates lar\": \"Gibbon\",\n    \"Macaca mulatta\": \"Rhesus Macaque\",\n    \"Papio anubis\": \"Olive Baboon\",\n}\n\n# Build tree structure\nchildren = {}\nbranch_lengths = {}\nclade_map = {}\nfor parent, child, length, clade in edges:\n    if parent not in children:\n        children[parent] = []\n    children[parent].append(child)\n    branch_lengths[(parent, child)] = length\n    clade_map[child] = clade\n\n\n# Get leaf nodes\ndef get_leaves(node):\n    if node not in children:\n        return [node]\n    leaves = []\n    for child in children[node]:\n        leaves.extend(get_leaves(child))\n    return leaves\n\n\nall_leaves = get_leaves(\"Root\")\nn_leaves = len(all_leaves)\nleaf_y = {leaf: i for i, leaf in enumerate(all_leaves)}\n\n\n# Calculate positions\ndef calc_x_positions(node, current_x=0):\n    positions = {node: current_x}\n    if node in children:\n        for child in children[node]:\n            child_x = current_x + branch_lengths[(node, child)]\n            positions.update(calc_x_positions(child, child_x))\n    return positions\n\n\ndef calc_y_positions(node):\n    if node not in children:\n        return {node: leaf_y[node]}\n    positions = {}\n    child_ys = []\n    for child in children[node]:\n        child_positions = calc_y_positions(child)\n        positions.update(child_positions)\n        child_ys.append(child_positions[child])\n    positions[node] = np.mean(child_ys)\n    return positions\n\n\nx_positions = calc_x_positions(\"Root\")\ny_positions = calc_y_positions(\"Root\")\n\n# Build line segments\nlines_data = []\nfor parent, child, _length, clade in edges:\n    parent_x = x_positions[parent]\n    parent_y = y_positions[parent]\n    child_x = x_positions[child]\n    child_y = y_positions[child]\n    clade_idx = [\"Apes\", \"Old World Monkeys\", \"Lesser Apes\"].index(clade)\n    color = IMPRINT[clade_idx]\n\n    lines_data.append(\n        {\n            \"x\": parent_x,\n            \"y\": parent_y,\n            \"x2\": parent_x,\n            \"y2\": child_y,\n            \"type\": \"vertical\",\n            \"clade\": clade,\n            \"color\": color,\n        }\n    )\n    lines_data.append(\n        {\n            \"x\": parent_x,\n            \"y\": child_y,\n            \"x2\": child_x,\n            \"y2\": child_y,\n            \"type\": \"horizontal\",\n            \"clade\": clade,\n            \"color\": color,\n        }\n    )\n\nlines_df = pd.DataFrame(lines_data)\n\n# Leaf nodes data\nnodes_data = []\nfor node in all_leaves:\n    label = leaf_nodes.get(node, node)\n    clade = clade_map.get(node, \"Unknown\")\n    clade_idx = (\n        [\"Apes\", \"Old World Monkeys\", \"Lesser Apes\"].index(clade)\n        if clade in [\"Apes\", \"Old World Monkeys\", \"Lesser Apes\"]\n        else 0\n    )\n    color = IMPRINT[clade_idx]\n    nodes_data.append(\n        {\n            \"x\": x_positions[node],\n            \"y\": y_positions[node],\n            \"label\": label,\n            \"species\": node,\n            \"clade\": clade,\n            \"color\": color,\n        }\n    )\n\nnodes_df = pd.DataFrame(nodes_data)\n\n# Internal nodes data\ninternal_nodes = [n for n in x_positions.keys() if n not in all_leaves and n != \"Root\"]\ninternal_data = []\nfor n in internal_nodes:\n    clade = clade_map.get(n, \"Unknown\")\n    clade_idx = (\n        [\"Apes\", \"Old World Monkeys\", \"Lesser Apes\"].index(clade)\n        if clade in [\"Apes\", \"Old World Monkeys\", \"Lesser Apes\"]\n        else 0\n    )\n    color = IMPRINT[clade_idx]\n    internal_data.append({\"x\": x_positions[n], \"y\": y_positions[n], \"name\": n, \"clade\": clade, \"color\": color})\ninternal_df = pd.DataFrame(internal_data)\n\n# Create branches with clade colors\nbranches = (\n    alt.Chart(lines_df)\n    .mark_rule(strokeWidth=3.5)\n    .encode(\n        x=\"x:Q\",\n        y=\"y:Q\",\n        x2=\"x2:Q\",\n        y2=\"y2:Q\",\n        color=alt.Color(\"color:N\", scale=alt.Scale(domain=IMPRINT, range=IMPRINT), legend=None),\n        tooltip=[\"clade:N\"],\n    )\n)\n\n# Create leaf node points\nleaf_points = (\n    alt.Chart(nodes_df)\n    .mark_circle(size=600)\n    .encode(\n        x=\"x:Q\",\n        y=\"y:Q\",\n        color=alt.Color(\"color:N\", scale=alt.Scale(domain=IMPRINT, range=IMPRINT), legend=None),\n        tooltip=[\"species:N\", \"label:N\", \"clade:N\"],\n    )\n)\n\n# Create leaf labels\nleaf_labels = (\n    alt.Chart(nodes_df)\n    .mark_text(align=\"left\", baseline=\"middle\", dx=12, fontSize=20, fontWeight=\"bold\")\n    .encode(x=\"x:Q\", y=\"y:Q\", text=\"label:N\", color=alt.value(INK))\n)\n\n# Create internal node points (larger now)\ninternal_points = (\n    alt.Chart(internal_df)\n    .mark_circle(size=350)\n    .encode(\n        x=\"x:Q\",\n        y=\"y:Q\",\n        color=alt.Color(\"color:N\", scale=alt.Scale(domain=IMPRINT, range=IMPRINT), legend=None),\n        tooltip=[\"name:N\", \"clade:N\"],\n    )\n)\n\n# Create scale bar\nmax_x = max(x_positions.values())\nscale_bar_length = 0.05\nscale_bar_data = pd.DataFrame([{\"x\": 0.02, \"y\": -0.8, \"x2\": 0.02 + scale_bar_length, \"y2\": -0.8}])\nscale_bar = (\n    alt.Chart(scale_bar_data)\n    .mark_rule(strokeWidth=3.5)\n    .encode(x=\"x:Q\", y=\"y:Q\", x2=\"x2:Q\", y2=\"y2:Q\", color=alt.value(INK_SOFT))\n)\n\nscale_bar_label = (\n    alt.Chart(pd.DataFrame([{\"x\": 0.02 + scale_bar_length / 2, \"y\": -1.2, \"text\": \"0.05 subs/site\"}]))\n    .mark_text(fontSize=16, color=INK_SOFT)\n    .encode(x=\"x:Q\", y=\"y:Q\", text=\"text:N\")\n)\n\n# Combine layers\nchart = (\n    alt.layer(branches, internal_points, leaf_points, leaf_labels, scale_bar, scale_bar_label)\n    .properties(\n        width=1600,\n        height=900,\n        background=PAGE_BG,\n        title=alt.Title(\n            \"Primate Evolution · tree-phylogenetic · altair · anyplot.ai\",\n            fontSize=28,\n            anchor=\"middle\",\n            color=INK,\n            subtitle=\"Phylogenetic relationships with evolutionary distance\",\n            subtitleFontSize=20,\n            subtitleColor=INK_SOFT,\n        ),\n    )\n    .configure_axis(\n        labelFontSize=18,\n        titleFontSize=22,\n        titleColor=INK,\n        labelColor=INK_SOFT,\n        domainColor=INK_SOFT,\n        gridColor=INK_SOFT,\n        gridOpacity=0.10,\n    )\n    .configure_title(color=INK)\n    .configure_view(fill=PAGE_BG, stroke=None)\n)\n\n# Set axes\nchart = chart.encode(\n    x=alt.X(\n        \"x:Q\", title=\"Evolutionary Distance (substitutions per site)\", scale=alt.Scale(domain=[-0.02, max_x + 0.15])\n    ),\n    y=alt.Y(\n        \"y:Q\",\n        title=\"\",\n        scale=alt.Scale(domain=[-1.5, n_leaves - 0.5]),\n        axis=alt.Axis(labels=False, ticks=False, domain=False),\n    ),\n)\n\n# Save outputs\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}