{"spec_id":"tree-phylogenetic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// tree-phylogenetic: Phylogenetic Tree Diagram\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-09\n\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: illustrative primate phylogeny (mitochondrial-DNA style) --------\n// Newick-equivalent topology (branch lengths in relative divergence units):\n// (((Lemur:3.0,Loris:3.0)Strepsirrhini:1.0,(Tarsier:5.0,((Marmoset:4.0,\n// SquirrelMonkey:4.2)Platyrrhini:0.8,((Macaque:3.2,Baboon:3.4)\n// Cercopithecidae:0.6,(Gibbon:3.0,(Orangutan:2.6,(Gorilla:2.0,\n// (Chimpanzee:1.6,Human:1.7)Hominini:0.2)Homininae:0.3)Hominidae:0.4)\n// Hominoidea:0.6)Catarrhini:0.7)Simiiformes:0.5)Haplorhini:1.0)Root;\nconst tree = {\n  length: 0,\n  children: [\n    {\n      // Strepsirrhini\n      clade: \"strepsirrhini\",\n      length: 1.0,\n      children: [\n        { name: \"Ring-tailed Lemur\", length: 3.0 },\n        { name: \"Slender Loris\", length: 3.0 },\n      ],\n    },\n    {\n      // Haplorhini\n      length: 1.0,\n      children: [\n        { name: \"Philippine Tarsier\", length: 5.0 },\n        {\n          // Simiiformes\n          length: 0.5,\n          children: [\n            {\n              // Platyrrhini\n              clade: \"platyrrhini\",\n              length: 0.8,\n              children: [\n                { name: \"Common Marmoset\", length: 4.0 },\n                { name: \"Squirrel Monkey\", length: 4.2 },\n              ],\n            },\n            {\n              // Catarrhini\n              length: 0.7,\n              children: [\n                {\n                  // Cercopithecidae\n                  clade: \"cercopithecidae\",\n                  length: 0.6,\n                  children: [\n                    { name: \"Rhesus Macaque\", length: 3.2 },\n                    { name: \"Olive Baboon\", length: 3.4 },\n                  ],\n                },\n                {\n                  // Hominoidea\n                  length: 0.6,\n                  children: [\n                    { name: \"Lar Gibbon\", length: 3.0 },\n                    {\n                      // Hominidae (great apes)\n                      clade: \"hominidae\",\n                      length: 0.4,\n                      children: [\n                        { name: \"Bornean Orangutan\", length: 2.6 },\n                        {\n                          // Homininae\n                          length: 0.3,\n                          children: [\n                            { name: \"Western Gorilla\", length: 2.0 },\n                            {\n                              // Hominini\n                              length: 0.2,\n                              children: [\n                                { name: \"Chimpanzee\", length: 1.6 },\n                                { name: \"Human\", length: 1.7 },\n                              ],\n                            },\n                          ],\n                        },\n                      ],\n                    },\n                  ],\n                },\n              ],\n            },\n          ],\n        },\n      ],\n    },\n  ],\n};\n\n// --- Layout: cumulative branch length -> x, leaf order -> y ---------------\nlet leafCounter = 0;\nlet maxX = 0;\nfunction layout(node, parentX) {\n  node.x = parentX + node.length;\n  maxX = Math.max(maxX, node.x);\n  if (node.children) {\n    node.children.forEach((c) => layout(c, node.x));\n    const ys = node.children.map((c) => c.y);\n    node.y = (Math.min(...ys) + Math.max(...ys)) / 2;\n  } else {\n    node.y = leafCounter++;\n  }\n}\nlayout(tree, 0);\nconst n = leafCounter;\nconst maxXTick = Math.ceil(maxX);\n\n// --- Clade colors (Imprint palette, canonical order) -----------------------\nconst cladeColors = {\n  strepsirrhini: t.palette[0], // brand green\n  platyrrhini: t.palette[1], // lavender\n  cercopithecidae: t.palette[2], // blue\n  hominidae: t.palette[3], // ochre — great apes, incl. the human lineage\n};\nconst cladeNames = {\n  strepsirrhini: \"Strepsirrhini\",\n  platyrrhini: \"Platyrrhini\",\n  cercopithecidae: \"Cercopithecidae\",\n  hominidae: \"Hominidae (great apes)\",\n};\nconst BACKBONE = t.inkSoft;\n\n// --- Collect elbow segments (horizontal branch + vertical connector) ------\nconst segments = [];\nconst leaves = [];\nfunction collect(node, parentX, inheritedColor) {\n  const color = node.clade ? cladeColors[node.clade] : inheritedColor;\n  const isLeaf = !node.children;\n  segments.push({\n    points: [\n      { x: parentX, y: node.y },\n      { x: node.x, y: node.y },\n    ],\n    color,\n    leaf: isLeaf,\n  });\n  if (isLeaf) {\n    leaves.push({ name: node.name, y: node.y, color });\n    return;\n  }\n  const ys = node.children.map((c) => c.y);\n  segments.push({\n    points: [\n      { x: node.x, y: Math.min(...ys) },\n      { x: node.x, y: Math.max(...ys) },\n    ],\n    color,\n    leaf: false,\n  });\n  node.children.forEach((c) => collect(c, node.x, color));\n}\nconst rootYs = tree.children.map((c) => c.y);\nsegments.push({\n  points: [\n    { x: tree.x, y: Math.min(...rootYs) },\n    { x: tree.x, y: Math.max(...rootYs) },\n  ],\n  color: BACKBONE,\n  leaf: false,\n});\ntree.children.forEach((c) => collect(c, tree.x, BACKBONE));\n\n// --- Mount -------------------------------------------------------------\ndocument.getElementById(\"container\").style.background = t.pageBg;\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart ---------------------------------------------------------------\nconst TITLE = \"tree-phylogenetic · javascript · chartjs · anyplot.ai\";\nconst SUBTITLE =\n  \"Branch & label color marks family-level clades — see legend below\";\n\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: {\n    datasets: segments.map((s) => ({\n      data: s.points,\n      showLine: true,\n      borderColor: s.color,\n      borderWidth: s.leaf ? 3 : 2.5,\n      pointRadius: s.leaf ? [0, 5] : 0,\n      pointBackgroundColor: s.color,\n      pointBorderColor: t.pageBg,\n      pointBorderWidth: 1.5,\n      fill: false,\n      tension: 0,\n    })),\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: TITLE,\n        color: t.ink,\n        font: { size: 22, weight: \"500\" },\n        padding: { top: 20, bottom: 8 },\n      },\n      subtitle: {\n        display: true,\n        text: SUBTITLE,\n        color: t.inkSoft,\n        font: { size: 14, style: \"italic\" },\n        padding: { bottom: 18 },\n      },\n      legend: {\n        display: true,\n        position: \"bottom\",\n        onClick: () => {},\n        labels: {\n          generateLabels: () =>\n            Object.keys(cladeColors).map((key) => ({\n              text: cladeNames[key],\n              fillStyle: cladeColors[key],\n              strokeStyle: cladeColors[key],\n              lineWidth: 0,\n              pointStyle: \"circle\",\n            })),\n          color: t.ink,\n          font: { size: 14 },\n          usePointStyle: true,\n          boxWidth: 10,\n          boxHeight: 10,\n          padding: 20,\n        },\n      },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: -0.3,\n        max: maxXTick + 0.3,\n        border: { color: t.inkSoft },\n        grid: { color: t.grid },\n        title: {\n          display: true,\n          text: \"Relative evolutionary distance\",\n          color: t.ink,\n          font: { size: 16 },\n        },\n        afterBuildTicks: (axis) => {\n          axis.ticks = Array.from({ length: maxXTick + 1 }, (_, i) => ({\n            value: i,\n          }));\n        },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n      },\n      y: {\n        type: \"linear\",\n        position: \"right\",\n        reverse: true,\n        min: -0.7,\n        max: n - 1 + 0.7,\n        grid: { display: false },\n        border: { display: false },\n        afterBuildTicks: (axis) => {\n          axis.ticks = Array.from({ length: n }, (_, i) => ({ value: i }));\n        },\n        ticks: {\n          color: (ctx) => {\n            const idx = Math.round(ctx.tick.value);\n            return leaves[idx] ? leaves[idx].color : t.inkSoft;\n          },\n          font: { size: 14, weight: \"500\" },\n          padding: 8,\n          callback: (value) => {\n            const idx = Math.round(value);\n            return leaves[idx] ? leaves[idx].name : \"\";\n          },\n        },\n      },\n    },\n  },\n});\n"}