{"spec_id":"dendrogram-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// dendrogram-basic: Basic Dendrogram\n// Library: muix 7.29.1 | JavaScript 22.22.3\n// Quality: 90/100 | Created: 2026-06-18\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\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, useDrawingArea } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst FONT = \"Inter, system-ui, -apple-system, sans-serif\";\n\n// Iris samples: sepal_length, sepal_width, petal_length, petal_width\nconst samples = [\n  { label: \"S-01\", features: [5.1, 3.5, 1.4, 0.2], species: 0 },\n  { label: \"S-02\", features: [4.9, 3.0, 1.4, 0.2], species: 0 },\n  { label: \"S-03\", features: [4.7, 3.2, 1.3, 0.2], species: 0 },\n  { label: \"S-04\", features: [5.0, 3.6, 1.4, 0.2], species: 0 },\n  { label: \"S-05\", features: [5.4, 3.9, 1.7, 0.4], species: 0 },\n  { label: \"Ve-01\", features: [7.0, 3.2, 4.7, 1.4], species: 1 },\n  { label: \"Ve-02\", features: [6.4, 3.2, 4.5, 1.5], species: 1 },\n  { label: \"Ve-03\", features: [6.9, 3.1, 4.9, 1.5], species: 1 },\n  { label: \"Ve-04\", features: [5.5, 2.3, 4.0, 1.3], species: 1 },\n  { label: \"Ve-05\", features: [6.5, 2.8, 4.6, 1.5], species: 1 },\n  { label: \"Vi-01\", features: [6.3, 3.3, 6.0, 2.5], species: 2 },\n  { label: \"Vi-02\", features: [5.8, 2.7, 5.1, 1.9], species: 2 },\n  { label: \"Vi-03\", features: [7.1, 3.0, 5.9, 2.1], species: 2 },\n  { label: \"Vi-04\", features: [6.3, 2.9, 5.6, 1.8], species: 2 },\n  { label: \"Vi-05\", features: [6.5, 3.0, 5.8, 2.2], species: 2 },\n];\n\nconst SPECIES_NAMES = [\"Iris setosa\", \"Iris versicolor\", \"Iris virginica\"];\n\nfunction euclid(a, b) {\n  return Math.sqrt(a.features.reduce((s, v, i) => s + (v - b.features[i]) ** 2, 0));\n}\n\nfunction buildTree(pts) {\n  let nodes = pts.map((_, i) => ({ height: 0, leaves: [i], children: null, x: 0 }));\n  while (nodes.length > 1) {\n    let minD = Infinity, mi = 0, mj = 1;\n    for (let i = 0; i < nodes.length; i++) {\n      for (let j = i + 1; j < nodes.length; j++) {\n        let d = 0;\n        for (const li of nodes[i].leaves)\n          for (const lj of nodes[j].leaves)\n            d = Math.max(d, euclid(pts[li], pts[lj]));\n        if (d < minD) { minD = d; mi = i; mj = j; }\n      }\n    }\n    const merged = {\n      height: minD,\n      leaves: [...nodes[mi].leaves, ...nodes[mj].leaves],\n      children: [nodes[mi], nodes[mj]],\n      x: 0,\n    };\n    nodes.splice(mj, 1);\n    nodes.splice(mi, 1);\n    nodes.push(merged);\n  }\n  return nodes[0];\n}\n\nfunction leafOrder(node) {\n  if (!node.children) return [node.leaves[0]];\n  return [...leafOrder(node.children[0]), ...leafOrder(node.children[1])];\n}\n\nfunction assignX(node, xMap) {\n  if (!node.children) { node.x = xMap[node.leaves[0]]; return; }\n  assignX(node.children[0], xMap);\n  assignX(node.children[1], xMap);\n  node.x = (node.children[0].x + node.children[1].x) / 2;\n}\n\nfunction getSegments(node) {\n  if (!node.children) return [];\n  const [l, r] = node.children;\n  return [\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    ...getSegments(l),\n    ...getSegments(r),\n  ];\n}\n\nconst root = buildTree(samples);\nconst n = samples.length;\nconst order = leafOrder(root);\nconst xMap = {};\norder.forEach((li, pos) => { xMap[li] = pos; });\nassignX(root, xMap);\nconst maxH = root.height;\nconst segs = getSegments(root);\n\nconst X_MIN = -0.5;\nconst X_MAX = n - 0.5;\nconst Y_MAX = maxH * 1.08;\nconst orderedLabels = order.map((li) => samples[li].label);\n\n// Three scatter series — one per species — for species-coloured leaf dots\nconst scatterSeries = [0, 1, 2].map((sp) => ({\n  type: \"scatter\",\n  data: order\n    .map((li, pos) => samples[li].species === sp ? { id: `sp${sp}-${pos}`, x: pos, y: 0 } : null)\n    .filter(Boolean),\n  label: SPECIES_NAMES[sp],\n  color: t.palette[sp],\n  markerSize: 8,\n}));\n\nconst TITLE_H = 60;\n\n// Dendrogram branch lines — drawn on the MUI X coordinate system\nfunction DendrogramBranches() {\n  const xs = useXScale();\n  const ys = useYScale();\n  if (!xs || !ys) return null;\n  return (\n    <g>\n      {segs.map((s, i) => (\n        <line\n          key={i}\n          x1={xs(s.x1)} y1={ys(s.y1)}\n          x2={xs(s.x2)} y2={ys(s.y2)}\n          stroke={t.ink}\n          strokeWidth={2}\n          strokeLinecap=\"round\"\n        />\n      ))}\n    </g>\n  );\n}\n\n// Species legend anchored to the right of the drawing area\nfunction Legend() {\n  const { left, top, width } = useDrawingArea();\n  const lx = left + width + 14;\n  return (\n    <g fontFamily={FONT}>\n      {SPECIES_NAMES.map((name, i) => (\n        <g key={i} transform={`translate(${lx}, ${top + 20 + i * 32})`}>\n          <circle cx={8} cy={0} r={7} fill={t.palette[i]} />\n          <text x={22} y={5} fontSize={14} fill={t.ink}>{name}</text>\n        </g>\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  return (\n    <div style={{\n      width: W, height: H, background: t.pageBg,\n      fontFamily: FONT, display: \"flex\", flexDirection: \"column\",\n    }}>\n      <div style={{ height: TITLE_H, display: \"flex\", alignItems: \"center\", justifyContent: \"center\" }}>\n        <span style={{ fontSize: 22, fontWeight: 600, color: t.ink }}>\n          dendrogram-basic · javascript · muix · anyplot.ai\n        </span>\n      </div>\n      <ChartContainer\n        width={W}\n        height={H - TITLE_H}\n        skipAnimation\n        series={scatterSeries}\n        margin={{ top: 16, right: 190, bottom: 110, left: 86 }}\n        xAxis={[{\n          min: X_MIN,\n          max: X_MAX,\n          tickInterval: Array.from({ length: n }, (_, i) => i),\n          valueFormatter: (v) => orderedLabels[Math.round(v)] ?? \"\",\n          tickLabelStyle: { angle: -45, textAnchor: \"end\", fontSize: 13, fontFamily: FONT },\n        }]}\n        yAxis={[{\n          min: 0,\n          max: Y_MAX,\n          label: \"Distance (Complete Linkage)\",\n          tickLabelStyle: { fontSize: 13, fontFamily: FONT },\n          labelStyle: { fontSize: 15, fontFamily: FONT },\n        }]}\n        sx={{\n          \"& .MuiChartsAxis-line\": { stroke: t.inkSoft },\n          \"& .MuiChartsAxis-tick\": { stroke: t.inkSoft },\n          \"& .MuiChartsLegend-root\": { display: \"none\" },\n        }}\n      >\n        <ChartsGrid horizontal />\n        <DendrogramBranches />\n        <ScatterPlot />\n        <ChartsXAxis />\n        <ChartsYAxis />\n        <Legend />\n      </ChartContainer>\n    </div>\n  );\n}\n"}