{"spec_id":"dendrogram-radial","library":"d3","language":"javascript","code":"// anyplot.ai\n// dendrogram-radial: Radial Dendrogram\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 87/100 | Updated: 2026-09-05\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data: a stylized programming-language family tree ---------------------\n// Leaves are languages; internal nodes are lineages. `cluster` tags the five\n// top-level lineages and is inherited down to every descendant so branches\n// and leaf labels can be colored by cluster, mirroring a clustering\n// dendrogram's color-coded cluster assignment. ~85 leaves showcases the\n// radial layout's space-efficiency advantage over a linear dendrogram.\nconst familyTree = {\n  name: \"root\",\n  children: [\n    {\n      name: \"Systems & Low-Level\",\n      cluster: 0,\n      children: [\n        {\n          name: \"C family\",\n          children: [\n            { name: \"C\" },\n            { name: \"C++\" },\n            { name: \"Objective-C\" },\n            { name: \"D\" },\n            { name: \"Cyclone\" },\n          ],\n        },\n        { name: \"Rust\" },\n        { name: \"Ada\" },\n        { name: \"Zig\" },\n        { name: \"Swift\" },\n        { name: \"Go\" },\n        { name: \"Nim\" },\n        { name: \"Forth\" },\n        { name: \"Assembly\" },\n        { name: \"Pascal\" },\n        { name: \"Modula-2\" },\n        { name: \"Vala\" },\n        { name: \"Odin\" },\n        { name: \"Crystal\" },\n        { name: \"V\" },\n      ],\n    },\n    {\n      name: \"Enterprise OOP\",\n      cluster: 1,\n      children: [\n        {\n          name: \"JVM family\",\n          children: [\n            { name: \"Java\" },\n            { name: \"Kotlin\" },\n            { name: \"Scala\" },\n            { name: \"Groovy\" },\n            { name: \"Xtend\" },\n            { name: \"Ceylon\" },\n          ],\n        },\n        { name: \"C#\" },\n        { name: \"VB.NET\" },\n        { name: \"Eiffel\" },\n        { name: \"Smalltalk\" },\n        { name: \"Dart\" },\n        { name: \"COBOL\" },\n        { name: \"Object Pascal\" },\n        { name: \"ABAP\" },\n        { name: \"PL/I\" },\n      ],\n    },\n    {\n      name: \"Functional\",\n      cluster: 2,\n      children: [\n        {\n          name: \"Lisp family\",\n          children: [\n            { name: \"Common Lisp\" },\n            { name: \"Scheme\" },\n            { name: \"Racket\" },\n            { name: \"Clojure\" },\n            { name: \"Hy\" },\n          ],\n        },\n        {\n          name: \"ML family\",\n          children: [\n            { name: \"OCaml\" },\n            { name: \"F#\" },\n            { name: \"Haskell\" },\n            { name: \"Elm\" },\n            { name: \"Standard ML\" },\n            { name: \"ReasonML\" },\n            { name: \"PureScript\" },\n            { name: \"Idris\" },\n          ],\n        },\n        {\n          name: \"BEAM family\",\n          children: [{ name: \"Erlang\" }, { name: \"Elixir\" }, { name: \"Gleam\" }],\n        },\n        { name: \"Agda\" },\n        { name: \"Lean\" },\n        { name: \"Miranda\" },\n      ],\n    },\n    {\n      name: \"Dynamic Scripting\",\n      cluster: 3,\n      children: [\n        {\n          name: \"Web family\",\n          children: [{ name: \"JavaScript\" }, { name: \"TypeScript\" }, { name: \"CoffeeScript\" }],\n        },\n        { name: \"Python\" },\n        { name: \"Ruby\" },\n        { name: \"PHP\" },\n        { name: \"Perl\" },\n        { name: \"Lua\" },\n        { name: \"Bash\" },\n        { name: \"PowerShell\" },\n        { name: \"Tcl\" },\n        { name: \"Raku\" },\n        { name: \"AWK\" },\n        { name: \"Io\" },\n        { name: \"Zsh\" },\n        { name: \"Fish\" },\n      ],\n    },\n    {\n      name: \"Data & Scientific\",\n      cluster: 4,\n      children: [\n        { name: \"R\" },\n        { name: \"Julia\" },\n        { name: \"MATLAB\" },\n        { name: \"Fortran\" },\n        { name: \"SQL\" },\n        { name: \"SAS\" },\n        { name: \"APL\" },\n        { name: \"Mathematica\" },\n        { name: \"Octave\" },\n        { name: \"Stata\" },\n        { name: \"J\" },\n        { name: \"K\" },\n        { name: \"SPSS\" },\n        { name: \"GAMS\" },\n        { name: \"AMPL\" },\n        { name: \"MiniZinc\" },\n        { name: \"Prolog\" },\n      ],\n    },\n  ],\n};\n\n// Deterministic LCG so merge-distance gaps are reproducible without Math.random.\nlet seed = 42;\nconst nextRandom = () => {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n};\n\n// Bottom-up merge distance: leaves sit at distance 0, every internal node's\n// distance is the max of its children plus a random positive gap — the same\n// \"later merges join at greater dissimilarity\" property a linkage matrix encodes.\nconst assignDistance = (node) => {\n  if (!node.children) {\n    node.distance = 0;\n    return 0;\n  }\n  const maxChild = Math.max(...node.children.map(assignDistance));\n  node.distance = maxChild + 8 + nextRandom() * 12;\n  return node.distance;\n};\nassignDistance(familyTree);\nconst maxDistance = familyTree.distance;\n\n// Propagate each top-level lineage's cluster id down to its descendants.\nconst propagateCluster = (node, inherited) => {\n  const cluster = node.cluster !== undefined ? node.cluster : inherited;\n  node.cluster = cluster;\n  if (node.children) node.children.forEach((child) => propagateCluster(child, cluster));\n};\npropagateCluster(familyTree, null);\n\n// --- Layout ------------------------------------------------------------------\nconst titleSpace = 90;\nconst cx = width / 2;\nconst outerRadius = Math.min(width, height - titleSpace) / 2 - 150;\n\nconst root = d3.hierarchy(familyTree);\nd3.cluster().size([2 * Math.PI, outerRadius])(root);\nroot.each((d) => {\n  d.r = outerRadius * (1 - d.data.distance / maxDistance);\n});\n\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n// The vertical translate is resolved after the content (including label\n// extents) is drawn, so the visual bounding box — not just the layout\n// radius — is centered in the space below the title.\nconst g = svg.append(\"g\");\n\n// --- Branches, colored by inherited cluster ----------------------------------\ng.append(\"g\")\n  .attr(\"fill\", \"none\")\n  .selectAll(\"path\")\n  .data(root.links())\n  .join(\"path\")\n  .attr(\n    \"d\",\n    d3\n      .linkRadial()\n      .angle((d) => d.x)\n      .radius((d) => d.r),\n  )\n  .attr(\"stroke\", (d) => (d.target.data.cluster != null ? t.palette[d.target.data.cluster] : t.inkSoft))\n  .attr(\"stroke-width\", (d) => (d.target.children ? 2.2 : 1.6))\n  .attr(\"stroke-opacity\", 0.85);\n\n// --- Internal merge points ----------------------------------------------------\ng.append(\"g\")\n  .selectAll(\"circle\")\n  .data(root.descendants().filter((d) => d.children))\n  .join(\"circle\")\n  .attr(\"transform\", (d) => `rotate(${(d.x * 180) / Math.PI - 90}) translate(${d.r},0)`)\n  .attr(\"r\", (d) => (d.depth === 0 ? 0 : 3))\n  .attr(\"fill\", t.pageBg)\n  .attr(\"stroke\", (d) => (d.data.cluster != null ? t.palette[d.data.cluster] : t.inkSoft))\n  .attr(\"stroke-width\", 1.6);\n\n// --- Leaf tips -----------------------------------------------------------------\ng.append(\"g\")\n  .selectAll(\"circle\")\n  .data(root.leaves())\n  .join(\"circle\")\n  .attr(\"transform\", (d) => `rotate(${(d.x * 180) / Math.PI - 90}) translate(${d.r},0)`)\n  .attr(\"r\", 4)\n  .attr(\"fill\", (d) => t.palette[d.data.cluster]);\n\n// --- Leaf labels -----------------------------------------------------------------\ng.append(\"g\")\n  .selectAll(\"text\")\n  .data(root.leaves())\n  .join(\"text\")\n  .attr(\n    \"transform\",\n    (d) =>\n      `rotate(${(d.x * 180) / Math.PI - 90}) translate(${d.r + 10},0) ${d.x >= Math.PI ? \"rotate(180)\" : \"\"}`,\n  )\n  .attr(\"text-anchor\", (d) => (d.x < Math.PI ? \"start\" : \"end\"))\n  .attr(\"dy\", \"0.32em\")\n  .attr(\"fill\", (d) => t.palette[d.data.cluster])\n  .style(\"font-size\", \"13px\")\n  .style(\"font-family\", \"sans-serif\")\n  .text((d) => d.data.name);\n\n// Balance the visual bounding box (branches + markers + label extents)\n// within the drawable band below the title, instead of only centering the\n// bare layout radius — leaf labels near the bottom of the circle otherwise\n// push further down than the top, leaving unequal whitespace.\nconst bbox = g.node().getBBox();\nconst cy = (height + titleSpace) / 2 - bbox.y - bbox.height / 2;\ng.attr(\"transform\", `translate(${cx},${cy})`);\n\n// --- Legend --------------------------------------------------------------------\nconst legendEntries = familyTree.children.map((node) => ({\n  name: node.name,\n  color: t.palette[node.cluster],\n}));\nconst legend = svg.append(\"g\").attr(\"transform\", `translate(40, ${titleSpace + 10})`);\nlegend\n  .selectAll(\"g\")\n  .data(legendEntries)\n  .join(\"g\")\n  .attr(\"transform\", (_, i) => `translate(0, ${i * 28})`)\n  .call((sel) => {\n    sel\n      .append(\"rect\")\n      .attr(\"width\", 14)\n      .attr(\"height\", 14)\n      .attr(\"y\", -11)\n      .attr(\"fill\", (d) => d.color);\n    sel\n      .append(\"text\")\n      .attr(\"x\", 20)\n      .attr(\"fill\", t.inkSoft)\n      .style(\"font-size\", \"14px\")\n      .style(\"font-family\", \"sans-serif\")\n      .text((d) => d.name);\n  });\n\n// --- Title -----------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 44)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .style(\"font-family\", \"sans-serif\")\n  .text(\"dendrogram-radial · javascript · d3 · anyplot.ai\");\n"}