{"spec_id":"tree-phylogenetic","library":"d3","language":"javascript","code":"// anyplot.ai\n// tree-phylogenetic: Phylogenetic Tree Diagram\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 100, right: 200, bottom: 150, left: 40 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: primate phylogeny from mitochondrial-DNA divergence estimates ---\n// Each `length` is the branch length in millions of years (Mya) since the\n// parent node split off — an ultrametric tree, so every tip lines up at the\n// same cumulative distance from the common ancestor (a molecular-clock read).\nconst tree = {\n  name: \"Common ancestor\",\n  children: [\n    { name: \"Ring-tailed Lemur\", length: 65, clade: 0 },\n    {\n      name: \"Anthropoidea\",\n      length: 25,\n      children: [\n        { name: \"Common Marmoset\", length: 40, clade: 1 },\n        {\n          name: \"Catarrhini\",\n          length: 11,\n          children: [\n            {\n              name: \"Cercopithecidae\",\n              length: 18,\n              children: [\n                { name: \"Olive Baboon\", length: 11, clade: 2 },\n                { name: \"Rhesus Macaque\", length: 11, clade: 2 },\n              ],\n            },\n            {\n              name: \"Hominoidea\",\n              length: 9,\n              children: [\n                { name: \"White-handed Gibbon\", length: 20, clade: 3 },\n                {\n                  name: \"Hominidae\",\n                  length: 5,\n                  children: [\n                    { name: \"Bornean Orangutan\", length: 15, clade: 3 },\n                    {\n                      name: \"Homininae\",\n                      length: 6,\n                      children: [\n                        { name: \"Western Gorilla\", length: 9, clade: 3 },\n                        {\n                          name: \"Homo/Pan\",\n                          length: 3,\n                          children: [\n                            { name: \"Human\", length: 6, clade: 3 },\n                            { name: \"Chimpanzee\", length: 6, clade: 3 },\n                          ],\n                        },\n                      ],\n                    },\n                  ],\n                },\n              ],\n            },\n          ],\n        },\n      ],\n    },\n  ],\n};\n\nconst CLADE_NAMES = [\"Strepsirrhini\", \"New World monkeys\", \"Old World monkeys\", \"Apes\"];\nconst cladeColor = d3.scaleOrdinal().domain([0, 1, 2, 3]).range(t.palette.slice(0, 4));\n\n// --- Hierarchy + phylogram layout -------------------------------------------\nconst root = d3.hierarchy(tree, (d) => d.children);\n\n// cumulative branch length (Mya since the common ancestor); each.() visits\n// in breadth-first order, so a parent's len is always set before its children\nroot.each((d) => {\n  d.len = d.parent ? d.parent.len + d.data.length : 0;\n});\n\n// leaves evenly spaced top-to-bottom in traversal order\nconst leaves = root.leaves();\nleaves.forEach((d, i) => {\n  d.py = (i / (leaves.length - 1)) * ih;\n});\n// internal nodes settle at the midpoint of their children (post-order)\nroot.eachAfter((d) => {\n  if (d.children) d.py = d3.mean(d.children, (c) => c.py);\n});\n\n// a node inherits a clade color only if every descendant shares one clade —\n// this keeps the deep backbone branches neutral and highlights each clade\nroot.eachAfter((d) => {\n  if (!d.children) {\n    d.clade = d.data.clade;\n  } else {\n    const clades = new Set(d.children.map((c) => c.clade));\n    d.clade = clades.size === 1 ? [...clades][0] : null;\n  }\n});\n\nconst maxLen = d3.max(root.descendants(), (d) => d.len);\nconst x = d3.scaleLinear().domain([0, maxLen]).range([0, iw]);\n\n// --- SVG mount ---------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Branches: elbow links (vertical at the parent's age, then horizontal) --\ng.selectAll(\"path.branch\")\n  .data(root.links())\n  .join(\"path\")\n  .attr(\"class\", \"branch\")\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", (d) => (d.target.clade !== null ? cladeColor(d.target.clade) : t.inkSoft))\n  .attr(\"stroke-width\", 2.5)\n  .attr(\"d\", (d) => `M${x(d.source.len)},${d.source.py} V${d.target.py} H${x(d.target.len)}`);\n\n// --- Nodes ---------------------------------------------------------------------\nconst nodes = g\n  .selectAll(\"g.node\")\n  .data(root.descendants())\n  .join(\"g\")\n  .attr(\"class\", \"node\")\n  .attr(\"transform\", (d) => `translate(${x(d.len)},${d.py})`);\n\nnodes\n  .append(\"circle\")\n  .attr(\"r\", (d) => (d.children ? 4 : 6))\n  .attr(\"fill\", (d) => (d.clade !== null ? cladeColor(d.clade) : t.pageBg))\n  .attr(\"stroke\", (d) => (d.clade !== null ? cladeColor(d.clade) : t.inkSoft))\n  .attr(\"stroke-width\", 1.5);\n\n// --- Leaf labels (species names, italicized per taxonomic convention) -------\nnodes\n  .filter((d) => !d.children)\n  .append(\"text\")\n  .attr(\"x\", 12)\n  .attr(\"dy\", \"0.32em\")\n  .style(\"font-size\", \"16px\")\n  .style(\"font-style\", \"italic\")\n  .attr(\"fill\", t.ink)\n  .text((d) => d.data.name);\n\n// --- X axis: divergence time (doubles as the branch-length scale bar) -------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih + 20})`)\n  .call(d3.axisBottom(x).ticks(6));\nxAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nxAxis.selectAll(\"line\").attr(\"stroke\", t.grid);\nxAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 52)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text(\"Divergence time since common ancestor (million years, mtDNA estimate)\");\n\n// --- Clade legend --------------------------------------------------------------\nconst legendSpacing = 240;\nconst legendStartX = (iw - legendSpacing * (CLADE_NAMES.length - 1)) / 2 - 70;\nconst legend = g.append(\"g\").attr(\"transform\", `translate(${legendStartX},${ih + 90})`);\nCLADE_NAMES.forEach((name, i) => {\n  const item = legend.append(\"g\").attr(\"transform\", `translate(${i * legendSpacing},0)`);\n  item.append(\"circle\").attr(\"r\", 6).attr(\"fill\", cladeColor(i));\n  item\n    .append(\"text\")\n    .attr(\"x\", 14)\n    .attr(\"dy\", \"0.32em\")\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\")\n    .text(name);\n});\n\n// --- Title --------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 48)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"tree-phylogenetic · javascript · d3 · anyplot.ai\");\n"}