{"spec_id":"tree-phylogenetic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// tree-phylogenetic: Phylogenetic Tree Diagram\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 95/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: primate phylogeny from mitochondrial DNA divergence --------------\n// Each node carries `length` = branch length to its parent, in substitutions\n// per site. `clade` marks the root of one of the four highlighted lineages;\n// descendants inherit it. Values are illustrative, not a literal alignment.\nconst TREE = {\n  name: \"Root\",\n  length: 0,\n  children: [\n    {\n      name: \"Prosimians\",\n      length: 0.32,\n      clade: 0,\n      children: [\n        { name: \"Lemur\", length: 0.28, children: [] },\n        { name: \"Galago\", length: 0.3, children: [] },\n      ],\n    },\n    {\n      name: \"Anthropoidea\",\n      length: 0.32,\n      children: [\n        {\n          name: \"New World Monkeys\",\n          length: 0.18,\n          clade: 1,\n          children: [\n            { name: \"Marmoset\", length: 0.14, children: [] },\n            { name: \"Squirrel Monkey\", length: 0.13, children: [] },\n          ],\n        },\n        {\n          name: \"Catarrhini\",\n          length: 0.14,\n          children: [\n            {\n              name: \"Old World Monkeys\",\n              length: 0.1,\n              clade: 2,\n              children: [\n                { name: \"Baboon\", length: 0.06, children: [] },\n                { name: \"Macaque\", length: 0.055, children: [] },\n              ],\n            },\n            {\n              name: \"Apes\",\n              length: 0.09,\n              clade: 3,\n              children: [\n                { name: \"Gibbon\", length: 0.085, children: [] },\n                {\n                  name: \"Great Apes\",\n                  length: 0.045,\n                  children: [\n                    { name: \"Orangutan\", length: 0.065, children: [] },\n                    {\n                      name: \"African Apes\",\n                      length: 0.035,\n                      children: [\n                        { name: \"Gorilla\", length: 0.045, children: [] },\n                        {\n                          name: \"Human-Chimp\",\n                          length: 0.02,\n                          children: [\n                            { name: \"Chimpanzee\", length: 0.018, children: [] },\n                            { name: \"Human\", length: 0.018, children: [] },\n                          ],\n                        },\n                      ],\n                    },\n                  ],\n                },\n              ],\n            },\n          ],\n        },\n      ],\n    },\n  ],\n};\n\nconst CLADE_NAMES = [\"Prosimians\", \"New World Monkeys\", \"Old World Monkeys\", \"Apes (Hominoidea)\"];\n\n// --- Layout: cumulative branch length -> x, leaf order -> y -----------------\n// Rectangular phylogram: each parent/child pair is joined via an invisible\n// corner node so branches meet at right angles, the classic cladogram style.\nconst nodes = [];\nconst edges = [];\nlet leafCount = 0;\n\nfunction colorFor(clade) {\n  return clade == null ? t.muted : t.palette[clade];\n}\n\nfunction walk(node, parentX, inheritedClade) {\n  const x = parentX + node.length;\n  const clade = node.clade != null ? node.clade : inheritedClade;\n  const isLeaf = node.children.length === 0;\n\n  if (isLeaf) {\n    const y = leafCount;\n    leafCount += 1;\n    nodes.push({\n      name: node.name,\n      x,\n      y,\n      length: node.length,\n      symbolSize: 16,\n      category: clade,\n      itemStyle: clade == null ? { color: t.muted } : undefined,\n      label: { show: true, position: \"right\", distance: 8, color: t.ink, fontSize: 15, fontWeight: 500 },\n    });\n    return { x, y, name: node.name };\n  }\n\n  const childResults = node.children.map((child) => walk(child, x, clade));\n  const ys = childResults.map((c) => c.y);\n  const y = (Math.min(...ys) + Math.max(...ys)) / 2;\n\n  nodes.push({\n    name: node.name,\n    x,\n    y,\n    length: node.length,\n    symbolSize: 9,\n    category: clade,\n    itemStyle: clade == null ? { color: t.muted } : undefined,\n    label: { show: false },\n  });\n\n  childResults.forEach((child, i) => {\n    const childClade = node.children[i].clade != null ? node.children[i].clade : clade;\n    const corner = `${node.name}->${child.name}`;\n    nodes.push({ name: corner, x, y: child.y, symbolSize: 0, silent: true, label: { show: false } });\n    edges.push({ source: node.name, target: corner, color: colorFor(childClade) });\n    edges.push({ source: corner, target: child.name, color: colorFor(childClade) });\n  });\n\n  return { x, y, name: node.name };\n}\n\nwalk(TREE, 0, null);\n\n// The shallowest split in the tree (Human-Chimp, 0.018) is the natural focal\n// point for a callout — annotate it directly on the static PNG.\nconst focalNode = nodes.find((n) => n.name === \"Human-Chimp\");\n\n// --- Init ---------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option ---------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"tree-phylogenetic · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 16,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 600 },\n  },\n  legend: {\n    data: CLADE_NAMES,\n    top: 58,\n    left: \"center\",\n    itemWidth: 16,\n    itemHeight: 12,\n    textStyle: { color: t.ink, fontSize: 15 },\n  },\n  tooltip: {\n    show: true,\n    trigger: \"item\",\n    backgroundColor: t.elevatedBg,\n    borderColor: t.grid,\n    textStyle: { color: t.ink, fontSize: 13 },\n    formatter: (params) => {\n      const d = params.data;\n      if (!d || !d.symbolSize) return \"\";\n      const cladeLabel = d.category != null ? CLADE_NAMES[d.category] : \"Unclustered (basal split)\";\n      const branch = typeof d.length === \"number\" ? `${d.length.toFixed(3)} subst./site` : \"—\";\n      return (\n        `<b>${d.name}</b><br/>Clade: ${cladeLabel}<br/>` +\n        `Branch length: ${branch}<br/>Cumulative distance: ${params.value[0].toFixed(3)}`\n      );\n    },\n  },\n  grid: { left: 24, right: 300, top: 108, bottom: 90 },\n  xAxis: {\n    type: \"value\",\n    min: 0,\n    name: \"Evolutionary distance (substitutions per site)\",\n    nameLocation: \"middle\",\n    nameGap: 42,\n    nameTextStyle: { color: t.inkSoft, fontSize: 15 },\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { onZero: false, lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  yAxis: {\n    type: \"value\",\n    show: false,\n    min: -1,\n    max: leafCount,\n    inverse: true,\n    axisLine: { onZero: false },\n  },\n  series: [\n    {\n      type: \"graph\",\n      coordinateSystem: \"cartesian2d\",\n      layout: \"none\",\n      symbol: \"circle\",\n      edgeSymbol: [\"none\", \"none\"],\n      categories: CLADE_NAMES.map((name, i) => ({ name, itemStyle: { color: t.palette[i] } })),\n      data: nodes.map((n) => ({\n        name: n.name,\n        value: [n.x, n.y],\n        symbolSize: n.symbolSize,\n        category: n.category,\n        itemStyle: n.itemStyle,\n        label: n.label,\n        silent: n.silent,\n      })),\n      edges: edges.map((e) => ({\n        source: e.source,\n        target: e.target,\n        lineStyle: { color: e.color, width: e.color === t.muted ? 2.2 : 3, curveness: 0 },\n      })),\n      emphasis: {\n        focus: \"adjacency\",\n        lineStyle: { width: 5 },\n        itemStyle: { borderColor: t.ink, borderWidth: 2 },\n        label: { fontWeight: 700 },\n      },\n      blur: { itemStyle: { opacity: 0.2 }, lineStyle: { opacity: 0.15 }, label: { opacity: 0.3 } },\n      z: 2,\n    },\n    {\n      // Static callout on the shortest split (Human-Chimp) — a zero-size,\n      // non-interactive scatter point used purely to place its label.\n      type: \"scatter\",\n      coordinateSystem: \"cartesian2d\",\n      silent: true,\n      z: 3,\n      data: [\n        {\n          value: [focalNode.x, focalNode.y],\n          symbolSize: 0,\n          label: {\n            show: true,\n            formatter: \"Shortest split:\\nHuman-Chimpanzee (0.018)\",\n            position: \"left\",\n            distance: 18,\n            align: \"right\",\n            color: t.inkSoft,\n            fontSize: 12,\n            fontStyle: \"italic\",\n            lineHeight: 16,\n          },\n        },\n      ],\n    },\n  ],\n});\n"}