{"spec_id":"dendrogram-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// dendrogram-basic: Basic Dendrogram\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 88/100 | Created: 2026-06-18\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\nconst margin = { top: 80, right: 185, bottom: 130, left: 90 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// Programming language similarity — pre-computed hierarchical clustering\n// h = normalized merge height: 0.0 = leaf (most similar), 1.0 = root (most distant)\nconst treeData = {\n  name: \"root\", h: 1.00,\n  children: [\n    {\n      name: \"compiled\", h: 0.65,\n      children: [\n        {\n          name: \"c_family\", h: 0.22,\n          children: [\n            { name: \"C\", h: 0, family: 0 },\n            { name: \"C++\", h: 0, family: 0 },\n            { name: \"Obj-C\", h: 0, family: 0 }\n          ]\n        },\n        {\n          name: \"jvm\", h: 0.38,\n          children: [\n            {\n              name: \"jvm_base\", h: 0.18,\n              children: [\n                { name: \"Java\", h: 0, family: 1 },\n                { name: \"Kotlin\", h: 0, family: 1 }\n              ]\n            },\n            { name: \"Scala\", h: 0, family: 1 }\n          ]\n        }\n      ]\n    },\n    {\n      name: \"dynamic\", h: 0.88,\n      children: [\n        {\n          name: \"functional\", h: 0.46,\n          children: [\n            {\n              name: \"core_func\", h: 0.26,\n              children: [\n                { name: \"Haskell\", h: 0, family: 2 },\n                { name: \"Erlang\", h: 0, family: 2 }\n              ]\n            },\n            { name: \"Elixir\", h: 0, family: 2 }\n          ]\n        },\n        {\n          name: \"scripting\", h: 0.62,\n          children: [\n            {\n              name: \"web\", h: 0.12,\n              children: [\n                { name: \"JavaScript\", h: 0, family: 3 },\n                { name: \"TypeScript\", h: 0, family: 3 }\n              ]\n            },\n            {\n              name: \"data_sci\", h: 0.44,\n              children: [\n                {\n                  name: \"py_r\", h: 0.30,\n                  children: [\n                    { name: \"Python\", h: 0, family: 4 },\n                    { name: \"R\", h: 0, family: 4 }\n                  ]\n                },\n                { name: \"Julia\", h: 0, family: 4 }\n              ]\n            }\n          ]\n        }\n      ]\n    }\n  ]\n};\n\n// 5 language families — Imprint palette, skipping index 4 (semantic red)\nconst families = [\n  { name: \"C / Systems\",   color: t.palette[0] },\n  { name: \"JVM Languages\", color: t.palette[1] },\n  { name: \"Functional\",    color: t.palette[2] },\n  { name: \"Web\",           color: t.palette[3] },\n  { name: \"Data Science\",  color: t.palette[5] }\n];\n\n// Build hierarchy and compute x-positions via cluster layout\nconst root = d3.hierarchy(treeData);\nconst clusterLayout = d3.cluster()\n  .size([iw, ih])\n  .separation(() => 1);\nclusterLayout(root);\n\n// Override y with merge-height positions (h=0 → bottom, h=1 → top)\nroot.descendants().forEach(d => {\n  d.y = ih * (1 - d.data.h);\n});\n\n// Compute dominant family for each node (-1 = mixed subtree)\nroot.descendants().forEach(d => {\n  if (d.data.family !== undefined) {\n    d._family = d.data.family;\n  } else {\n    const familySet = new Set(d.leaves().map(leaf => leaf.data.family));\n    d._family = familySet.size === 1 ? [...familySet][0] : -1;\n  }\n});\n\n// SVG\nconst svg = d3.select(\"#container\")\n  .append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\n\nconst g = svg.append(\"g\")\n  .attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// Y scale for the distance axis\nconst yScale = d3.scaleLinear()\n  .domain([0, 1])\n  .range([ih, 0]);\n\n// Horizontal gridlines\ng.selectAll(\"line.grid\")\n  .data(yScale.ticks(5))\n  .join(\"line\")\n  .attr(\"class\", \"grid\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", d => yScale(d))\n  .attr(\"y2\", d => yScale(d))\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// Internal node horizontal bars (spanning children's x range)\nconst internals = root.descendants().filter(d => d.children);\n\ng.selectAll(\"line.hbar\")\n  .data(internals)\n  .join(\"line\")\n  .attr(\"class\", \"hbar\")\n  .attr(\"x1\", d => d3.min(d.children, c => c.x))\n  .attr(\"y1\", d => d.y)\n  .attr(\"x2\", d => d3.max(d.children, c => c.x))\n  .attr(\"y2\", d => d.y)\n  .attr(\"stroke\", d => d._family >= 0 ? families[d._family].color : t.inkSoft)\n  .attr(\"stroke-width\", 2)\n  .attr(\"stroke-linecap\", \"round\");\n\n// Vertical bars: each node up to its parent's merge height\nconst nonRoot = root.descendants().filter(d => d.parent);\n\ng.selectAll(\"line.vbar\")\n  .data(nonRoot)\n  .join(\"line\")\n  .attr(\"class\", \"vbar\")\n  .attr(\"x1\", d => d.x)\n  .attr(\"y1\", d => d.parent.y)\n  .attr(\"x2\", d => d.x)\n  .attr(\"y2\", d => d.y)\n  .attr(\"stroke\", d => d._family >= 0 ? families[d._family].color : t.inkSoft)\n  .attr(\"stroke-width\", 2)\n  .attr(\"stroke-linecap\", \"round\");\n\n// Leaf circles (colored by language family)\nconst leaves = root.leaves();\n\ng.selectAll(\"circle.leaf\")\n  .data(leaves)\n  .join(\"circle\")\n  .attr(\"class\", \"leaf\")\n  .attr(\"cx\", d => d.x)\n  .attr(\"cy\", d => d.y)\n  .attr(\"r\", 5)\n  .attr(\"fill\", d => families[d.data.family].color)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2);\n\n// Leaf labels (rotated -45° so they extend down-left from the leaf)\ng.selectAll(\"text.leaf-label\")\n  .data(leaves)\n  .join(\"text\")\n  .attr(\"class\", \"leaf-label\")\n  .attr(\"transform\", d => `translate(${d.x},${d.y + 12}) rotate(-45)`)\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", d => families[d.data.family].color)\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", \"500\")\n  .text(d => d.data.name);\n\n// Left Y axis (merge distance)\nconst yAxisGroup = g.append(\"g\")\n  .call(\n    d3.axisLeft(yScale)\n      .ticks(5)\n      .tickFormat(d3.format(\".1f\"))\n  );\n\nyAxisGroup.selectAll(\"text\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\");\nyAxisGroup.selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\nyAxisGroup.select(\".domain\")\n  .attr(\"stroke\", t.inkSoft);\n\n// Y axis label\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -65)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"Merge Distance\");\n\n// Legend (inside right margin)\nconst legendG = g.append(\"g\")\n  .attr(\"transform\", `translate(${iw + 22}, 10)`);\n\nfamilies.forEach((fam, i) => {\n  const row = legendG.append(\"g\")\n    .attr(\"transform\", `translate(0, ${i * 26})`);\n\n  row.append(\"circle\")\n    .attr(\"r\", 6)\n    .attr(\"cx\", 6)\n    .attr(\"fill\", fam.color);\n\n  row.append(\"text\")\n    .attr(\"x\", 18)\n    .attr(\"y\", 5)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\")\n    .text(fam.name);\n});\n\n// Title\nsvg.append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 50)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"dendrogram-basic · javascript · d3 · anyplot.ai\");\n"}