{"spec_id":"dendrogram-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// dendrogram-basic: Basic Dendrogram\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 89/100 | Created: 2026-06-18\n\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: iris measurements (sepal_len, sepal_wid, petal_len, petal_wid) ---\nconst ITEMS = [\n  { label: \"Setosa 1\",    f: [5.1, 3.5, 1.4, 0.2] },\n  { label: \"Setosa 2\",    f: [4.9, 3.0, 1.4, 0.2] },\n  { label: \"Setosa 3\",    f: [4.7, 3.2, 1.3, 0.2] },\n  { label: \"Setosa 4\",    f: [5.0, 3.6, 1.4, 0.2] },\n  { label: \"Versicolor 1\", f: [7.0, 3.2, 4.7, 1.4] },\n  { label: \"Versicolor 2\", f: [6.4, 3.2, 4.5, 1.5] },\n  { label: \"Versicolor 3\", f: [6.9, 3.1, 4.9, 1.5] },\n  { label: \"Versicolor 4\", f: [5.5, 2.3, 4.0, 1.3] },\n  { label: \"Virginica 1\", f: [6.3, 3.3, 6.0, 2.5] },\n  { label: \"Virginica 2\", f: [5.8, 2.7, 5.1, 1.9] },\n  { label: \"Virginica 3\", f: [7.1, 3.0, 5.9, 2.1] },\n  { label: \"Virginica 4\", f: [6.3, 2.9, 5.6, 1.8] },\n];\n\nconst n = ITEMS.length;\n\n// --- Pairwise Euclidean distances ---\nconst distMat = Array.from({ length: n }, (_, i) =>\n  Array.from({ length: n }, (_, j) => {\n    const a = ITEMS[i].f, b = ITEMS[j].f;\n    return Math.sqrt(a.reduce((s, v, k) => s + (v - b[k]) ** 2, 0));\n  })\n);\n\n// --- Average-linkage agglomerative clustering ---\nfunction buildTree() {\n  const leaves = ITEMS.map((_, i) => ({ type: \"leaf\", idx: i }));\n  let active = leaves.map((node, i) => ({ node, members: [i] }));\n\n  while (active.length > 1) {\n    let minD = Infinity, mi = 0, mj = 1;\n    for (let i = 0; i < active.length; i++) {\n      for (let j = i + 1; j < active.length; j++) {\n        let sum = 0;\n        for (const a of active[i].members)\n          for (const b of active[j].members)\n            sum += distMat[a][b];\n        const d = sum / (active[i].members.length * active[j].members.length);\n        if (d < minD) { minD = d; mi = i; mj = j; }\n      }\n    }\n    const merged = {\n      node: {\n        type: \"internal\",\n        left: active[mi].node,\n        right: active[mj].node,\n        height: minD,\n      },\n      members: [...active[mi].members, ...active[mj].members],\n    };\n    active = active.filter((_, k) => k !== mi && k !== mj);\n    active.push(merged);\n  }\n  return active[0].node;\n}\n\nconst root = buildTree();\n\n// --- DFS to determine left-to-right leaf order ---\nconst leafOrder = [];\n(function dfs(node) {\n  if (node.type === \"leaf\") { leafOrder.push(node.idx); return; }\n  dfs(node.left);\n  dfs(node.right);\n})(root);\n\nconst leafPos = {};\nleafOrder.forEach((idx, pos) => { leafPos[idx] = pos; });\n\n// --- Species → Imprint palette color ---\nfunction speciesColor(name) {\n  if (name === \"Setosa\")   return t.palette[0];  // brand green\n  if (name === \"Versicolor\") return t.palette[1];  // lavender\n  return t.palette[2];                            // blue\n}\n\n// --- Collect bridge segments (U-shaped connectors at each merge height) ---\nconst bridges = [];\n(function collectBridges(node) {\n  if (node.type === \"leaf\") {\n    const species = ITEMS[node.idx].label.split(\" \")[0];\n    return { x: leafPos[node.idx], topH: 0, species: new Set([species]) };\n  }\n  const left  = collectBridges(node.left);\n  const right = collectBridges(node.right);\n  const h     = node.height;\n  const merged = new Set([...left.species, ...right.species]);\n  const color  = merged.size === 1 ? speciesColor([...merged][0]) : t.inkSoft;\n\n  bridges.push({\n    points: [\n      { x: left.x,  y: left.topH  },\n      { x: left.x,  y: h          },\n      { x: right.x, y: h          },\n      { x: right.x, y: right.topH },\n    ],\n    color,\n  });\n  return { x: (left.x + right.x) / 2, topH: h, species: merged };\n})(root);\n\nconst maxH = Math.max(...bridges.map(b => b.points[1].y)) * 1.12;\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 = \"dendrogram-basic · javascript · chartjs · anyplot.ai\";\n\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: {\n    datasets: bridges.map((b) => ({\n      data: b.points,\n      showLine: true,\n      borderColor: b.color,\n      borderWidth: 2.5,\n      pointRadius: 0,\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: 16 },\n      },\n      legend: { display: false },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: -0.5,\n        max: n - 0.5,\n        border: { color: t.inkSoft },\n        grid: { color: t.grid },\n        afterBuildTicks: (axis) => {\n          axis.ticks = Array.from({ length: n }, (_, i) => ({ value: i }));\n        },\n        ticks: {\n          callback: (value) => {\n            const idx = Math.round(value);\n            if (idx >= 0 && idx < n) return ITEMS[leafOrder[idx]].label;\n            return null;\n          },\n          color: t.inkSoft,\n          font: { size: 13 },\n          maxRotation: 45,\n          minRotation: 45,\n        },\n      },\n      y: {\n        min: 0,\n        max: maxH,\n        border: { color: t.inkSoft },\n        grid: { color: t.grid },\n        title: {\n          display: true,\n          text: \"Euclidean Distance\",\n          color: t.ink,\n          font: { size: 16 },\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n        },\n      },\n    },\n  },\n});\n"}