{"spec_id":"dendrogram-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// dendrogram-basic: Basic Dendrogram\n// Library: muix 9.16.0 | JavaScript 22.23.3\n// Quality: 86/100 | Updated: 2026-10-24\n\nimport { ChartsContainer } from \"@mui/x-charts/ChartsContainer\";\nimport { ScatterPlot } from \"@mui/x-charts/ScatterChart\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Constants\nconst TITLE = \"dendrogram-basic · javascript · muix · anyplot.ai\";\nconst FONT = \"Inter, system-ui, -apple-system, sans-serif\";\nconst DISTANCE_LABEL = \"Euclidean distance (complete linkage)\";\nconst Y_PAD_SHARE = 0.08;\nconst BRANCH_COLOR = t.palette[0];\n\n// Columns: set these to your data's column names\nconst LABEL = \"Sample ID\"; // role: labels\nconst SEPAL_L = \"Sepal length (cm)\"; // role: features\nconst SEPAL_W = \"Sepal width (cm)\"; // role: features\nconst PETAL_L = \"Petal length (cm)\"; // role: features\nconst PETAL_W = \"Petal width (cm)\"; // role: features\n\n// Data: replace this block with your own records\nconst data = [\n  { [LABEL]: \"S-01\", [SEPAL_L]: 5.1, [SEPAL_W]: 3.5, [PETAL_L]: 1.4, [PETAL_W]: 0.2 },\n  { [LABEL]: \"S-02\", [SEPAL_L]: 4.9, [SEPAL_W]: 3.0, [PETAL_L]: 1.4, [PETAL_W]: 0.2 },\n  { [LABEL]: \"S-03\", [SEPAL_L]: 4.7, [SEPAL_W]: 3.2, [PETAL_L]: 1.3, [PETAL_W]: 0.2 },\n  { [LABEL]: \"S-04\", [SEPAL_L]: 5.0, [SEPAL_W]: 3.6, [PETAL_L]: 1.4, [PETAL_W]: 0.2 },\n  { [LABEL]: \"S-05\", [SEPAL_L]: 5.4, [SEPAL_W]: 3.9, [PETAL_L]: 1.7, [PETAL_W]: 0.4 },\n  { [LABEL]: \"Ve-01\", [SEPAL_L]: 7.0, [SEPAL_W]: 3.2, [PETAL_L]: 4.7, [PETAL_W]: 1.4 },\n  { [LABEL]: \"Ve-02\", [SEPAL_L]: 6.4, [SEPAL_W]: 3.2, [PETAL_L]: 4.5, [PETAL_W]: 1.5 },\n  { [LABEL]: \"Ve-03\", [SEPAL_L]: 6.9, [SEPAL_W]: 3.1, [PETAL_L]: 4.9, [PETAL_W]: 1.5 },\n  { [LABEL]: \"Ve-04\", [SEPAL_L]: 5.5, [SEPAL_W]: 2.3, [PETAL_L]: 4.0, [PETAL_W]: 1.3 },\n  { [LABEL]: \"Ve-05\", [SEPAL_L]: 6.5, [SEPAL_W]: 2.8, [PETAL_L]: 4.6, [PETAL_W]: 1.5 },\n  { [LABEL]: \"Vi-01\", [SEPAL_L]: 6.3, [SEPAL_W]: 3.3, [PETAL_L]: 6.0, [PETAL_W]: 2.5 },\n  { [LABEL]: \"Vi-02\", [SEPAL_L]: 5.8, [SEPAL_W]: 2.7, [PETAL_L]: 5.1, [PETAL_W]: 1.9 },\n  { [LABEL]: \"Vi-03\", [SEPAL_L]: 7.1, [SEPAL_W]: 3.0, [PETAL_L]: 5.9, [PETAL_W]: 2.1 },\n  { [LABEL]: \"Vi-04\", [SEPAL_L]: 6.3, [SEPAL_W]: 2.9, [PETAL_L]: 5.6, [PETAL_W]: 1.8 },\n  { [LABEL]: \"Vi-05\", [SEPAL_L]: 6.5, [SEPAL_W]: 3.0, [PETAL_L]: 5.8, [PETAL_W]: 2.2 },\n];\n\n// Plot: reads only data and the constants above\nconst FEATURE_COLS = [SEPAL_L, SEPAL_W, PETAL_L, PETAL_W];\nconst n = data.length;\n\nconst distMatrix = Array.from({ length: n }, () => new Array(n).fill(0));\nfor (let i = 0; i < n; i++) {\n  for (let j = i + 1; j < n; j++) {\n    let sumSq = 0;\n    for (const col of FEATURE_COLS) {\n      const diff = Number(data[i][col]) - Number(data[j][col]);\n      sumSq += diff * diff;\n    }\n    const d = Math.sqrt(sumSq);\n    distMatrix[i][j] = d;\n    distMatrix[j][i] = d;\n  }\n}\n\ntype TreeNode = {\n  id: number;\n  height: number;\n  leaves: number[];\n  children: [TreeNode, TreeNode] | null;\n  x: number;\n};\n\nconst allNodes: TreeNode[] = data.map((_, i) => ({\n  id: i,\n  height: 0,\n  leaves: [i],\n  children: null,\n  x: 0,\n}));\n\nconst activeNodes: TreeNode[] = [...allNodes];\nwhile (activeNodes.length > 1) {\n  let minD = Infinity;\n  let mi = 0;\n  let mj = 1;\n  for (let i = 0; i < activeNodes.length; i++) {\n    for (let j = i + 1; j < activeNodes.length; j++) {\n      let d = 0;\n      for (const li of activeNodes[i].leaves) {\n        for (const lj of activeNodes[j].leaves) {\n          if (distMatrix[li][lj] > d) d = distMatrix[li][lj];\n        }\n      }\n      if (d < minD) {\n        minD = d;\n        mi = i;\n        mj = j;\n      }\n    }\n  }\n  const leftNode = activeNodes[mi];\n  const rightNode = activeNodes[mj];\n  const merged: TreeNode = {\n    id: allNodes.length,\n    height: minD,\n    leaves: [...leftNode.leaves, ...rightNode.leaves],\n    children: [leftNode, rightNode],\n    x: 0,\n  };\n  allNodes.push(merged);\n  activeNodes.splice(mj, 1);\n  activeNodes.splice(mi, 1);\n  activeNodes.push(merged);\n}\n\nconst root = activeNodes[0];\n\nconst order: number[] = [];\nconst stack: TreeNode[] = [root];\nwhile (stack.length > 0) {\n  const cur = stack.pop()!;\n  if (!cur.children) {\n    order.push(cur.leaves[0]);\n  } else {\n    stack.push(cur.children[1]);\n    stack.push(cur.children[0]);\n  }\n}\n\norder.forEach((leafIdx, pos) => {\n  allNodes[leafIdx].x = pos;\n});\nfor (let id = n; id < allNodes.length; id++) {\n  const node = allNodes[id];\n  if (node.children) {\n    node.x = (node.children[0].x + node.children[1].x) / 2;\n  }\n}\n\nconst segs: { x1: number; y1: number; x2: number; y2: number }[] = [];\nfor (let id = n; id < allNodes.length; id++) {\n  const node = allNodes[id];\n  if (!node.children) continue;\n  const [l, r] = node.children;\n  segs.push(\n    { x1: l.x, y1: node.height, x2: r.x, y2: node.height },\n    { x1: l.x, y1: l.height, x2: l.x, y2: node.height },\n    { x1: r.x, y1: r.height, x2: r.x, y2: node.height },\n  );\n}\n\nconst maxH = root.height;\nconst X_MIN = -0.5;\nconst X_MAX = n - 0.5;\nconst Y_MAX = maxH * (1 + Y_PAD_SHARE);\nconst orderedLabels = order.map((li) => String(data[li][LABEL]));\nconst SUBTITLE = `${n} items clustered over ${FEATURE_COLS.length} features · root merge height ${maxH.toFixed(2)}`;\n\nconst scatterSeries = [\n  {\n    type: \"scatter\" as const,\n    id: \"leaves\",\n    data: order.map((li, pos) => ({ id: `${li}`, x: pos, y: 0 })),\n    color: BRANCH_COLOR,\n    markerSize: 7,\n    xAxisId: \"x\",\n    yAxisId: \"y\",\n  },\n];\n\nfunction DendrogramBranches() {\n  const xs = useXScale(\"x\") as any;\n  const ys = useYScale(\"y\") as any;\n  return (\n    <g>\n      {segs.map((s, i) => (\n        <line\n          key={i}\n          x1={xs(s.x1)}\n          y1={ys(s.y1)}\n          x2={xs(s.x2)}\n          y2={ys(s.y2)}\n          stroke={BRANCH_COLOR}\n          strokeWidth={2.4}\n          strokeLinecap=\"round\"\n        />\n      ))}\n    </g>\n  );\n}\n\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width;\n  const H = window.ANYPLOT_SIZE.height;\n  const HEADER_H = 84;\n\n  return (\n    <div\n      style={{\n        width: W,\n        height: H,\n        background: t.pageBg,\n        fontFamily: FONT,\n        display: \"flex\",\n        flexDirection: \"column\",\n        alignItems: \"center\",\n      }}\n    >\n      <div\n        style={{\n          height: HEADER_H,\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          gap: 4,\n          paddingTop: 8,\n        }}\n      >\n        <span style={{ fontSize: 22, fontWeight: 700, color: t.ink }}>{TITLE}</span>\n        <span style={{ fontSize: 15, color: t.inkSoft }}>{SUBTITLE}</span>\n      </div>\n\n      <ChartsContainer\n        width={W - 48}\n        height={H - HEADER_H - 12}\n        skipAnimation\n        series={scatterSeries}\n        margin={{ top: 18, right: 36, bottom: 20, left: 24 }}\n        xAxis={[\n          {\n            id: \"x\",\n            min: X_MIN,\n            max: X_MAX,\n            height: 60,\n            label: LABEL,\n            tickInterval: Array.from({ length: n }, (_, i) => i),\n            valueFormatter: (v: number) => orderedLabels[Math.round(v)] ?? \"\",\n            tickLabelStyle: {\n              fontSize: 14,\n              fill: t.inkSoft,\n              fontFamily: FONT,\n            },\n            labelStyle: { fontSize: 15, fill: t.ink, fontFamily: FONT },\n          },\n        ]}\n        yAxis={[\n          {\n            id: \"y\",\n            min: 0,\n            max: Y_MAX,\n            width: 84,\n            label: DISTANCE_LABEL,\n            tickLabelStyle: { fontSize: 14, fill: t.inkSoft, fontFamily: FONT },\n            labelStyle: { fontSize: 15, fill: t.ink, fontFamily: FONT },\n          },\n        ]}\n        sx={{\n          \"& .MuiChartsAxis-line\": { stroke: t.inkSoft },\n          \"& .MuiChartsAxis-tick\": { stroke: t.inkSoft },\n          \"& .MuiChartsGrid-line\": { stroke: t.grid, strokeDasharray: \"4 4\" },\n        }}\n      >\n        <ChartsGrid horizontal />\n        <DendrogramBranches />\n        <ScatterPlot />\n        <ChartsXAxis axisId=\"x\" />\n        <ChartsYAxis axisId=\"y\" />\n      </ChartsContainer>\n    </div>\n  );\n}\n"}